C # .NET - Есть ли простой способ запроса одного и того же узла XML через коллекцию файлов XML в одном файле ZIP? - PullRequest
1 голос
/ 04 ноября 2019

Я пытаюсь перевести на C # фрагмент кода Python, который принимает ZIP-файл, полный XML-файлов, затем для каждого 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-файлов, оно занимает * 100 * * памяти больше, чем реализация Python, и вылетает из памяти, если в ZIP-файле слишком много XML-файлов. в этом. Есть ли другой, более эффективный способ достижения этого?

1 Ответ

1 голос
/ 05 ноября 2019

Как объяснено в Каков наилучший способ анализа (большого) 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 здесь .

...