【问题标题】:Parsing large xml file with lxml SAX使用 lxml SAX 解析大型 xml 文件
【发布时间】:2020-03-28 13:37:24
【问题描述】:

我有一个像这样的巨大 xml 文件

<environment>
    <category name='category1'>
        <peoples>
            <people>
                <name>Mary</name>
                <city>NY</city>
                <age>10</age>
            </people>
            <people>
                <name>Jane</name>
                <city>NY</city>
                <age>19</age>
            </people>
            <people>
                <name>John</name>
                <city>NY</city>
                <age>20</age>
            </people>
            <people>
                <name>Carl</name>
                <city>DC</city>
                <age>11</age>
            </people>
            ...
        </people>
    </category>
    <category name='category2'>
    ...
    </category
</environment>

我想将 xml 文件和输出解析为字典,其中键是类别的名称(示例中为 category1、category2)和每个类别可能不同的值字典。现在我只对类别 1 感兴趣,我想在其中创建一个字典,其中键是名称,值是年龄,它只包含住在 city = NY 的人

所以最终输出会是这样的:

{ 'cateogory1': { 'Mary': 10, 'Jane': 19, 'John': 20 }, 'cateogory2': {} }

我首先尝试使用 iterparse,但出现内存错误:

result = {}
for _, element in etree.iterparse( 'file.xml', tag = 'category' ):
    result[element.get('name')] = {}
    if element.get('name') == 'category':
        persons = {}
        for person in element.findall('peoples/people'):
            name, city, age = person.getchildren()
            if city.text == 'NY':
                persons[ name.text ] = age.text
        result[element.get('name')] = persons
    element.clear()

return results

所以我的第二次尝试是使用 SAX,但我不熟悉它。我首先从这里获取了一个脚本,但找不到将名字与一个人的城市和年龄相关联的方法:

class CategoryParser(object):
    def __init__( self, d ):
        self.d = d
    def start( self, start, attrib ):
        if tag == 'category':
            self.group = self.d[attrib['name']] = {}
        elif tag == 'people':
            #don't know how to access name, city and age for this person
    def close( self ):
        pass

result = {}
parser = lxml.etree.XMLParser( target=CategoryParser(result) )
lxml.etree.parse( 'file.xml', parser )

实现预期结果的最佳方式是什么?我愿意使用其他方法。

【问题讨论】:

  • 你能发布你得到的整个内存错误吗?你应该可以用 iterparse 做到这一点;是读取 XML 的内存错误还是因为您的 result 字典太大?

标签: python xml-parsing lxml saxparser


【解决方案1】:

您的lxml 方法看起来非常接近,但我不确定它为什么给出MemoryError。不过,您可以使用内置的 xml.etree.ElementTree 轻松完成此操作。

使用此 xml(根据您的示例稍作修改):

xml = '''<environment>
    <category name='category1'>
        <peoples>
            <people>
                <name>Mary</name>
                <city>NY</city>
                <age>10</age>
            </people>
            <people>
                <name>Jane</name>
                <city>NY</city>
                <age>19</age>
            </people>
            <people>
                <name>John</name>
                <city>NY</city>
                <age>20</age>
            </people>
            <people>
                <name>Carl</name>
                <city>DC</city>
                <age>11</age>
            </people>
        </peoples>
    </category>
    <category name='category2'>
        <peoples>
            <people>
                <name>Mike</name>
                <city>NY</city>
                <age>200</age>
            </people>
            <people>
                <name>Jimmy</name>
                <city>HW</city>
                <age>94</age>
            </people>
        </peoples>
    </category>
</environment>'''

我这样做:

import xml.etree.ElementTree as ET

root = ET.fromstring(xml)

x = dict()

# Iterate all "category" nodes
for c in root.findall('./category'):

    # Store "name" attribute
    name = c.attrib['name']

    # Insert empty dictionary for current category
    x[name] = {}

    # Iterate all people nodes contained in this category that have
    # a child "city" node matching "NY"
    for p in c.findall('./peoples/people[city="NY"]'):

        # Get text of "name" child node
        # (accessed by iterating parent node)
        # i.e. "list(p)" -> [<Element 'name' at 0x04BB2750>, <Element 'city' at 0x04BB2900>, <Element 'age' at 0x04BB2A50>])
        person_name = next(e for e in p if e.tag == 'name').text

        # Same for "age" node, and convert to int
        person_age = int(next(e for e in p if e.tag == 'age').text)

        # Add entry to current category dictionary
        x[name][person_name] = person_age

这给了我以下字典:

{'category1': {'Mary': 10, 'Jane': 19, 'John': 20}, 'category2': {'Mike': 200}}

另外,关于您的示例 xml 的一些注释(可能只是复制/粘贴工件,但以防万一):

  • 您的关闭 /peoples 节点缺少“s”
  • 您上次关闭的/category 节点缺少关闭“>”

【讨论】:

    【解决方案2】:

    由于您使用lxml 并表示可以使用其他方法,请考虑XSLT,这是一种专用语言,旨在将 XML 文档转换为包括文本文件在内的各种格式。

    具体来说,沿着你的树向下走,并按节点值构建所需的大括号和引号。因为您需要的字典可以是有效的 JSON,所以将您的 XSLT 结果导出为 .json!

    XSLT (另存为.xsl文件,特殊的.xml文件)

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output indent="yes" method="text"/>
      <xsl:strip-space elements="*"/>
    
      <xsl:variable name="pst">&apos;</xsl:variable>
    
      <xsl:template match="/environment">
          <xsl:text>{&#xa;</xsl:text>
          <xsl:apply-templates select="category"/>
          <xsl:text>&#xa;}</xsl:text>
      </xsl:template>
    
      <xsl:template match="category">
          <xsl:value-of select="concat('  ', $pst, @name, $pst, ': {')"/>
          <xsl:apply-templates select="peoples/people[city='NY']"/>
          <xsl:text>}</xsl:text>
          <xsl:if test="position() != last()">
              <xsl:text>,&#xa;</xsl:text>
          </xsl:if>
      </xsl:template>
    
      <xsl:template match="people">
          <xsl:value-of select="concat($pst, name, $pst, ': ', age)"/>
          <xsl:if test="position() != last()">
              <xsl:text>, </xsl:text>
          </xsl:if>
      </xsl:template>
    
    </xsl:stylesheet>
    

    Python (没有for 循环、if 逻辑或def 构建)

    import ast
    import lxml.etree as et
    
    # LOAD XML AND XSL
    xml = et.parse('Input.xml')
    xsl = et.parse('Script.xsl')
    
    # TRANSFORM INPUT
    transformer = et.XSLT(xsl)
    output_str = transformer(xml)
    
    # BUILD DICT LITERALLY
    new_dict = ast.literal_eval(str(output_str))
    
    print(new_dict)
    # {'category1': {'Mary': 10, 'Jane': 19, 'John': 20} }
    
    # OUTPUT JSON
    with open('Output.json', 'wb') as f:
       f.write(output_str)
    
    # {
    #   "category1": {"Mary": 10, "Jane": 19, "John": 20}
    # }
    

    Online Demo (带有扩展节点用于演示)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-10
      • 1970-01-01
      • 2023-03-18
      • 2012-04-09
      • 2011-03-25
      • 1970-01-01
      相关资源
      最近更新 更多