Шаг 104.
Python: сборник рецептов.
Кодирование и обработка данных. Пошаговый парсинг очень больших XML-файлов

    На этом шаге мы рассмотрим решение этой задачи.

Задача

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

Решение

    Каждый раз, когда вы сталкиваетесь с пошаговой обработкой данных, вы должны вспоминать об итераторах и генераторах. Вот простая функция, которая может быть использована для пошаговой обработки огромных XML-файлов при очень небольшом потреблении памяти:

from xml.etree.ElementTree import iterparse


def parse_and_remove(filename, path):
    path_parts = path.split('/')
    doc = iterparse(filename, ('start', 'end'))
    # Пропуск корневого элемента
    next(doc)

    tag_stack = []
    elem_stack = []
    for event, elem in doc:
        if event == 'start':
            tag_stack.append(elem.tag)
            elem_stack.append(elem)
        elif event == 'end':
            if tag_stack == path_parts:
                yield elem
                elem_stack[-2].remove(elem)
        try:
            tag_stack.pop()
            elem_stack.pop()
        except IndexError:
            pass
Архив с файлом можно взять здесь.

    Чтобы протестировать функцию, вам потребуется большой XML-файл. Часто такие файлы можно найти на государственных сайтах и ресурсах с открытой информацией. Например, вы можете скачать базу данных выбоин на дорогах Чикаго в формате XML. Данный файл состоял из более чем 100 000 строк данных, которые были закодированы так:

<response>
<row>
  <row ...>
    <creation_date>2012-11-18T00:00:00</creation_date>
    <status>Completed</status>
    <completion_date>2012-11-18T00:00:00</completion_date>
    <service_request_number>12-01906549</service_request_number>
    <type_of_service_request>Pot Hole in Street</type_of_service_request>
    <current_activity>Final Outcome</current_activity>
    <most_recent_action>CDOT Street Cut ... Outcome</most_recent_action>
    <street_address>4714 S TALMAN AVE</street_address>
    <zip>60632</zip>
    <x_coordinate>1159494.68618856</x_coordinate>
    <y_coordinate>1873313.83503384</y_coordinate>
    <ward>14</ward>
    <police_district>9</police_district>
    <community_area>58</community_area>
    <latitude>41.808090232127896</latitude>
    <longitude>-87.69053684711305</longitude>
    <location latitude="41.808090232127896"
      longitude="-87.69053684711305" />
  </row>
  <row ...>
    <creation_date>2012-11-18T00:00:00</creation_date>
    <status>Completed</status>
    <completion_date>2012-11-18T00:00:00</completion_date>
    <service_request_number>12-01906695</service_request_number>
    <type_of_service_request>Pot Hole in Street</type_of_service_request>
    <current_activity>Final Outcome</current_activity>
    <most_recent_action>CDOT Street Cut ... Outcome</most_recent_action>
    <street_address>3510 W NORTH AVE</street_address>
    <zip>60647</zip>
    <x_coordinate>1152732.14127696</x_coordinate>
    <y_coordinate>1910409.38979075</y_coordinate>
    <ward>26</ward>
    <police_district>14</police_district>
    <community_area>23</community_area>
    <latitude>41.91002084292946</latitude>
    <longitude>-87.71435952353961</longitude>
    <location latitude="41.91002084292946"
      longitude="-87.71435952353961" />
  </row>
</row>
</response>

    Предположим, что вы хотите написать скрипт, который отсортирует ZIP-коды по количеству отчетов о выбоинах. Чтобы сделать это, вы можете написать такой код:

from xml.etree.ElementTree import parse
from collections import Counter

potholes_by_zip = Counter()
doc = parse('potholes.xml')
for pothole in doc.iterfind('row/row'):
    potholes_by_zip[pothole.findtext('zip')] += 1
for zipcode, num in potholes_by_zip.most_common():
    print(zipcode, num)

    Единственная проблема с этим скриптом заключается в том, что он читает в память XML-файл целиком. Если же применить код из этого рецепта, программа изменится совсем чуть-чуть:

from collections import Counter

potholes_by_zip = Counter()
data = parse_and_remove('potholes.xml', 'row/row')
for pothole in data:
    potholes_by_zip[pothole.findtext('zip')] += 1
for zipcode, num in potholes_by_zip.most_common():
    print(zipcode, num)

    Но эта версия занимает при запуске всего 7 мегабайт оперативной памяти - огромная экономия налицо!

Обсуждение

    Этот рецепт основывается на двух базовых возможностях модуля ElementTree. Метод iterparse() позволяет обрабатывать XML-документы пошагово. Чтобы использовать его, вы передаете имя файла вместе со списком событий, состоящим из одного или более следующих аргументов: start, end, start-ns и end-ns. Итератор, созданный iterparse(), производит кортежи формата (event, elem), где event - одно из событий списка, а elem - полученный XML-элемент. Например:

>>> data = iterparse('potholes.xml', ('start', 'end'))
>>> next(data)
('start', <Element 'response' at 0x100771d60>)
>>> next(data)
('start', <Element 'row' at 0x100771e68>)
>>> next(data)
('start', <Element 'row' at 0x100771fc8>)
>>> next(data)
('start', <Element 'creation_date' at 0x100771f18>)
>>> next(data)
('end', <Element 'creation_date' at 0x100771f18>)
>>> next(data)
('start', <Element 'status' at 0x1006a7f18>)
>>> next(data)
('end', <Element 'status' at 0x1006a7f18>)
>>>

    События start создаются, когда элемент создан, но еще не наполнен любыми другими данными (например, элементами-потомками). События end создаются, когда элемент завершен. Хотя в данном рецепте это и не показано, события start-ns и end-ns используются для работы с объявлениями пространств имен XML.

    В этом рецепте события start и end используются для управления стеками элементов и тегов. Стеки представляют текущую иерархическую структуру документа в процессе его парсинга, а также используются для определения того, совпадает ли элемент с запрашиваемым путем, переданным в функцию parse_and_remove(). Если совпадение произошло, yield выдает его обратно вызывавшему.

    Следующая инструкция после yield - базовая возможность ElementTree, которая позволяет этому рецепту экономить память:

elem_stack[-2].remove(elem)

    Эта инструкция удаляет выданный ранее элемент из его родителя. Исходя из предположения, что нигде более на него не осталось ссылок, элемент уничтожается, а память высвобождается.

    Конечный эффект итеративного парсинга и удаления узлов - крайне эффективный пошаговый проход по документу. Ни на одном этапе не создается полное дерево документа. Однако можно написать код, который обрабатывает XML- данные прямолинейным способом.

    Главный недостаток этого рецепта - производительность. При тестировании версия, которая читает весь документ в память, отработала в 2 раза быстрее, чем пошаговая. Однако она потребовала в 60 раз больше памяти. Так что если память важна, пошаговый подход дает большой выигрыш.

    На следующем шаге мы рассмотрим преобразование словарей в XML.




Предыдущий шаг Содержание Следующий шаг