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

我正在尝试将一段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>

演示小提琴here。

更新资料

如果<Widget>标记的集合实际上不包含在XML根目录下,而是实际上包含在根目录的单个<Widgets>子树中,那么您的代码将如何更改?

您在这里有几个选择。首先,您可以通过将LINQ语句链接在一起来嵌套调用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));

如果您仅对仅在某些特定XPath上读取<Widget>节点感兴趣,请使用此选项。

另外,您可以简单地阅读所有名为<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));

如果有兴趣读取XML中出现的<Widget>节点,请使用此选项。

演示小提琴#2 here。

ASP.net C#崩溃一行 - c#

我有一个母版页,在on load事件中包含以下几行: string menuIDdata = Page.Request.QueryString["mid"]; menuID = 0; // Get the menu ID if (!int.TryParse(menuIDdata, out menuID)) { menuID = 0; } …

我可以在ID为键而不是ASP.NET MVC(C#)中的数组的情况下输出JSON吗 - javascript

因此,在ASP.NET MVC中,我有一个Controller动作,如下所示:public JsonResult People() { var people = db.People.ToList(); return Json(people); } 并在退出时将返回如下内容:[ { "ID": 1, "Name": &#…

.NET C#Webbrowser填充输入,不带ID或类名 - javascript

我需要在网络浏览器中填写一个输入,但这不起作用。我认为必须使用name属性来完成,但是怎么做呢?foreach (HtmlElement login in webBrowser1.Document.GetElementsByTagName("input")) { if (login.GetAttribute("name"…

Div单击与单选按钮相同吗? - php

有没有一种方法可以使div上的click事件与表单环境中的单选按钮相同?我只希望下面的div提交值,单选按钮很丑代码输出如下:<input id="radio-2011-06-08" value="2011-06-08" type="radio" name="radio_date&#…

ASP.NET-如何更改JSON序列化的方式? - javascript

我正在使用ASP.NET通过以下查询返回Json文件:public ActionResult getTransactionTotals(int itemID) { DBEntities db = new DBEntities(); var query = from trans in db.Transactions // Linq query removed …