使用python的zip和list comprehensions将xml转换为csv

时间:2013-07-08 22:18:34

标签: python xml csv zip list-comprehension

我一直在寻找关于拉链和魔术*的其他问题,这些问题帮助我了解了它的工作原理。例如:

即使我仍然需要考虑一下实际发生的事情,我现在有了更好的理解。所以我想要实现的是将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>

如您所见,我可以在<child>下拥有2个或更多相同的元素。此外,如果某个元素没有值,它甚至都不存在(就像第二个<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. 它跳过了第二个孩子,因为它没有<Fax>元素(我想)。如果我只通过设置tags = ('Name', 'Surname')来搜索两个孩子中存在的元素,那么我有2个列表(很棒!)

  2. 第一个孩子实际上有2个电话号码但只返回一个

  3. 根据我的测试,当zip *进入游戏时,东西开始消失......我怎么可能设置一个默认值,这样我可以保持空值?

    更新:为了更清楚我打算做什么,这是预期的输出格式(带分号分隔符的CSV,每个字段中的多个值用逗号分隔):

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

    谢谢!

2 个答案:

答案 0 :(得分:0)

你说,关于你的第一个问题,“我只搜索两个孩子中存在的元素......我有两个列表,”暗示第二个孩子缺乏输出与两个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行为的相同原则解释了你的第二个问题。请注意,上面的第一个输出列表包含四个元素:一个用于三个标签的长度为1的列表,以及一个带有两个电话元素的长度为二的列表。当您将它们压缩在一起时,在耗尽任何子列表后,该过程将再次停止。在这种情况下,最短子列表的长度为1,因此结果只从电话子列表中抽取一个元素。

我不确定你想要的输出是什么样的,但如果你只是想为每个子节点构建一个包含该节点中每个元素的文本的列表,你可以做类似的事情。 :

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

这会产生:

['John', 'Doe', '123456', '654321', '111111']
['Tom', 'Cat', '98765', '56789', '00000']

答案 1 :(得分:0)

我一起砍了这个。阅读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)