如 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);
}
这里的内存使用将再次受到限制,因为在任何时候都只会引用一个对应于单个<Widget> 的XElement。
演示小提琴here.
更新
如果 <Widget> 标记的集合,而不是直接从 XML 根中提取,实际上它们本身包含在根的单个 <Widgets> 子树中,您的代码将如何变化?
这里有几个选项。首先,您可以通过将使用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 上读取 <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.