python 使用LXML从XML文档中提取内容的最快方法是什么?

wz1wpwve  于 2023-01-08  发布在  Python
关注(0)|答案(2)|浏览(123)

我正在使用LXML从一堆XML文件中提取信息。我想知道我处理这个任务的方式是否是最有效的。现在我在LXML中使用xpath()方法来识别特定的目标,然后在lxml中使用不同的方法来提取这些信息。
正如我在前面的问题(用LXML Python处理XML文件非常慢)中注意到的,当文件达到一定大小时,使用etree.parse(file)或etree.parse(file). getroot()会非常慢。它们不需要很大,12MB的XML文件已经很慢了。
我现在想知道的是是否有更快的替代方法。在LXML文档中,使用XPath类可能比使用XPath()方法更快。我遇到的问题是XPath类处理Element对象,而不是etree.parse()生成的ElementTree对象。
我所需要的是一些比我现在所做的更快的替代方法,基本上是下面内容的一些变体。这只是我用来从相关XML文件中提取信息的许多同类脚本中的一个示例。如果您认为是使用正则表达式导致了速度缓慢,我做过一些测试,其中使用了XPath root_element.xpath('//tok[text()="lo"]'),但没有使用正则表达式。处理20到30MB文件所花费的时间可能会少一些,但不会少很多。无论我对所有这些文件做什么,如果它涉及到检查XPath表达式并执行某些操作的for循环,它只是需要很长的时间比人们所期望的使用最新的Python和Mac与M1 Max芯片。我有一个旧的笔记本电脑,当我尝试同样的事情,它需要3天!!

XMLDIR = "/path_to_dir_with_xml_files"
myCSV_FILE = "/path_to_some_csv_file.csv"

ext = ".xml"

def xml_extract(root_element):

    for el in root_element.xpath('//tok[re:match(., "^[EeLl][LlOoAa][Ss]*$") and not(starts-with(@xpos, "D"))]',
        namespaces={"re": "http://exslt.org/regular-expressions"}): 

        target = el.text
        # allRelevantElements = el.xpath('preceding::tok[position() >= 1 and not(position() > 6)]/following::tok[position() >= 1 and not(position() > 6)]')
        RelevantPrecedingElements = el.xpath(
            "preceding::tok[position() >= 1 and not(position() > 6)]"
        )
        RelevantFollowingElements = el.xpath(
            "following::tok[position() >= 1 and not(position() > 6)]"
        )
        context_list = []

        for elem in RelevantPrecedingElements:
            elem_text = "".join(elem.itertext())
            assert elem_text is not None
            context_list.append(elem_text)

        # adjective = '<' + str(el.text) + '>'
        target = f"<{el.text}>"
        print(target)
        context_list.append(target)

        following_context = []
        for elem in RelevantFollowingElements:
            elem_text = "".join(elem.itertext())
            assert elem_text is not None
            following_context.append(elem_text)

        lema_fol = el.xpath('following::tok[1]')[0].get('lemma') if el.xpath('following::tok[1]') else None
        lema_prec = el.xpath('preceding::tok[1]')[0].get('lemma') if el.xpath('preceding::tok[1]') else None
        xpos_fol = el.xpath('following::tok[1]')[0].get('xpos') if el.xpath('following::tok[1]') else None
        xpos_prec = el.xpath('preceding::tok[1]')[0].get('xpos') if el.xpath('preceding::tok[1]') else None
        form_fol = el.xpath('following::tok[1]')[0].text if el.xpath('following::tok[1]') else None
        form_prec = el.xpath('preceding::tok[1]')[0].text if el.xpath('preceding::tok[1]') else None

        context = " ".join(context_list)
        print(f"Context is: {context}")

        llista = [
            context,
            lema_prec,
            xpos_prec,
            form_prec,
            target,
            lema_fol,
            xpos_fol,
            form_fol,
        ]

        writer = csv.writer(csv_file, delimiter=";")
        writer.writerow(llista)

with open(myCSV_FILE, "a+", encoding="UTF8", newline="") as csv_file:

    for root, dirs, files in os.walk(XMLDIR):

        for file in files:
            if file.endswith(ext):
                file_path = os.path.join(XMLDIR, file)
                file_root = et.parse(file_path).getroot()
                doc = file
                xml_extract(file_root)

下面是一段XML文档的示例,其中包含了我正在使用的XPath表达式的匹配项。函数'xml_extract'将在此匹配项上被调用,不同的信息段将被正确地提取并存储到CSV文件中。这工作得很好,并且做了我想要做的事情,但是它太慢了。

<tok id="w-6387" ord="24" lemma="per" xpos="SPS00">per</tok>
<tok id="w-6388" ord="25" lemma="algun" xpos="DI0FP0">algunes</tok>
<tok id="w-6389" ord="26" lemma="franquesa" xpos="NCFP000">franqueses</tok>
<tok id="w-6390" nform="el" ord="27" lemma="el" xpos="L3MSA">lo</tok>
<tok id="w-6391" ord="28" lemma="haver" xpos="VMIP1S0">hac</tok>

编辑:
提供一些额外的相关信息,可能对试图帮助我的@人有所帮助。前面的XML内容相当直接,但文档的结构有时会变得相当复杂。我正在研究中世纪的文本,这些文本中的XML标签可能包含不同类型的信息。"tok"标签包含语言注解,这是我感兴趣的。在正常情况下,XML看起来与前面的示例类似,但是在某些情况下,编辑会包含其他带有手稿元数据的标记(例如,是否存在抄写员的修改或删除,是否存在新的节或新的页,节的标题,等等)。这可以让你对能找到什么有所了解,也许还能帮助你理解我为什么要使用这种方法。在这个阶段,大多数元数据对我来说都不相关。相关的是包含在"dtok"标签中的信息。当缩略形式必须分解为独立的单词时,这些标签是"tok"的子标签。这个系统允许将缩略语可视化为单个单词,但提供有关其组成部分的语言信息。标签是自动完成的,但错误百出。我提取信息的目标之一是能够检测模式,从而帮助我们以半自动化的方式改进语言注解。

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE document SYSTEM "estcorpus.dtd">
<TEI title="Full title" name="Doc_I44">
  <!--this file comes from the Stand-by folder: it needs to be checked because it has inaccurate xml tags-->
  <header>
    <filiation type="obra">Book title</filiation>
    <filiation type="autor">Author name</filiation>
    <filiation type="data">Segle XVIIa</filiation>
    <filiation type="tipologia">Letters</filiation>
    <filiation type="dialecte">Oc:V</filiation>
  </header>
  <text section="logic" lang="català" analyse="norest">
    <pb n="1r" type="folio" id="e-1" />
    <space />
    <space />
    <mark name="empty line" />
    <add>
      <tok form="IX" id="w-384" ord="1" lemma="IX" xpos="Z">IX</tok>
    </add>
    <mark name="lang=Latin" />
    <tok id="w-385" ord="2" lemma="morir" xpos="TMMS">Mort</tok>
    <tok id="w-386" ord="3" lemma="de" xpos="SPC00">de</tok>
    <tok id="w-387" ord="4" lemma="sant" xpos="NCMS000">sent</tok>
    <tok id="w-388" ord="5" lemma="Vicent" xpos="NP00000">Vicent</tok>
    <tok id="w-389" ord="6" lemma="Ferrer" xpos="NPCS00">Ferrer</tok>
    <tok id="w-99769" ord="23" xpos="CC" lemma="i">e</tok>
    <tok id="w-99770" ord="24" lemma="jo" xpos="PP1CSN00">jo</tok>
    <tok id="w-99771">dar-los 
    <dtok form="dar" id="d-99771-1" ord="25" lemma="dar" xpos="VMN0000" />
    <dtok form="los" id="d-99771-2" ord="26" lemma="els" xpos="L3CP0" /></tok>
    <tok id="w-99772" ord="27" lemma="haver" xpos="V0IF3S0">hé</tok>
    <tok id="w-99773" ord="28" lemma="diner" xpos="NCMP000">diners</tok>
    <space />
    <mark name="/lang" />
    <foreign name="Latin">
      <tok id="w-390" ord="7" lemma="any" xpos="CC">Annum</tok>
    </foreign>
  </text>
</TEI>

这是使用LXML处理XML文件的唯一方法吗?还是有更快的方法?目前,处理一个30MB的文件并获取与特定XPath表达式相关的信息需要21分钟。我使用的是Python 3.11和一台功能强大的计算机。我不禁想到,一定有更有效的方法来完成我正在做的事情。我有大约400个文件在目录中。它需要永远每次我必须通过他们和做一些事情。
编辑2:
按照建议使用编译过的XPath表达式之后,我使用@Martin Honnen提供的修改过的代码运行了一个测试,下面是结果。我还没有尝试其他推荐的替代方法。当我尝试时,我会报告。

File sizes:
A-01.xml : 13.2 MB
A-02.xml : 31.4 MB
A-03.xml : 7.7 MB
A-04.xml : 11.6 MB
I-44.xml : 22.9 MB

Original run:

File:      seconds

A-01.xml ➝ 56.845274686813354
A-02.xml ➝ 1281.4102880954742
A-03.xml ➝ 80.60795021057129
A-04.xml ➝ 149.65892505645752
I-44.xml ➝ 983.7257928848267

With compiled XPath expressions:

File:      seconds

A-01.xml ➝ 59.663841009140015
A-02.xml ➝ 1533.5482828617096
A-03.xml ➝ 78.68556118011475
A-04.xml ➝ 149.15855598449707
I-44.xml ➝ 876.2536578178406
mwecs4sa

mwecs4sa1#

    • 根据您的详细说明修改我的答案:**:

你能试试XMLPullParser()吗,应该很快,而且不会阻塞你的机器。如果你有非常大的文件,你可以决定读取的哪一部分应该馈送到解析器。在我的例子中,我加载了整个XML,但在实际工作中一定不是这样的。

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

with open('jfontana.xml', 'r') as input_file:
    xml = input_file.read()

parser = ET.XMLPullParser(['end'])
parser.feed(xml)

data = []
for event, elem in parser.read_events():
    #print(elem)
    if elem.tag == 'dtok':
        #print(elem.tag, elem.text, elem.attrib)
        data.append(elem.attrib)
        
df = pd.DataFrame.from_dict(data)
df.to_csv("jfontana.csv")
print(df)

输出:

form         id ord lemma     xpos
0  dar  d-99771-1  25   dar  VMN0000
1  los  d-99771-2  26   els    L3CP0

或者,如果您对所有tok和dtok感兴趣:

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

with open('jfontana.xml', 'r') as input_file:
    xml = input_file.read()

parser = ET.XMLPullParser(['end'])
parser.feed(xml)

data = []
for event, elem in parser.read_events():
    #print(elem)
    if event =='end' and 'tok' in elem.tag:
        #print(elem.tag, elem.text, elem.attrib)
        data.append(elem.attrib)

        
df = pd.DataFrame.from_dict(data)
df.to_csv("jfontana_all.csv")
print(df)

输出:

form         id  ord   lemma      xpos
0    IX      w-384    1      IX         Z
1   NaN      w-385    2   morir      TMMS
2   NaN      w-386    3      de     SPC00
3   NaN      w-387    4    sant   NCMS000
4   NaN      w-388    5  Vicent   NP00000
5   NaN      w-389    6  Ferrer    NPCS00
6   NaN    w-99769   23       i        CC
7   NaN    w-99770   24      jo  PP1CSN00
8   dar  d-99771-1   25     dar   VMN0000
9   los  d-99771-2   26     els     L3CP0
10  NaN    w-99771  NaN     NaN       NaN
11  NaN    w-99772   27   haver   V0IF3S0
12  NaN    w-99773   28   diner   NCMP000
13  NaN      w-390    7     any        CC
e4eetjau

e4eetjau2#

基于编译XPath表达式一次的建议,以及我的评论,即您实际上多次执行XPath求值前后的所有操作,而不是我尝试使用的一次

match_xpath = et.XPath('//tok[re:match(., "^[EeLl][LlOoAa][Ss]*$") and not(starts-with(@xpos, "D"))]',
        namespaces={"re": "http://exslt.org/regular-expressions"})

preceding_xpath = et.XPath('preceding::tok[position() >= 1 and not(position() > 6)]')

following_xpath = et.XPath('following::tok[position() >= 1 and not(position() > 6)]')

def xml_extract(root_element):

    for el in match_xpath(root_element):

        target = el.text
        # allRelevantElements = el.xpath('preceding::tok[position() >= 1 and not(position() > 6)]/following::tok[position() >= 1 and not(position() > 6)]')
        RelevantPrecedingElements = preceding_xpath(el)
        prec1 = RelevantPrecedingElements[-1]
        RelevantFollowingElements = following_xpath(el)
        foll1 = RelevantFollowingElements[0]
        context_list = []

        for elem in RelevantPrecedingElements:
            elem_text = "".join(elem.itertext())
            assert elem_text is not None
            context_list.append(elem_text)

        # adjective = '<' + str(el.text) + '>'
        target = f"<{el.text}>"
        print(target)
        context_list.append(target)

        following_context = []
        for elem in RelevantFollowingElements:
            elem_text = "".join(elem.itertext())
            assert elem_text is not None
            following_context.append(elem_text)

        lema_fol = foll1.get('lemma') if foll1 is not None else None
        lema_prec = prec1.get('lemma') if prec1 is not None else None
        xpos_fol = foll1.get('xpos') if foll1 is not None else None
        xpos_prec = prec1.get('xpos') if prec1 is not None else None
        form_fol = foll1.text if foll1 is not None else None
        form_prec = prec1.text if prec1 is not None else None

        context = " ".join(context_list)
        print(f"Context is: {context}")

        llista = [
            context,
            lema_prec,
            xpos_prec,
            form_prec,
            target,
            lema_fol,
            xpos_fol,
            form_fol,
        ]

        writer = csv.writer(csv_file, delimiter=";")
        writer.writerow(llista)

with open(myCSV_FILE, "a+", encoding="UTF8", newline="") as csv_file:

    for root, dirs, files in os.walk(XMLDIR):
        for file in files:
            if file.endswith(ext):
                file_path = os.path.join(XMLDIR, file)
                file_root = et.parse(file_path).getroot()
                doc = file
                xml_extract(file_root)

并检查这是否提高了性能。
作为另一种选择,我现在还尝试在Python列表上实现大多数比较,而不是在lxmlXPath中实现,我在XSLT 3中尝试过的东西就像Python3一样实现了(例如,首先用XPath选择所有tok元素,然后在XSLT中查找序列,或者在Python中查找所有toks的列表,以查找regexp匹配toks之一的索引,并查找前面的和/或在该序列/列表中跟随):

import os
import csv
import re

from lxml import etree as et

XMLDIR = "original-samples"
myCSV_FILE = "lxml-single-xpath-py-list-original-samples.csv"

ext = ".xml"

tok_path = et.XPath('//tok')

def xml_extract(root_element):

    all_toks = tok_path(root_element)

    matching_toks = filter(lambda tok: re.match(r'^[EeLl][LlOoAa][Ss]*$', "".join(tok.itertext())) is not None and not(tok.get('xpos').startswith('D')), all_toks)

    for el in matching_toks: 

        target = "".join(el.itertext())
        pos = all_toks.index(el)
        
        RelevantPrecedingElements = all_toks[max(pos - 6, 0):pos]

        prec1 = RelevantPrecedingElements[-1]
        foll1 = all_toks[pos + 1]

        context_list = []

        for elem in RelevantPrecedingElements:
            elem_text = "".join(elem.itertext())
            assert elem_text is not None
            context_list.append(elem_text)

        # adjective = '<' + str(el.text) + '>'
        target = f"<{target}>"
        print(target)
        context_list.append(target)

        lema_fol = foll1.get('lemma') if foll1 is not None else None
        lema_prec = prec1.get('lemma') if prec1 is not None else None
        xpos_fol = foll1.get('xpos') if foll1 is not None else None
        xpos_prec = prec1.get('xpos') if prec1 is not None else None
        form_fol = foll1.text if foll1 is not None else None
        form_prec = prec1.text if prec1 is not None else None

        context = " ".join(context_list)
        print(f"Context is: {context}")

        llista = [
            context,
            lema_prec,
            xpos_prec,
            form_prec,
            target,
            lema_fol,
            xpos_fol,
            form_fol,
        ]

        writer = csv.writer(csv_file, delimiter=";")
        writer.writerow(llista)

with open(myCSV_FILE, "a+", encoding="UTF8", newline="") as csv_file:

    for root, dirs, files in os.walk(XMLDIR):
        for file in files:
            if file.endswith(ext):
                file_path = os.path.join(XMLDIR, file)
                file_root = et.parse(file_path).getroot()
                doc = file
                xml_extract(file_root)

作为一种替代方法,首先对于单个文件,SaxonC(https://saxonica.com/saxon-c/1199/)和XSLT 3如何使用下面这样的XSLT样式表(但是为了测试,我不得不注解掉所有示例中的<!DOCTYPE ..>节点,因为没有提供引用的DTD),这会很有趣:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="#all"
    version="3.0">
  
  <xsl:output method="text"/>

  <xsl:template match="/">
    <xsl:variable name="toks" select="//tok"/>
    <xsl:variable name="toks-id" select="$toks/generate-id()"/>
    <xsl:for-each select="$toks[matches(., '^[EeLl][LlOoAa][Ss]*$') and not(starts-with(@xpos, 'D'))]">
      <xsl:variable name="pos" select="index-of($toks-id, generate-id())"/>
      <xsl:variable name="target" select="'&lt;' || . || '>'"/>
      <xsl:variable name="prec-tok" select="$toks[$pos - 1]"/>
      <xsl:variable name="foll-tok" select="$toks[$pos + 1]"/>
      <xsl:value-of 
        select="let $s := string-join(($toks[position() = ($pos - 6) to ($pos - 1)], $target), ' ') return if (contains($s, '&quot;')) then '&quot;' || replace($s, '&quot;', '&quot;&quot;') || '&quot;' else $s, 
                $prec-tok/@lemma => string(),
                $prec-tok/@xpos => string(),
                $prec-tok => string(),
                $target,
                $foll-tok/@lemma => string(),
                $foll-tok/@xpos => string(),
                $foll-tok => string()" 
                separator=";"/>
      <xsl:text>&#10;</xsl:text>
    </xsl:for-each>
  </xsl:template>
  
</xsl:stylesheet>

当然,XSLT 3 with Saxon也可以轻松地处理一个目录中的所有.xml文件,以生成单个输出文件(例如'csv'):

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="#all"
    version="3.0">

  <xsl:param name="csv-path" select="'saxonc-original-samples-single-xslt-to-file.csv'"/>

  <xsl:param name="directory-uri" as="xs:string" select="'original-samples-no-dtd/'"/>

  <xsl:param name="ext" as="xs:string" select="'*.xml'"/>

  <xsl:template name="xsl:initial-template">
    <xsl:result-document href="{$csv-path}">
      <xsl:apply-templates select="uri-collection($directory-uri || '?select=' || $ext)!doc(.)"/>
    </xsl:result-document>
  </xsl:template>
  
  <xsl:output method="text"/>

  <xsl:template match="/">
    <xsl:message expand-text="yes">Processing {base-uri()}</xsl:message>
    <xsl:variable name="toks" select="//tok"/>
    <xsl:variable name="toks-id" select="$toks/generate-id()"/>
    <xsl:for-each select="$toks[matches(., '^[EeLl][LlOoAa][Ss]*$') and not(starts-with(@xpos, 'D'))]">
      <xsl:variable name="pos" select="index-of($toks-id, generate-id())"/>
      <xsl:variable name="target" select="'&lt;' || . || '>'"/>
      <xsl:variable name="prec-tok" select="$toks[$pos - 1]"/>
      <xsl:variable name="foll-tok" select="$toks[$pos + 1]"/>
      <xsl:value-of 
        select="let $s := string-join(($toks[position() = ($pos - 6) to ($pos - 1)], $target), ' ') return if (contains($s, '&quot;')) then '&quot;' || replace($s, '&quot;', '&quot;&quot;') || '&quot;' else $s, 
                $prec-tok/@lemma => string(),
                $prec-tok/@xpos => string(),
                $prec-tok => string(),
                $target,
                $foll-tok/@lemma => string(),
                $foll-tok/@xpos => string(),
                $foll-tok => string()" 
                separator=";"/>
      <xsl:text>&#10;</xsl:text>
    </xsl:for-each>
  </xsl:template>
  
</xsl:stylesheet>

最后一个示例可以使用SaxonC的Python API运行,例如:

from saxonc import *

with PySaxonProcessor(license=True) as proc:
    print(proc.version)

    proc.set_cwd('.')
    
    xslt30_processor = proc.new_xslt30_processor()

    xslt30_executable = xslt30_processor.compile_stylesheet(stylesheet_file = 'xslt3-original-samples-to-csv.xsl')

    if xslt30_processor.exception_occurred:
        print(xslt30_processor.error_message)
    else:

        xslt30_executable.call_template_returning_file(template_name = None, output_file = 'xslt3-result.xml')
        if xslt30_executable.exception_occurred:
            print(xslt30_executable.error_message)

相关问题