65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 XStreamingElement 合并 XML 文件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (10投票s)

2010年4月16日

CPOL

4分钟阅读

viewsIcon

43623

downloadIcon

1099

需要合并几个 GB 的 XML 数据?这里有一种方法,不会耗尽你的内存。

引言

让我们面对现实。有时,你需要处理的 XML 数据量会变得有点失控。我每天需要合并和处理大约 3 GB 的文件。但仅仅为了合并就将所有这些数据加载到内存中似乎有点过分。流式处理肯定是个办法,肯定有人做过这件事吧?嗯,至少我找不到,所以这是我用 XStreamingElement 来合并多个文件的方法。

在我进行处理时,我假设我正在处理的 XML 文件具有以下特点:

  • 这些文件包含各种元素,名义上构成了头部、正文和尾部(或页脚)。
  • 对于上述“结构”,每个文件只合并其中一部分,即“正文”。
  • “正文”包含在每个文件中的单个元素内。
  • 我不需要(也不想)知道“正文”中包含哪些元素。事实上,我希望能够以最少的知识来合并文件。
  • 我不会对元素进行任何重新排序。每个文件按顺序处理,每个文件的内容也按顺序处理。

现在,以上所有内容可能对你来说有所不同,但希望你能在此基础上修改代码以适应你的目的。

Using the Code

该项目附带了一些示例 XML 文件可供合并。下载项目后,请在项目文件和程序本身中调整路径,使其与你存放的位置匹配。然后你应该能够编译并运行,看到三个测试文件合并成一个。

当我第一次尝试这样做时,有一个特别的点我还没有完全理解,为了节省大家的时间,我在这里说明一下:

通常,XStreamingElement 是通过使用扩展方法“构建”的,该方法逐个节点地使用 Reader 流式传输源 XML。但这过程只适用于过滤节点或将现有节点操作成新形式。如果你想实际将节点插入到“现有”节点中(这正是我们要为合并做的),那么你不能这样做,因为(yield)返回的节点本身尚未构建完成(你实际上只有一个指向它的指针)。要做到这一点,你必须构建该“现有”节点的新版本,并将所有原始内容加上新内容都放入其中。

以下是我的做法:

// Get 'collections' of the attributes and elements we want to combine together
IEnumerable<XAttribute> rootAttr = FileMergeAttributeStreamAxis(localFiles.First(), rootName);
IEnumerable<XElement> headerElem = 
            FileHeaderStreamAxis(localFiles.First(), rootName, mergeName);
IEnumerable<XAttribute> mergeAttr = 
            FileMergeAttributeStreamAxis(localFiles.First(), mergeName);
IEnumerable<XElement> mergeElem = FileMergeElementStreamAxis(localFiles, mergeName);
IEnumerable<XElement> trailerElem = FileTrailerStreamAxis(localFiles.Last(), mergeName);

// Now piece them all together in our new XStreamingElement
// - note the internal XStreamingElement
XStreamingElement mergeElement = new XStreamingElement(rootName, rootAttr, headerElem,
     new XStreamingElement(mergeName, mergeAttr, mergeElem),
     trailerElem);

// Write it all to disk
mergeElement.Save(folder + mergeFileName);

这会根据源文件的碎片构建新的“合并”文件,如下所示:

  • 根元素(新建)
    • 根元素的属性(从第一个文件中获取)
    • “头部”元素(从第一个文件中获取),我们在要合并的元素之前找到的任何内容
    • 合并元素(新建)
      • 合并元素的属性(从第一个文件中获取)
      • 合并元素的内容(从所有文件中获取)
    • “尾部”元素(从最后一个文件中获取),我们在要合并的元素之后找到的任何内容

你还会注意到使用了多个扩展方法,每个方法都有不同的职责。起初,我尝试让一个扩展方法完成所有工作,但它就是不行(因为你不能插入到一个已存在的元素中)。然而,通过多次调用多个扩展方法就可以正常工作。

如果你的文档合并需求不同,那么你应该从上述陈述开始。例如,如果你希望“头部”和“尾部”来自第一个文件,并且你需要合并的每个文件的“正文”不包含在单个元素中,而是介于“头部”的某个元素和“尾部”的某个元素之间的所有元素,那么你可能需要类似这样的东西:

IEnumerable<XAttribute> rootAttr = FileMergeAttributeStreamAxis(localFiles.First(), rootName);
IEnumerable<XElement> headerElem = 
            FileHeaderStreamAxis(localFiles.First(), rootName, mergeName);
IEnumerable<XElement> mergeElem = FileRangeElementStreamAxis(localFiles, mergeName);
IEnumerable<XElement> trailerElem = FileTrailerStreamAxis(localFiles.Last(), mergeName);

XStreamingElement mergeElement = new XStreamingElement(rootName, rootAttr, 
                                     headerElem, mergeElem, trailerElem);

而且,你还需要创建 FileRangeStreamAxis 来汇总最后一个头部元素之后和第一个尾部元素之前的所有元素。

这是执行实际“合并”的扩展方法,以及一个使获取要合并的元素内容更容易的额外方法。

/// <summary>
/// Read through each of the files until the node with
/// 'mergeElementName' is found, then read all of its element nodes
/// </summary>
/// <param name="mergeFiles">Names of files to read through</param>
/// <param name="mergeElementName">Name of node from which to obtain the elements</param>
/// <returns>All element nodes found in the node named
///             'mergeElementName' within each file</returns>
private static IEnumerable<XElement> 
        FileMergeElementStreamAxis(IEnumerable<string> mergeFiles, string mergeElementName)
{
  foreach (string mergeFile in mergeFiles)
  {
    using (XmlReader reader = XmlReader.Create(mergeFile))
    {
      XmlReader subReader = FileMergeElementReader(reader, mergeElementName);

      if (subReader != null)
      {
        do
        {
          // Test if this is an element and it is not the merge element
          // - if not the merge element then by definition we will be 'in' the merge element
          if (subReader.NodeType == XmlNodeType.Element && subReader.Name != mergeElementName)
          {
            XElement el = XElement.ReadFrom(subReader) as XElement;
            if (el != null)
              yield return el;
          }
          else
            subReader.Read();
        } while (!subReader.EOF);

        subReader.Close();
      }
      reader.Close();
    }
  }
}

/// <summary>
/// This method returns a subtree reader positioned
/// on the contents of the mergeElementName element.
/// It is the responsibility of the calling method to close both the reader passed in
/// and the reader that is returned by this method.
/// </summary>
/// <param name="reader">The reader we will use to try
///            and find the 'mergeElementName' node</param>
/// <param name="mergeElementName">Name of node
///            for which we want to read its subtree</param>
/// <returns>A reader positioned on the subtree identified
///            by 'mergeElementName' or null</returns>
private static XmlReader FileMergeElementReader(XmlReader reader, 
                         string mergeElementName)
{
  XmlReader subReader = null;

  if (reader == null || mergeElementName == "")
    return subReader;

  do
  {
    if (reader.NodeType == XmlNodeType.Element && reader.Name == mergeElementName)
    {
      subReader = reader.ReadSubtree();
      break;
    }
    else
      reader.Read();
  } while (!reader.EOF);

  return subReader;
}

这个特定的扩展方法(及其辅助方法)实际上就是三个循环:一个 foreach 循环按顺序处理每个文件,一个外部 while 循环(在辅助方法中)用于定位我们要合并的元素,最后是内部 while 循环来实际执行合并。

这里的技巧是使用 XmlReaderReadSubTree 方法。这会将我们要合并的整个元素作为一个独立于主 XmlReader 的对象获取。完成后,它会将主 XmlReader 精确地定位在我们正在合并的元素的末尾,在我们的例子中,这意味着工作完成了。

历史

  • 2010 年 4 月 16 日 - 初版
  • 2010 年 5 月 4 日 - *新*和*改进*的代码 - 运行效果更好,奇怪的 bug 更少
© . All rights reserved.