C# .NET - 是否有一种简单的方法可以通过单个 ZIP 文件中的 XML 文件集合查询相同的 XML 节点?

标签 c# python xml xpath zip

我正在尝试将一段 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 文件,则会出现内存不足的情况。是否有另一种更有效的方法来实现这一目标?

最佳答案

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 元素层次结构向下查找名为 <Widget> 的节点嵌套在名为 <Root> 的节点内——这可能不是你想要的。同样tmp.SelectSingleNode("//Description")<Widget> 下递归地降低 XML 层次结构查找描述节点。递归下降在这里可能有效,但如果有多个嵌套,则可能返回不同的结果 <Description>节点。

  • 使用 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);
    }
    

    这里内存使用将再次受到限制,因为只有一个 XElement对应单个<Widget>随时会被引用。

演示 fiddle here .

更新

如果 <Widget> 的集合,您的代码将如何更改标签并非直接来自 XML 根,实际上它们本身包含在单个 <Widgets> 中。根的子树?

这里有几个选择。首先,您可以嵌套调用 ReadElements通过将 LINQ 语句链接在一起,使用 SelectMany 展平元素层次结构:

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));

如果您只想阅读<Widget>,请使用此选项仅在某些特定 XPath 上的节点。

或者,您可以简单地读取名为 <Widget> 的所有后代。如下所示:

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));

如果有兴趣阅读 <Widget>,请使用此选项节点,无论它们出现在 XML 中的何处。

演示 fiddle #2 here .

关于C# .NET - 是否有一种简单的方法可以通过单个 ZIP 文件中的 XML 文件集合查询相同的 XML 节点?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58692275/

相关文章:

xml - 有人知道 Wiktionary XML 文件结构吗?

c# - C# 的 Matlab exprnd(a,b,c) 模拟?

c# - LINQ + 加入嵌套的 foreach Razor 输出,从 groupby 写出标题行

python - Python 中的多个字符串插值

python Holoviews 带有列的布局选项卡

python - numpy.as_strided 的结果是否取决于输入数据类型?

c++ - 将具有多个值的 XML 属性加载到不同的表格单元格

c# - 启用缓存的 HttpWebRequest 抛出异常

c# - GUI 线程中的异常导致 vshost.exe 崩溃

C# 数据集到 Xml,节点重命名