Использование zip и списков python для преобразования xml в csv

Я просматривал другие вопросы здесь, в SO, о zip и магии *, которые очень помогли мне понять, как это работает. Например:

Несмотря на то, что мне все еще нужно немного подумать о том, что происходит на самом деле, теперь я лучше понимаю. Итак, чего я пытаюсь добиться, так это преобразовать документ xml в csv. Эта последняя ссылка выше очень близка к тому, что я хочу сделать, однако мой исходный xml не имеет наиболее последовательной структуры, и именно здесь я упираюсь в стену. Вот пример моего исходного xml (упрощенного ради этого примера):

<?xml version="1.0" encoding="utf-8"?>
<root>
    <child>
        <Name>John</Name>
        <Surname>Doe</Surname>
        <Phone>123456</Phone>
        <Phone>654321</Phone>
        <Fax>111111</Fax>
    </child>
    <child>
        <Name>Tom</Name>
        <Surname>Cat</Surname>
        <Phone>98765</Phone>
        <Phone>56789</Phone>
        <Phone>00000</Phone>
    </child>
</root>

Как видите, у меня может быть 2 или более одинаковых элементов под <child>. Кроме того, если определенный элемент не имеет значения, он даже не будет существовать (как во втором <child>, где нет <Fax>).

Это код, который у меня сейчас есть:

data = etree.parse(open('test.xml')).findall(".//child")
tags = ('Name', 'Surname', 'Phone', 'Fax')

for child in data:
    for a in zip(*[child.findall(x) for x in tags]):
        print([x.text for x in a])

>> Result:

['John', 'Doe', '123456', '111111']

Хотя это дает мне формат, который я могу использовать для записи csv, у него есть две проблемы:

  1. Он пропускает 2-го ребенка, потому что у него нет элемента <Fax> (я полагаю). Если я ищу только элементы, которые существуют в обоих дочерних элементах, устанавливая tags = ('Name', 'Surname'), тогда у меня есть 2 списка (отлично!)

  2. У этого первого ребенка на самом деле есть 2 телефонных номера, но возвращается только один

Из того, что я мог проверить, вещи начинают исчезать, когда в игру вступает zip * ... Как я могу установить значение по умолчанию, чтобы я мог оставить пустые значения?

Обновление: чтобы было более понятно, что я собираюсь сделать, вот ожидаемый формат вывода (CSV с разделителем с запятой, где несколько значений в каждом поле разделены запятой):

John;Joe;123456,654321;111111;
Tom;Cat;98765,56789;00000;;

Спасибо!


person bergonzzi    schedule 08.07.2013    source источник


Ответы (2)


Я взломал это вместе. Прочтите документацию модуля csv и внесите соответствующие изменения, если вам нужен более конкретный формат.

from csv import DictWriter
from StringIO import StringIO
import xml.etree
from xml.etree import ElementTree

xml_str = \
'''
<?xml version="1.0" encoding="utf-8"?>
<root>
    <child>
        <Name>John</Name>
        <Surname>Doe</Surname>
        <Phone>123456</Phone>
        <Phone>654321</Phone>
        <Fax>111111</Fax>
    </child>
    <child>
        <Name>Tom</Name>
        <Surname>Cat</Surname>
        <Phone>98765</Phone>
        <Phone>56789</Phone>
        <Phone>00000</Phone>
    </child>
</root>
'''

root = ElementTree.parse(StringIO(xml_str.strip()))
entry_list = []
for child_tag in root.iterfind("child"):
    child_tags = child_tag.getchildren()

    tag_count = {}
    [tag_count.__setitem__(tag.tag, tag_count.get(tag.tag, 0) + 1) for tag in child_tags]

    m_count = dict([(key, 0) for (key, val) in filter(lambda (x, y): y > 1, tag_count.items())])

    enum = lambda x: ("%s%s" % (x.tag, (" %d" % m_count.setdefault(x.tag, m_count.pop(x.tag) + 1)) if(tag_count[x.tag] > 1) else ""), x.text)
    tmp_dict = dict([enum(tag) for tag in child_tags])

    entry_list.append(tmp_dict)

field_order = ["Name", "Surname", "Phone 1", "Phone 2", "Phone 3", "Fax"]
field_check = lambda q: field_order.index(q) if(field_order.count(q)) else sys.maxint

all_fields = list(reduce(lambda x, y: x | set(y.keys()), entry_list, set([])))
all_fields.sort(cmp=lambda x, y: field_check(x) - field_check(y))

with open("test.csv", "w") as file_h:
    writer = DictWriter(file_h, all_fields, restval="", extrasaction="ignore", dialect="excel", lineterminator="\n")
    writer.writerow(dict(zip(all_fields, all_fields)))
    writer.writerows(entry_list)
person dilbert    schedule 09.07.2013
comment
Wowww... это работает отлично, но мне придется потратить несколько часов, пытаясь понять все, что вы только что сделали! Спасибо! - person bergonzzi; 09.07.2013

Вы говорите, что касается вашей первой проблемы, что «[i] если я ищу только элементы, которые существуют в обоих дочерних элементах ... у меня есть 2 списка назад», подразумевая, что отсутствие вывода для второго дочернего элемента имеет что-то делать с взаимодействием между двумя узлами child. Это не так. Аспект поведения zip, который вы, кажется, упускаете из виду, заключается в том, что zip прекращает обработку своих аргументов после того, как он исчерпал самый короткий.

Рассмотрим вывод следующего упрощения вашего кода:

for child in data:
    print [child.findall(x) for x in tags]

Вывод будет (без адресов памяти):

[[<Element 'Name'>], [<Element 'Surname'>], [<Element 'Phone'>, <Element 'Phone'>], [<Element 'Fax'>]]
[[<Element 'Name'>], [<Element 'Surname'>], [<Element 'Phone'>, <Element 'Phone'>, <Element 'Phone'>], []]

Обратите внимание, что во втором списке есть пустой подсписок (поскольку второй дочерний элемент не имеет узла Fax). Это означает, что когда вы объединяете эти подсписки вместе, процесс немедленно останавливается и возвращает пустой список; при первом проходе он уже исчерпал один из подсписков. Поэтому ваш второй дочерний элемент опущен в выводе; это не имеет ничего общего с элементами, разделяемыми между детьми.

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

Я не совсем уверен, как вы хотите, чтобы ваш вывод выглядел, но если вы просто пытаетесь создать для каждого дочернего узла список, содержащий текст каждого элемента в этом узле, вы можете сделать что-то вроде:

for child in data:
    print [x.text for x in child]

Это произведет:

['John', 'Doe', '123456', '654321', '111111']
['Tom', 'Cat', '98765', '56789', '00000']
person Alp    schedule 08.07.2013
comment
Привет, Альп, спасибо за ответ. Однако не хватает нескольких моментов. Я отредактирую свой ответ, чтобы добавить ожидаемый результат, чтобы сделать его более понятным. Моя цель - преобразовать результат в формат csv, чтобы ваш вывод не был в порядке в этом контексте. Вот что я хочу в конце: John;Joe;123456,654321;111111; Tom;Cat;98765,56789;00000;; Итак, если у меня есть 2 элемента <phone>, они должны оказаться вместе в одном поле csv. Кроме того, порядок важен, поскольку каждое поле в формате csv должно, очевидно, соответствовать соответствующему заголовку. - person bergonzzi; 09.07.2013