我会用Ruby编写一个模块,可以完成两个任务:
1)在Ruby中执行各种XML输入格式的SAX解析,输出一个中间格式的XML文档和一个验证错误/键违规错误列表;
2)从中间格式的XML文件创建DOM树,在原地修改它,在导入数据的同时增强它,并将修改后的DOM树输出到标准格式。
使用SAX的第一步允许从文件中剥离冗余数据(并且不加载到DOM模型中!),并快速地将非冗余、所需的数据组放置在统一命名的标记中。为了最大限度地提高速度,在转换为中间格式XML之前,数据组不应以任何方式进行排序,并且中间格式XML应使用短标记名称。
使用DOM的第二步允许对未发现验证错误的中间格式XML的标记进行排序并快速处理。
在这里,验证错误指的是一系列问题,例如缺少字段或无效的键格式、数字范围等。它还会检测文件中缺失的键引用的对象;为此,它构建了两个哈希表,一个是引用的键,一个是现有的键,并在完成前的最后一步检查引用的键是否与现有的键匹配。虽然您可以使用XSD或DTD进行一些检查,但Ruby允许您更灵活地处理许多实际上是“软”错误的验证问题,其中可以进行一些有限的纠正。
该模块应该限制每个任务并行执行的数量,以避免系统耗尽CPU或RAM。
我的建议的核心是在Ruby中全部完成,但将工作分为两个阶段——第一阶段是那些可以使用SAX快速完成的任务,第二阶段是那些可以使用DOM快速完成的任务。
编辑:
>我们如何使用SAX进行结构转换?
好吧,你不能方便地进行任何类型的重新排序,否则你就不再真正获得逐个解析XML的内存使用优势了,但这里是一个使用Java的示例,说明我所说的第一阶段的方法(抱歉不是Ruby,但应该很容易翻译过来——把它看作伪代码!)。
class MySAXHandler implements org.xml.sax.ContentHandler extends Object {
final static int MAX_DEPTH=512;
final static int FILETYPE_A=1;
final static int FILETYPE_B=2;
String[] qualifiedNames = new String[MAX_DEPTH];
String[] localNames = new String[MAX_DEPTH];
String[] namespaceURIs = new String[MAX_DEPTH];
int[] meaning = new int[MAX_DEPTH];
int pathPos=0;
public java.io.Writer destination;
ArrayList errorList=new ArrayList();
org.xml.sax.Locator locator;
public int inputFileSchemaType;
String currentFirstName=null;
String currentLastName=null;
puiblic void setDocumentLocator(org.xml.sax.Locator l) { this.locator=l; }
public void startElement(String uri, String localName, String qName,
org.xml.sax.Attributes atts) throws SAXException {
qualifiedNames[pathPos] = qName;
localNames[pathPos] = localName;
namespaceURIs[pathPos] = uri;
int meaning;
meaning=0; pm=pathPos==0?0:meanings[pathPos-1];
switch (inputFileSchemaType) {
case FILETYPE_A:
switch(pathPos) {
case 0:
if(localName.equals("document")&&uri.equals("http://xyz")) meaning=1;
break; case 1: if (pm==1&&localName.equals("clients")) meaning=2;
break; case 2: if (pm==2&&localName.equals("firstName")) meaning=3;
else if (pm==2&&localName.equals("lastName")) meaning=4;
else if (pm==2) meaning=5;
}
break; case FILETYPE_B:
switch(pathPos) {
case 0:
if(localName.equals("DOC")&&uri.equals("http://abc")) meaning=1;
break; case 1: if (pm==1&&localName.equals("CLS")) meaning=2;
break; case 2: if (pm==2&&localName.equals("FN1")) meaning=3;
else if (pm==2&&localName.equals("LN1")) meaning=4;
else if (pm==2) meaning=5;
}
}
meanings[pathPos]=meaning;
switch (meaning) {
case 0:errorList.add(new Object[]{locator.getPublicId(),
locator.getSystemId(),
locator.getLineNumber(),locator.getColumnNumber(),
"Meaningless tag found: "+localName+" ("+qName+
"; namespace: \""+uri+"\")});
break;case 1:
destination.write("<?xml version=\"1.0\" ?>\n");
destination.write("<imdoc xmlns=\"http://someurl\" lang=\"xyz\">\n");
destination.write("<!-- Copyright notice -->\n");
destination.write("<!-- Generated by xyz -->\n");
break;case 2: destination.write(" <cl>\n");
currentFirstName="";currentLastName="";
}
pathPos++;
}
public void characters(char[] ch, int start, int length)
throws SAXException {
int meaning=meanings[pathPos-1]; switch (meaning) {
case 1: case 2:
errorList.add(new Object[]{locator.getPublicId(),
locator.getSystemId(),
locator.getLineNumber(),locator.getColumnNumber(),
"Unexpected extra characters found"});
break; case 3:
break; case 4:
break; default:
}
}
public void endElement(String uri, String localName, String qName)
throws SAXException {
pathPos--;
int meaning=meanings[pathPos]; switch (meaning) { case 1:
destination.write("</imdoc>");
break; case 2:
destination.write(" <ln>"+currentLastName.trim()+"</ln>\n");
destination.write(" <fn>"+currentFirstName.trim()+"</fn>\n");
destination.write(" </cl>\n");
break; case 3:
if (currentFirstName==null||currentFirstName.equals(""))
errorList.add(new Object[]{locator.getPublicId(),
locator.getSystemId(),
locator.getLineNumber(),locator.getColumnNumber(),
"Invalid first name length"});
break; case 4:
if (currentLastName==null||currentLastName.equals(""))
errorList.add(new Object[]{locator.getPublicId(),
locator.getSystemId(),
locator.getLineNumber(),locator.getColumnNumber(),
"Invalid last name length"});
}
}
public void endDocument() {
}
}
第一阶段的代码并不是用于重新排序数据,而仅是将其标准化为单个中间格式(尽管数据组的顺序可能会因源文件类型而异,因为数据组的顺序将与源文件的顺序相同),并进行验证。
但是,如果您已经满意于您的XSLT,则编写SAX处理程序是没有必要的。假设您对此提出问题,那么您可能还不满意于您的XSLT...?
另一方面,如果您喜欢您的XSLT,并且它运行得足够快,我认为为什么要更改架构呢?在这种情况下,如果您还没有将相关的Xalan调用包装在Ruby模块中,您可能会发现
this 文章有所帮助。您可能想尝试使其成为用户的一步过程(对于未发现数据错误的情况)。
编辑
使用这种方法,您必须手动在输出时转义XML,因此:
& 变成 &
> 变成 >
< 变成 <
非 ASCII 字符如果需要,变成字符实体,否则是 UTF-8 序列
等等
同时应编写一个函数,该函数可以接受一个 SAX 属性对象和与输入标签的含义和文件格式相关的灵活验证规范作为对象数组或类似物,并根据需要严格或宽松地匹配和返回值,并标记错误。
最后,您应该有一个可配置的 MAX_ERRORS 概念,默认值为1000,在达到此限制时记录“太多错误”错误,并在达到限制后停止记录错误。
如果您需要提高可以并行处理的 XML 数量,并且仍然在容量/性能方面遇到困难,则建议 DOM 步骤仅加载、重新排序和保存,因此一次可以处理一两个文档,但相对较快,因此可以分批进行处理,然后第二个 SAX 处理器再对 N 个文档并行进行 Google 调用和 XML 处理。
希望这些对您有所帮助。
编辑:
> 我们有大约50种不同的输入格式,因此进行
> switch/case FORMAT_X 不是好的选择。
这是传统的智慧,但以下情况如何:
if (fileFormat>=GROUP10) switch (fileFormat) {
case GROUP10_FORMAT1:
switch(pathPos) {
case 0: if (...) { meaning=GROUP10_CUSTOMER; avr=AVR6_A; }
break; case 1: if (...) { meaning=...; avr=...; }
...
}
break; case GROUP10_FORMAT2: ...
break; case GROUP10_FORMAT3: ...
}
else if (fileFormat>=GROUP9) switch (fileFormat) {
case GROUP9_FORMAT1: ...
break; case GROUP9_FORMAT2: ...
}
...
else if (fileFormat>=GROUP1) switch (fileFormat) {
case GROUP1_FORMAT1: ...
break; case GROUP1_FORMAT2: ...
}
...
result = validateAttribute(atts,avr);
if (meaning >= MEANING_SET10) switch (meaning) {
case ...: ...
break; case ...: ...
}
else if (meaning >= MEANING_SET9) switch (meaning) {
}
etc
“可能足够快,比许多函数或类更易于阅读。”
“我不满意的部分是我不能使用某种同质过程进行结构和值的转换(就像Java我可以为Xalan编写扩展程序)。”
听起来你已经达到了XSLT的限制,或者你只是在谈论明显的限制,即从源文件以外的数据源中获取数据很麻烦?
另一个想法是拥有一个验证样式表,一个输出用于在Google Maps上尝试的关键字列表的样式表,一个输出用于在您的数据库上尝试的关键字列表的样式表,实际执行Google/db调用并输出更多XML的过程,“XML连接”功能,以及一个将数据组合起来的样式表,输入如下:
<?xml version="1.0" ?>
<myConsolidatedInputXmlDoc>
<myOriginalOrIntermediateFormatDoc>
...
</myOriginalOrIntermediateFormatDoc>
<myFetchedRelatedDataFromGoogleMaps>
...
</myFetchedRelatedDataFromGoogleMaps>
<myFetchedDataFromSQL>
...
</myFetchedDataFromSQL>
</myConsolidatedInputXmlDoc>
以此方式,您可以在不调用Xalan扩展的情况下在“多通道”场景中使用XSLT。