在 Python 中生成 XML 的最快方法

Fastest way to generate XML in Python

我有两个 tables(从数据库中提取的 csv 文件),一个包含订单,第二个包含与订单 table 相关的项目。我需要从这两个文件构建一个 XML 文件以具有这种结构(由于可读性而简化):

<ORDERS>
    <ORDER>
        <ORDER_ID>11039515178</ORDER_ID>
        <CUSTOMER_ID>394556458</CUSTOMER_ID>
        <ITEMS>
            <ITEM>
                <PRODUCT_ID>1401817</PRODUCT_ID>
                <AMOUNT>2</AMOUNT>
            </ITEM>
            <ITEM>
                <PRODUCT_ID>1138857</PRODUCT_ID>
                <AMOUNT>10</AMOUNT>
            </ITEM>
            <ITEM>
                <PRODUCT_ID>4707595</PRODUCT_ID>
                <AMOUNT>15</AMOUNT>
            </ITEM>
        </ITEMS>
    </ORDER>
</ORDERS>

我使用此代码生成 XML 对象。它被简化为代码的主要结构,因此易于阅读:

import xml.etree.ElementTree as ET
import pandas as pd

order = pd.read_csv("order.csv", encoding='utf8', keep_default_na=False, dtype=str)
order_item = pd.read_csv("order_item.csv", encoding='utf8', keep_default_na=False, dtype=str)

# create XML
xml_orrder = ET.Element('ORDERS')
for row in order.itertuples():
    item = ET.SubElement(xml_orrder, 'ORDER')

    o_id = ET.Element('ORDER_ID')
    o_id.text = row.order_id
    item.append(o_id)

    customer = ET.Element('CUSTOMER_ID')
    customer.text = row.customer_id
    item.append(customer)

    order_item_id = order_item[order_item['order_id'] == row.order_id]

    items = ET.SubElement(item, 'ITEMS')
    for order_row in order_item_id.itertuples():
        single_item = ET.SubElement(items, 'ITEM')

        item_id = ET.Element('PRODUCT_ID')
        item_id.text = order_row.product_id
        single_item.append(item_id)

        quantity = ET.Element('AMOUNT')
        quantity.text = order_row.quantity_ordered
        single_item.append(quantity)

我的问题是它运行的时间长得令人难以置信(每 1000 个订单大约需要 15 分钟,每个订单大约有 20 件商品)。我想我在这里做错了什么,但我找不到。有没有办法加快速度?使用另一个库? 我试过使用 itertuples() 而不是 iterrows()。但这不是很有帮助。

编辑:

我的数据是这样的:

order = pd.DataFrame({"order_id": range(1000000,1000010,1),
                         "customer_id": np.random.RandomState(0).randint(1000,2000,10)})

order_item = pd.DataFrame({"order_id": np.random.RandomState(0).randint(1000000,1000010,100),
                         "product_id": np.random.RandomState(0).randint(1000,2000,100),
                         "amount": np.random.RandomState(0).randint(1,100,100)})
order_item.sort_values(by="order_id",inplace=True,ignore_index=True)

我不确定你的数据是什么样的,所以我希望这对你有用,我花了几秒钟来处理大约 5000 行:

import pandas as pd
import lxml.etree as et

df_order = pd.read_csv("order.csv", encoding='utf8', keep_default_na=False, dtype=str)
df_order_item = pd.read_csv("order_items.csv", encoding='utf8', keep_default_na=False, dtype=str)

new_orders = df_order.merge(df_order_item, 'left', left_on='order_id', right_on='order_id')

orders = et.Element('ORDERS')
for order_id in new_orders['order_id'].unique():
    rows = new_orders[new_orders['order_id'] == order_id]
    customer_id = int(rows['customer_id'].unique())
    order = et.SubElement(orders, 'ORDER')
    o_id = et.SubElement(order, 'ORDER_ID')
    o_id.text = order_id
    c_id = et.SubElement(order, 'CUSTOMER_ID')
    c_id.text = str(customer_id)
    items = et.SubElement(order, 'ITEMS')
    for product in rows.itertuples():
        item = et.SubElement(items, 'ITEM')
        p_id = et.SubElement(item, 'PRODUCT_ID')
        p_id.text = product.product_id
        amount = et.SubElement(item, 'AMOUNT')
        amount.text = product.quantity_ordered

在编写 XML 或 HTML 时,以文本方式编写通常比增加构建 in-memory XML 文档的费用更快。您可以直接编写文件或使用模板语言(如 jinja 2)。以下是使用多行 f-strings 编写具有所需间距的文档的示例。由于 XML 不关心换行或漂亮的打印,我倾向于在没有额外间距的情况下写作。

代码有点难看,但恕我直言,所有模板都是如此。

import pandas as pd

order = pd.read_csv("order.csv", encoding='utf8', keep_default_na=False, dtype=str)
order_item = pd.read_csv("order_item.csv", encoding='utf8', keep_default_na=False, dtype=str)

with open("out.xml", "w") as outfile:
    outfile.write("""\
<ORDERS>
""")
    for row in order.itertuples():
        outfile.write(
f"""\
    <ORDER>
        <ORDER_ID>{row.order_id}</ORDER_ID>
        <CUSTOMER_ID>f{row.customer_id)</CUSOMTER_ID>
""")

        outfile.write(f"""\
        <ITEMS>
""")

        order_item_id = order_item[order_item['order_id'] == row.order_id]
        for order_row in order_item_id.itertuples():
            outfile.write(f"""\
            <ITEM>
                <PRODUCT_ID>{order_row.product_id}</PRODUCT_ID>
                <AMOUNT>{order_row.quantity_ordered}</AMOUNT>
            </ITEM>
"""
        outfile.write("""\
        </ITEMS>
""")
    outfile.write("""\
    </ORDERS>
</ORDER>"""

显然 pandas to_xml 不处理这种层次结构。您可以将它的一部分直接写入 xml 文件并在 sub-df:

分组上使用 to_xml
df = order.merge(order_item, on='order_id')

with open('output.xml', 'w') as f:
    f.write('<ORDERS>')

    for (ord_id, cust_id), sub_df in df.groupby(['order_id', 'customer_id']):
        f.write(f'\n<ORDER>\n<ORDER_ID>{ord_id}</ORDER_ID>\n<CUSTOMER_ID>{cust_id}</CUSTOMER_ID>\n')
        f.write(sub_df.to_xml(root_name='ITEMS', row_name='ITEM', xml_declaration=False, elem_cols=['product_id', 'amount'], index=False))
        f.write(f'\n</ORDER>')

    f.write('\n</ORDERS>')

如果您发现任何性能改进,请告诉我们!

注意:您还可以使用 kwarg parser=('lxml' 或 'etree')

选择 xml-parser

我已经尝试了上面提到的一些方法,但显着加快整个过程的一件事是使那些嵌套的 <ITEMS> 标签已经在数据库中。我们正在使用雪花,我使用 LISTAGG 按功能分组在 order_item table 上做了一个简单的分组:

CREATE OR REPLACE TABLE "wrk_order_item" AS
SELECT
    "order_id",
    '<ITEMS>' || LISTAGG('<ITEM><PRODUCT_ID>' || "product_id"  || '</PRODUCT_ID>'
        || '<AMOUNT>' || "quantity_ordered"  || '</AMOUNT>'
        || '<PRICE>' || "sell_price" || '</PRICE></ITEM>') || '</ITEMS>' AS "items"
FROM "ORDER_ITEM"
GROUP BY "order_id";

将其与订单 table 合并,并在 python 脚本中删除了订单 table 每次迭代中项目数据框的创建。两个代码(雪花,python)现在都在几秒钟内完成。