python-3.x 用于解析小型、严格结构化XML文件的最快lxml接口

jchrr9hc  于 2023-02-14  发布在  Python
关注(0)|答案(3)|浏览(119)

我有一个XML文档,具有以下结构,我期待解析:

<base>
  <intermediate>
    <element>
      <field1>some_text</field2>
      <field2>more_text</field2>
    </element>
    # <element> repeated about 2000 times
  </intermediate>
</base>

我的第一种方法是使用lxml的xslt接口将其转换为CSV,然后将此CSV读入python列表。
在表现不太像我所希望的那样之后,我想我会给予以下的尝试:

for intermediate in root.xpath('./intermediate'):
    for element in index.xpath('./element[field2/text()]'):
        field1 = element.xpath('field1/text()')[0]
        field2 = element.xpath('field2/text()')[0]

结果是慢了很多。-这并不奇怪。
但是,我是否以最佳方式使用lxml呢?它的特性如此丰富,以至于我不能确定我没有为给定的问题选择一个糟糕的接口。

编辑1:基准测试结果

  • 解决方案1:Python循环树遍历(如OP)- 77.9ms
  • 解决方案2:交互解析- 11.4ms
  • 解决方案3:XML解析和xslt(无csv解析)- 11.9ms
  • 解决方案4:使用libxml 2在C中使用cython链接SAX解析器(在python中读取文件)- 2.33ms

用于xslt的代码:

xml = lxml.etree.parse('my_xml_file.xml')
xsl = lxml.etree.parse("my_xsl_file.xsl")
transformer = lxml.etree.XSLT(xsl)    
result = transformer(xml)
csv_data = str(result)

使用的XSL:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="text" encoding="UTF-8"/>

    <xsl:template match="/base">
        <xsl:text>field1|field2&#10;</xsl:text>
        <xsl:apply-templates select='intermediate/element[field2/text()]'/>
    </xsl:template>

    <xsl:template match="element">
        <xsl:value-of select="field1"/><xsl:text>|</xsl:text>
        <xsl:value-of select="field2"/><xsl:text>&#10;</xsl:text>
    </xsl:template>

</xsl:stylesheet>
i2byvkas

i2byvkas1#

下面的代码迭代地解析XML文件:

import lxml.etree as ET

with open("input.xml", "rb") as f:
    context = ET.iterparse(f)
    for action, elem in context:
        if elem.tag == "field1":
            field1 = elem.text

        if elem.tag == "field2":
            field2 = elem.text

        if elem.tag == "element":
            print(field1, field2)
            field1 = None
            field2 = None

这里lxml以基于事件的方式工作,每次遇到end元素(</xyz>),就会生成一个end事件,并由for循环处理。
变量field1field2会根据元素结束的位置进行设置,这里隐含的假设是<field1><field2>只出现在<element>内部,并且不再嵌套。
如果这些都得到了保证,那么当我们遇到一个结束的</element>时,这两个变量就包含了预期的字符串,如果这些不一定是真的,那么在迭代过程中就需要维护某种状态。
在Python内部,这应该是最快的,因为它只依赖于在事件发生时解析事件,而根本不使用任何XPath。

3npbholx

3npbholx2#

您可以尝试iterparse

from lxml import etree as ET

def parse_xml(file_name, tag_name):
    for event, element in ET.iterparse(file_name, tag = tag_name):
        if (element.xpath('field2/text()')):
            yield (element.find('field1').text, element.find('field2').text)

result = [tuple for tuple in parse_xml('input-file.xml', 'element')]

print(result)
sr4lhrrt

sr4lhrrt3#

这个答案是将备选方案4记录在@CPLTarun的OP中。
注意,这不是可生产的代码。这只是一个快速破解的基准测试。代码可能有bug。而且,它什至不能从python访问读取的数据,它只是把它保存在cython变量中,不断被覆盖。-这段代码并不打算有用,而是为完全充实的代码提供一个运行时的下限。
我可能从别人那里复制了这个代码的 backbone 。如果这个代码是你的,让我知道,我会相信你。
下面是jupyter笔记本上的代码,每个新单元格用#%%表示:

#%%
%load_ext cython

#%% cython SAX parser
%%cython
# distutils: include_dirs = <your liblxml2 include directory>
# distutils: libraries = libxml2
# distutils: library_dirs = <your liblxml2 lib directory>

from lxml.includes.xmlparser cimport (xmlParserCtxt, xmlSAXHandler, 
        xmlCreatePushParserCtxt, xmlParseChunk, xmlFreeParserCtxt, xmlCleanupParser,         
        XML_SAX2_MAGIC, xmlCtxtResetPush,
        startElementNsSAX2Func, endElementNsSAX2Func, charactersSAXFunc)
from libc.string cimport memset, memcpy, strcmp
from libc.stdlib cimport malloc, free
from lxml.includes.tree cimport const_xmlChar

cdef xmlParserCtxt* c_ctxt

cpdef void init_parser():
    global c_ctxt
    cdef xmlSAXHandler SAXHander

    memset(&SAXHander, 0, sizeof(xmlSAXHandler))
    SAXHander.initialized = XML_SAX2_MAGIC
    SAXHander.startElementNs = <startElementNsSAX2Func>OnStartElementNs
    SAXHander.endElementNs = <endElementNsSAX2Func>OnEndElementNs
    SAXHander.characters = <charactersSAXFunc>_handleSaxData

    c_ctxt = xmlCreatePushParserCtxt(&SAXHander, NULL, NULL, 0, NULL)

cpdef void parse_chunk(char* chars):
    global c_ctxt
    xmlParseChunk(c_ctxt, chars, len(chars), 0)
    xmlParseChunk(c_ctxt, NULL, 0, 1)
    xmlCtxtResetPush(c_ctxt, NULL, 0, NULL, NULL)

cpdef void cleanup_parser():
    global c_ctxt
    xmlFreeParserCtxt(c_ctxt)
    xmlCleanupParser()

cdef char* field2 = NULL
cdef char* field1 = NULL
cdef char* element = NULL

cdef char* FIELD1 = b'field1'
cdef char* FIELD2 = b'field2'

cdef void OnStartElementNs(void* ctx,
                           const_xmlChar* localname,
                           const_xmlChar* prefix,
                           const_xmlChar* URI,
                           int nb_namespaces,
                           const_xmlChar** namespaces,
                           int nb_attributes,
                           int nb_defaulted,
                           const_xmlChar** attributes):
    c_ctxt = <xmlParserCtxt*>ctx

    global element

    if strcmp(<char*>localname, b'field2') == 0:
        element = FIELD2
    elif strcmp(<char*>localname, b'field1') == 0:
        element = FIELD1

cdef void OnEndElementNs(void* ctx,
                           const_xmlChar* localname,
                           const_xmlChar* prefix,
                           const_xmlChar* URI):
    c_ctxt = <xmlParserCtxt*>ctx
    global element
    if strcmp(<char*>localname, b'element') == 0:
        if strcmp(field2, b'') != 0:
            element= NULL
    else:
        element = NULL

cdef void _handleSaxData(void* ctxt, const_xmlChar* c_data, int data_len):
    # can only be called if parsing with a target
    global field2, field1
    if element == FIELD1:
        if field1 is not NULL:
            free(field1)
        field1 = <char*>malloc(sizeof(char) * (data_len+1))
        field1[data_len] = 0
        memcpy(field1, c_data, data_len)
    if element == FIELD2:
        if field2 is not NULL:
            free(field2)
        field2 = <char*>malloc(sizeof(char) * data_len+1)
        field2[data_len] = 0
        memcpy(field2, c_data, data_len)

#%% cython libxml
%%timeit -r20
with open('benchmark.xml', 'rb') as file:
    xml = file.read()
init_parser()
parse_chunk(xml)
cleanup_parser()

注意:在清理之前,你可以在处理多个文件时只初始化解析器一次,这样可以保存时间。

相关问题