【问题标题】:C# .NET - Is there a simple way of querying the same XML node over a collection of XML files in a single ZIP file?C# .NET - 是否有一种简单的方法可以通过单个 ZIP 文件中的 XML 文件集合查询相同的 XML 节点?
【发布时间】:2019-11-04 11:13:25
【问题描述】:

我正在尝试将一段 Python 代码转换为 C#,该代码采用一个包含 XML 文件的 ZIP 文件,然后为每个 XML 文件执行特定的 XPath 查询并返回结果。在 Python 中,它非常轻量级,看起来像这样(我意识到下面的示例并不是严格意义上的 XPath,但我是前段时间写的!):

with zipfile.ZipFile(fullFileName) as zf:
zfxml = [f for f in zf.namelist() if f.endswith('.xml')]
for zfxmli in zfxml:
    with zf.open(zfxmli) as zff:
        zfft = et.parse(zff).getroot()
        zffts = zfft.findall('Widget')
        print ([wgt.find('Description').text for wgt in zffts])

我在 C# 中最接近的是:

foreach (ZipArchiveEntry entry in archive.Entries)
{
    FileInfo fi = new FileInfo(entry.FullName);

    if (fi.Extension.Equals(".xml", StringComparison.OrdinalIgnoreCase))
    {
        using (Stream zipEntryStream = entry.Open())
        {
            XmlDocument xmlDoc = new XmlDocument();

            xmlDoc.Load(zipEntryStream);
            XmlNodeList wgtNodes = xmlDoc.SelectNodes("//Root/Widget");

            foreach (XmlNode tmp in wgtNodes)
            {
                zipListBox.Items.Add(tmp.SelectSingleNode("//Description"));
            }
        }
    }
}

虽然这确实适用于较小的 ZIP 文件,但它比 Python 实现占用更多的内存方式,并且如果 ZIP 文件中包含太多 XML 文件,则会崩溃内存。是否有另一种更有效的方法来实现这一目标?

【问题讨论】:

标签: c# python xml xpath zip


【解决方案1】:

What is the best way to parse (big) XML in C# Code? 中所述,您可以使用 XmlReader 流式传输具有有限内存消耗的大型 XML 文件。但是,XmlReader 使用起来有些棘手,因为如果 XML 与预期的完全不符,则很容易阅读太少或太多。 (即使是微不足道的空白也可能导致 XmlReader 算法失效。)

为了减少出现此类错误的机会,首先引入以下扩展方法,该方法遍历当前元素的所有直接子元素:

public static partial class XmlReaderExtensions
{
    /// <summary>
    /// Read all immediate child elements of the current element, and yield return a reader for those matching the incoming name & namespace.
    /// Leave the reader positioned after the end of the current element
    /// </summary>
    public static IEnumerable<XmlReader> ReadElements(this XmlReader inReader, string localName, string namespaceURI)
    {
        inReader.MoveToContent();
        if (inReader.NodeType != XmlNodeType.Element)
            throw new InvalidOperationException("The reader is not positioned on an element.");
        var isEmpty = inReader.IsEmptyElement;
        inReader.Read();
        if (isEmpty)
            yield break;
        while (!inReader.EOF)
        {
            switch (inReader.NodeType)
            {
                case XmlNodeType.EndElement:
                    // Move the reader AFTER the end of the element
                    inReader.Read();
                    yield break;
                case XmlNodeType.Element:
                    {
                        if (inReader.LocalName == localName && inReader.NamespaceURI == namespaceURI)
                        {
                            using (var subReader = inReader.ReadSubtree())
                            {
                                subReader.MoveToContent();
                                yield return subReader;
                            }
                            // ReadSubtree() leaves the reader positioned ON the end of the element, so read that also.
                            inReader.Read();
                        }
                        else
                        {
                            // Skip() leaves the reader positioned AFTER the end of the element.
                            inReader.Skip();
                        }
                    }
                    break;
                default:
                    // Not an element: Text value, whitespace, comment.  Read it and move on.
                    inReader.Read();
                    break;
            }
        }
    }

    /// <summary>
    /// Read all immediate descendant elements of the current element, and yield return a reader for those matching the incoming name & namespace.
    /// Leave the reader positioned after the end of the current element
    /// </summary>
    public static IEnumerable<XmlReader> ReadDescendants(this XmlReader inReader, string localName, string namespaceURI)
    {
        inReader.MoveToContent();
        if (inReader.NodeType != XmlNodeType.Element)
            throw new InvalidOperationException("The reader is not positioned on an element.");
        using (var reader = inReader.ReadSubtree())
        {
            while (reader.ReadToFollowing(localName, namespaceURI))
            {
                using (var subReader = inReader.ReadSubtree())
                {
                    subReader.MoveToContent();
                    yield return subReader;
                }
            }
        }
        // Move the reader AFTER the end of the element
        inReader.Read();
    }
}

这样,你的python算法可以重现如下:

var zipListBox = new List<string>();

using (var archive = ZipFile.Open(fullFileName, ZipArchiveMode.Read))
{
    foreach (var entry in archive.Entries)
    {
        if (Path.GetExtension(entry.Name).Equals(".xml", StringComparison.OrdinalIgnoreCase))
        {
            using (var zipEntryStream = entry.Open())
            using (var reader = XmlReader.Create(zipEntryStream))
            {
                // Move to the root element
                reader.MoveToContent();

                var query = reader
                    // Read all child elements <Widget>
                    .ReadElements("Widget", "")
                    // And extract the text content of their first child element <Description>
                    .SelectMany(r => r.ReadElements("Description", "").Select(i => i.ReadElementContentAsString()).Take(1));

                zipListBox.AddRange(query);
            }
        }
    }
}

注意事项:

  • 您的 c# XPath 查询与您原来的 python 查询不匹配。您的原始 python 代码执行以下操作:

    zfft = et.parse(zff).getroot()
    

    这无条件获取根元素(docs)。

    zffts = zfft.findall('Widget')
    

    这会查找所有名为“Widget”的直接子元素(未使用递归下降运算符//)(docs)。

    wgt.find('Description').text for wgt in zffts
    

    这会遍历小部件,并为每个小部件找到名为“Description”的第一个子元素并获取其文本 (docs)。

    为了比较,xmlDoc.SelectNodes("//Root/Widget") 递归地遍历整个 XML 元素层次结构以查找名为 &lt;Widget&gt; 的节点,这些节点嵌套在名为 &lt;Root&gt; 的节点中——这可能不是您想要的。类似地,tmp.SelectSingleNode("//Description") 递归地下降&lt;Widget&gt; 下的 XML 层次结构以找到描述节点。递归下降在这里可能有效,但如果有多个嵌套的 &lt;Description&gt; 节点,可能会返回不同的结果。

  • 使用XmlReader.ReadSubtree() 可确保消耗整个元素——不多也不少。

  • ReadElements()LINQ to XML 配合得很好。例如。如果您想通过 XML 流式传输并获取每个小部件的 id、描述和名称而不将它们全部加载到内存中,您可以这样做:

    var query = reader
        .ReadElements("Widget", "")
        .Select(r => XElement.Load(r))
        .Select(e => new { Description = e.Element("Description")?.Value, Id = e.Attribute("id")?.Value, Name = e.Element("Name")?.Value });
    
    foreach (var widget in query)
    {
        Console.WriteLine("Id = {0}, Name = {1}, Description = {2}", widget.Id, widget.Name, widget.Description);
    }
    

    这里的内存使用将再次受到限制,因为在任何时候都只会引用一个对应于单个&lt;Widget&gt;XElement

演示小提琴here.

更新

如果 &lt;Widget&gt; 标记的集合,而不是直接从 XML 根中提取,实际上它们本身包含在根的单个 &lt;Widgets&gt; 子树中,您的代码将如何变化?

这里有几个选项。首先,您可以通过将使用SelectMany 扁平化元素层次结构的LINQ 语句链接在一起来对ReadElements 进行嵌套调用:

var query = reader
    // Read all child elements <Widgets>
    .ReadElements("Widgets", "")
    // Read all child elements <Widget>
    .SelectMany(r => r.ReadElements("Widget", ""))
    // And extract the text content of their first child element <Description>
    .SelectMany(r => r.ReadElements("Description", "").Select(i => i.ReadElementContentAsString()).Take(1));

如果您只想在某些特定 XPath 上读取 &lt;Widget&gt; 节点,请使用此选项。

或者,您可以简单地读取所有名为 &lt;Widget&gt; 的后代,如下所示:

var query = reader
    // Read all descendant elements <Widget>
    .ReadDescendants("Widget", "")
    // And extract the text content of their first child element <Description>
    .SelectMany(r => r.ReadElements("Description", "").Select(i => i.ReadElementContentAsString()).Take(1));

如果有兴趣读取 XML 中出现的 &lt;Widget&gt; 节点,请使用此选项。

演示小提琴 #2 here.

【讨论】:

  • 感谢您在回复中付出如此努力和细节!那里有一些我不知道的方面,例如 SelectNodes 的实现。我有很多东西要尝试,所以我会去给它一个狂欢,让你知道我的进展情况。
  • 我有一个后续问题:如果 标记的集合,而不是直接从 XML 根目录,实际上它们本身包含在单个 中,您的代码将如何改变根的子树?
  • @AdamGripton - 请编辑您的原始问题以共享 XML 示例 - 即 minimal reproducible example。您问题中显示的 python 是否适用于该 XML?
  • 另外,在 XML 中的任何位置读取任何 &lt;Widget&gt; 标记是否就足够了,或者您是否需要将其限制为仅在某些指定的 XPath 上读取 &lt;Widget&gt; 标记?
  • @AdamGripton - 答案已更新。但在未来,请不要认为堆栈溢出问题的首选格式是one question per post,因此如果您有一些后续问题,最好接受当前答案,并提出另一个问题。
猜你喜欢
  • 2010-12-03
  • 1970-01-01
  • 1970-01-01
  • 2020-05-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-16
相关资源
最近更新 更多