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