使用XSLT转换结构 + 使用Ruby转换值?

3
我们有来自不同来源的相当大(~200mb)的xml文件,希望将它们转换为通用格式。对于结构转换(元素名称、嵌套等),我们决定使用XSLT(1.0)。因为它必须快速(我们接收了很多这样的文件),所以我们选择了Apache Xalan作为引擎。结构转换可能相当复杂(不仅仅是 -> ),并且针对来自不同源的xml文件也不同。
但是,我们还需要转换元素的值。变换可能相当复杂(例如,一些需要访问Google Maps API,另一些需要访问我们的数据库等...),因此我们决定使用一个简单的基于Ruby的DSL,它是“xpath选择器”=>转换器实体列表,例如:
{"rss/channel/item" => {:class => 'ItemMutators', :method => :guess_location}}
然而,将元素变换与值变换分开似乎更像是一个hack。是否有更好的解决方案?
例如,使用Java您可以编写xalan扩展,并使用它们来转换值。是否有类似于Ruby的东西?
谢谢,大家!所有回复都非常有价值。我正在思考 :)

顺便说一句:我不建议在处理大文件时使用XSLT或Xalan,因为文件通常会在RAM中膨胀到原始大小的3至5倍,而Xalan并不是那么快。我建议使用stx/joost而不是纯SAX库...如果你坚持使用Xalan,那么请至少优先考虑使用Jaxen进行XPath查询 :-) - Karussell
3个回答

2
我会用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 { 

    // record current tag in stack
    qualifiedNames[pathPos] = qName;
    localNames[pathPos] = localName;
    namespaceURIs[pathPos] = uri;
    int meaning;

    // what is the meaning of the current tag?
    meaning=0; pm=pathPos==0?0:meanings[pathPos-1];
    switch (inputFileSchemaType) {
           case FILETYPE_A:
      switch(pathPos) {
        // this checking can be as strict or as lenient as you like on case,
        // namespace URIs and tag prefixes
             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) {
        // this checking can be as strict or as lenient as you like on case,
        // namespace URIs and tag prefixes
             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;

    // does the tag have unrecognised attributes?
    // does the tag have all required attributes?
    // record any keys in hashtables...
    // (TO BE DONE)

    // generate output
    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:
      // APPEND to currentFirstName IF WITHIN SIZE LIMITS
    break; case 4:
      // APPEND to currentLastName IF WITHIN SIZE LIMITS
    break; default: // ignore other characters
    }
  }
  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"});
      // ADD FIELD FORMAT VALIDATION USING REGEXES / RANGE CHECKING
    break; case 4:
      if (currentLastName==null||currentLastName.equals(""))
              errorList.add(new Object[]{locator.getPublicId(),
        locator.getSystemId(),
        locator.getLineNumber(),locator.getColumnNumber(),
        "Invalid last name length"});
      // ADD FIELD FORMAT VALIDATION USING REGEXES / RANGE CHECKING
    }
  }
  public void endDocument() {
    // check for key violations
  }
}

第一阶段的代码并不是用于重新排序数据,而仅是将其标准化为单个中间格式(尽管数据组的顺序可能会因源文件类型而异,因为数据组的顺序将与源文件的顺序相同),并进行验证。
但是,如果您已经满意于您的XSLT,则编写SAX处理程序是没有必要的。假设您对此提出问题,那么您可能还不满意于您的XSLT...?
另一方面,如果您喜欢您的XSLT,并且它运行得足够快,我认为为什么要更改架构呢?在这种情况下,如果您还没有将相关的Xalan调用包装在Ruby模块中,您可能会发现 this 文章有所帮助。您可能想尝试使其成为用户的一步过程(对于未发现数据错误的情况)。
编辑
使用这种方法,您必须手动在输出时转义XML,因此:

& 变成 &amp;

> 变成 &gt;

< 变成 &lt;

非 ASCII 字符如果需要,变成字符实体,否则是 UTF-8 序列

等等

同时应编写一个函数,该函数可以接受一个 SAX 属性对象和与输入标签的含义和文件格式相关的灵活验证规范作为对象数组或类似物,并根据需要严格或宽松地匹配和返回值,并标记错误。

最后,您应该有一个可配置的 MAX_ERRORS 概念,默认值为1000,在达到此限制时记录“太多错误”错误,并在达到限制后停止记录错误。

如果您需要提高可以并行处理的 XML 数量,并且仍然在容量/性能方面遇到困难,则建议 DOM 步骤仅加载、重新排序和保存,因此一次可以处理一两个文档,但相对较快,因此可以分批进行处理,然后第二个 SAX 处理器再对 N 个文档并行进行 Google 调用和 XML 处理。

希望这些对您有所帮助。

编辑:

> 我们有大约50种不同的输入格式,因此进行

> switch/case FORMAT_X 不是好的选择。

这是传统的智慧,但以下情况如何:
// set meaning and attributesValidationRule (avr)
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。

我非常支持领域特定语言(DSLs)的使用。我喜欢这种思维方式。但基本上,这是 SAX/DOM 的工作。只有在你需要大量处理这种事情时,才值得考虑使用 DSLs。 - martinr
我们如何使用SAX进行结构转换?编写用于结构转换的DSL似乎相当复杂。我们有许多不同的格式,有时需要进行一些复杂的结构更改。 XSLT非常适合描述它们,但不幸的是,它无法转换值。现在我们的工作流程包括4个rake任务:
  1. 下载xml文件
  2. 使用XSLT转换下载的xml文件
  3. 使用Ruby转换第2步的结果(仅元素值)。(Nokogiri / SAX,像帖子中的那种DSL)。
  4. 读取第3步的结果,验证每个项目,将其保存到数据库中。
- glebm
+1 给 Xalan-Ruby 的链接。现在我们只是用 %x[...] 调用 Xalan CLI。DOM 的问题在于它根本不可行。200MB 的文件对于 DOM 处理来说太多了(变得极慢)。我们有大约 50 种不同的输入格式,因此使用 switch/case FORMAT_X 不好。为此编写 DSL 也不可行,因为转换可能相当复杂,但 XSLT 可以处理。让我不满意的部分是,我不能使用某种均匀的过程进行结构和值的转换(就像 Java 中我可以为 Xalan 编写扩展)。 - glebm
可以将其放入数据库进行排序。如果需要避免使用DOM... 阶段1:SAX->(验证/键检查/部分标准化)->“INSERT INTO queries”。阶段2:“各种SELECT ORDER BY查询”->(添加GoogleMaps /主数据库数据/其余标准化)-> 最终SQL。可以使用本地数据库来保存中间数据和生产数据库的快照,或者如果您具有足够的容量和数据稳定性,则可以在主数据库上执行所有SQL。 - martinr
我有点同意(带有一些限制)这个人的观点:http://www.codinghorror.com/blog/2005/07/martin-fowler-hates-xslt-too.html。在我看来,XSLT 基本上是一个非常好的原型设计工具,但一旦概念验证完成,通常其他工具更好。我不想成为阻力,但我能说什么 - 这就是我的经验。 - martinr

2
您应该能够使用XSLT扩展。通过网络搜索,我们发现Xalan支持使用Java进行扩展:http://xml.apache.org/xalan-j/extensions.html 引用链接页面的语句如下:
对于那些想要通过调用过程语言来增强XSLT功能的情况,Xalan-Java支持创建和使用扩展元素和扩展函数。Xalan-Java还提供了一个适用于您的扩展库。
此外,显然有人在Ruby中编写了一个软件包,可以提供xslt扩展:http://greg.rubyfr.net/pub/packages/ruby-xslt/classes/XML/XSLT.html

如果你跟随那些链接,甚至可以使用JRuby。 - Kyle Butt
@Kyle:显然,甚至有一个为Ruby开发的软件包! - Aryabhatta
Java和JRuby不是一个选择,因为值转换器需要访问主应用程序中的模型(很多模型)。我看了一下Ruby包,它自2006年以来就没有更新过,可能会很慢(我们的源XML文件有时约为200 MB,而我们每天需要导入很多这样的文件)。 - glebm
@Glex:Ruby 实现似乎是共享源代码的,也许您可以修改它以适应您的需求。顺便问一下,真正的瓶颈在哪里?我会认为 Google API + DB 调用是瓶颈。在这种情况下,把值转换与元素转换分开可能会很有用:您可以批量处理那些 API/DB 调用,做一些缓存等等,也许? - Aryabhatta
我们会做所有这些(缓存、后台 Google API 调用等)。有两个性能问题:
  • 保存时的模型验证非常慢(比其他任何事情都要慢)
  • XML 文件的大小迫使我们使用 SAX,但这并不是一个问题。
现在我想了想,也许我们分开它们是正确的做法。
- glebm
如果您真的需要验证,而且这是瓶颈所在,也许您可以尝试优化验证步骤。例如,您是否仅需要结构进行验证?那么,您可以将其转换为裸骨XML进行验证。然后再次进行转换,关闭验证... - Aryabhatta

1

一种方法是使用带有一些扩展的Xalan-J,使其向您的Ruby进程发出RPC调用。返回的数据可以通过XSLT进一步处理。

为了更紧密地集成,您可以将Xalan-C ++绑定为Ruby库。您可能只需要Xalan API的一小部分,类似于命令行驱动程序XalanExe中使用的部分。使用Xalan在进程中运行,您的扩展可以直接访问您的Ruby模型。

链接:


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接