Синтаксический анализ XML: поиск элемента дерева без цикла - PullRequest
0 голосов
/ 22 февраля 2020

Я анализирую полезную нагрузку XML, используя ElementTree . Я не могу поделиться точным кодом или файлом, так как он делится конфиденциальной информацией. Я могу успешно извлечь нужную мне информацию, перебирая элемент (как видно из документации ElementTree) и добавляя вывод в списки. Например:

list_col_name = []
list_col_value = []

for col in root.iter('my_table'):
    # get col name
    col_name = col.find('col_name').text
    list_col_name.append(col_name
    # get col value
    col_value = col.find('col_value').text
    list_col_value.append(col_value)

Теперь я могу поместить их в словарь и перейти к остальной части того, что необходимо сделать:

dict_ = dict(zip(list_col_name, list_col_value))

Однако мне нужно, чтобы это произошло так быстро насколько это возможно, и мне интересно, если есть способ, которым я могу извлечь list_col_name сразу (то есть, используя findall() или что-то подобное). Просто любопытно, как увеличить скорость разбора xml, если это возможно. Все ответы / рекомендации приветствуются. Заранее спасибо.

Ответы [ 3 ]

2 голосов
/ 22 февраля 2020

Мое предложение состоит в том, чтобы использовать "инкрементальный" синтаксический анализ исходного файла, основанный на методе iterparse . Причина в том, что вам на самом деле:

  • не требуется никакого полного разобранного дерева XML,
  • во время инкрементального анализа вы можете отбросить обработанные элементы, поэтому потребность в памяти также меньше.

Еще один совет - использовать библиотеку l xml вместо ElementTree . Причина в том, что хотя iterparse метод существует в обеих этих библиотеках, но версия l xml имеет дополнительный параметр tag , поэтому Вы можете "ограничить" l oop только обработкой интересующих тегов.

В качестве исходного файла, который я использовал (что-то вроде):

<root>
  <my_table id="t1">
    <col_name>N1</col_name>
    <col_value>V1</col_value>
    <some_other_stuff>xx1</some_other_stuff>
  </my_table>
  <my_table id="t2">
    <col_name>N2</col_name>
    <col_value>V2</col_value>
    <some_other_stuff>xx1</some_other_stuff>
  </my_table>
  <my_table id="t3">
    <col_name>N3</col_name>
    <col_value>V3</col_value>
    <some_other_stuff>xx1</some_other_stuff>
  </my_table>
</root>

Собственно, мой источник файл:

  • включает 9 my_table элемент (не 3 ),
  • some_other_stuff повторяется 8 раз (в каждом my_table), для имитации других элементов, содержащихся в каждом my_table.

Я выполнил 3 теста, используя % timeit :

  1. Ваш l oop, с предварительным анализом исходного файла XML:

    from lxml import etree as et
    
    def fn1():
        root = et.parse('Tables.xml')
        list_col_name = []
        list_col_value = []
        for col in root.iter('my_table'):
            col_name = col.find('col_name').text
            list_col_name.append(col_name)
            col_value = col.find('col_value').text
            list_col_value.append(col_value)
        return dict(zip(list_col_name, list_col_value))
    

    Время выполнения составило 1,74 мс.

  2. Мой л oop, основываясь на iterparse , обрабатывая только "обязательные" элементы:

    def fn2():
        key = ''
        dict_ = {}
        context = et.iterparse('Tables.xml', tag=['my_table', 'col_name', 'col_value'])
        for action, elem in context:
            tag = elem.tag
            txt = elem.text
            if tag == 'col_name':
                key = txt
            elif tag == 'col_value':
                dict_[key] = txt
            elif tag == 'my_table':
                elem.clear()
                elem.getparent().remove(elem)
        return dict_
    

    Я предполагаю, что в каждом my_table элементе col_name происходит до col_value * 106 9 * и каждый my_table содержит только одного дочернего элемента с именами col_name и col_value .

    Обратите внимание, что вышеуказанная функция очищает каждую my_table Элемент и удаляет его из проанализированного дерева XML (функция getparent доступна только в версии l xml).

    Другое улучшение состоит в том, что Я "напрямую" добавляю каждую пару key / value в словарь, который должен быть возвращен этой функцией, поэтому zip не требуется.

    Время выполнения составляет 1,33 мс. Не намного быстрее, но, по крайней мере, некоторое увеличение времени видно.

  3. Вы также можете прочитать все элементы col_name и col_value , вызывая findall и затем вызов zip :

    def fn3():
        root = et.parse('Tables.xml')
        list_col_name = []
        for elem in root.findall('.//col_name'):
            list_col_name.append(elem.text)
        list_col_value = []
        for elem in root.findall('.//col_value'):
            list_col_value.append(elem.text)
        return dict(zip(list_col_name, list_col_value))
    

    Время выполнения составляет 1,38 мс. Также что-то более быстрое, чем ваше первоначальное решение, но без существенного отличия от моего первого решения ( fn2 ).

Конечно, конечный результат сильно зависит от:

  • размер вашего входного файла,
  • сколько "других вещей" содержит каждый элемент my_table .
1 голос
/ 26 февраля 2020

Я не знаю, есть ли что-нибудь, что вы хотите.

from simplified_scrapy import SimplifiedDoc
html = '''
<?xml version="1.0"?>
<data>
    <country name="Liechtenstein">
        <rank>1</rank>
        <year>2008</year>
        <gdppc>141100</gdppc>
        <neighbor name="Austria" direction="E"/>
        <neighbor name="Switzerland" direction="W"/>
    </country>
    <country name="Singapore">
        <rank>4</rank>
        <year>2011</year>
        <gdppc>59900</gdppc>
        <neighbor name="Malaysia" direction="N"/>
    </country>
    <country name="Panama">
        <rank>68</rank>
        <year>2011</year>
        <gdppc>13600</gdppc>
        <neighbor name="Costa Rica" direction="W"/>
        <neighbor name="Colombia" direction="E"/>
    </country>
</data>
'''
doc = SimplifiedDoc(html)
ranks = doc.selects('country>(rank>text())')
print (ranks)
ranks = doc.selects('country>rank()')
print (ranks)
ranks = doc.selects('country>children()')
print (ranks)

Результат:

['1', '4', '68']
[{'tag': 'rank', 'html': '1'}, {'tag': 'rank', 'html': '4'}, {'tag': 'rank', 'html': '68'}]
[[{'tag': 'rank', 'html': '1'}, {'tag': 'year', 'html': '2008'}, {'tag': 'gdppc', 'html': '141100'}, {'name': 'Austria', 'direction': 'E', 'tag': 'neighbor'}, {'name': 'Switzerland', 'direction': 'W', 'tag': 'neighbor'}], [{'tag': 'rank', 'html': '4'}, {'tag': 'year', 'html': '2011'}, {'tag': 'gdppc', 'html': '59900'}, {'name': 'Malaysia', 'direction': 'N', 'tag': 'neighbor'}], [{'tag': 'rank', 'html': '68'}, {'tag': 'year', 'html': '2011'}, {'tag': 'gdppc', 'html': '13600'}, {'name': 'Costa Rica', 'direction': 'W', 'tag': 'neighbor'}, {'name': 'Colombia', 'direction': 'E', 'tag': 'neighbor'}]]

Вот еще примеры: https://github.com/yiyedata/simplified-scrapy-demo/tree/master/doc_examples

1 голос
/ 22 февраля 2020

Рассмотрите понимание списка с помощью findall, чтобы избежать инициализации / добавления списка и явного for l oop, что может незначительно улучшить производительность :

# FINDALL LIST COMPREHENSION
list_col_name = [e.text for e in root.findall('./my_table/col_name')]
list_col_value = [e.text for e in root.findall('./my_table/col_value')]

dict(zip(list_col_name, list_col_value))

В качестве альтернативы, с lxml (сторонняя библиотека), которая полностью поддерживает XPath 1.0, рассмотрим xpath(), который может назначать вывод синтаксического анализа непосредственно спискам, также избегая инициализации / добавления и for l oop:

import lxml.etree as et
...

# XPATH LISTS
list_col_name = root.xpath('my_table/col_name/text()')
list_col_value = root.xpath('my_table/col_value/text()')

dict(zip(list_col_name, list_col_value))
...