Как объяснено в Каков наилучший способ анализа (большого) XML в коде C #? , вы можете использовать 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")
рекурсивно спускается по иерархии XML в <Widget>
, чтобы найти узел описания. Рекурсивный спуск может работать здесь, но может возвратить другой результат, если есть несколько вложенных <Description>
узлов.
Использование XmlReader.ReadSubtree()
гарантирует, что весь элементпотребляется - не больше и не меньше.
ReadElements()
хорошо работает с LINQ to XML . Например, если вы хотите выполнить потоковую передачу через ваш XML и получить идентификатор, описание и имя каждого виджета, не загружая их все в память, вы можете сделать следующее:
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>
, будет ссылаться в любое время.
Демонстрационная скрипка здесь .
Обновление
Как изменился бы ваш код, если бы коллекция тегов <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.
Демонстрационная скрипка # 2 здесь .