【问题标题】:How do I get the whole content between two xml tags in Python?如何获取 Python 中两个 xml 标签之间的全部内容?
【发布时间】:2012-06-22 18:10:33
【问题描述】:

我尝试在打开的 xml 标记和它的关闭对应项之间获取全部内容。

在像下面的title 这样的直接案例中获取内容很容易,但是如果使用 mixed-content 并且我如何在标签之间获取 整个内容想要保留内部标签?

<?xml version="1.0" encoding="UTF-8"?>
<review>
  <title>Some testing stuff</title>
  <text sometimes="attribute">Some text with <extradata>data</extradata> in it.
  It spans <sometag>multiple lines: <tag>one</tag>, <tag>two</tag> 
  or more</sometag>.</text>
</review>

我想要的是两个text标签之间的内容,包括任何标签:Some text with &lt;extradata&gt;data&lt;/extradata&gt; in it. It spans &lt;sometag&gt;multiple lines: &lt;tag&gt;one&lt;/tag&gt;, &lt;tag&gt;two&lt;/tag&gt; or more&lt;/sometag&gt;.

现在我使用正则表达式,但它有点乱,我不喜欢这种方法。我倾向于基于 XML 解析器的解决方案。我查看了minidom、etree、lxml 和BeautifulSoup,但找不到这种情况的解决方案(整个内容,包括内部标签)。

【问题讨论】:

    标签: python xml xml-parsing lxml


    【解决方案1】:

    以下内容对我和您的示例都有效:

    from lxml import etree
    doc = etree.XML(
    """<?xml version="1.0" encoding="UTF-8"?>
    <review>
      <title>Some testing stuff</title>
      <text>Some text with <extradata>data</extradata> in it.</text>
    </review>"""
    )
    
    def flatten(seq):
      r = []
      for item in seq:
        if isinstance(item,(str,unicode)):
          r.append(unicode(item))
        elif isinstance(item,(etree._Element,)):
          r.append(etree.tostring(item,with_tail=False))
      return u"".join(r)
    
    print flatten(doc.xpath('/review/text/node()'))
    

    产量:

    Some text with <extradata>data</extradata> in it.
    

    xpath 选择 &lt;text&gt; 元素的所有子节点,如果它们是字符串/unicode 子类 (&lt;class 'lxml.etree._ElementStringResult'&gt;),则直接将它们呈现为 unicode,或者如果它是 Element,则在其上调用 etree.tostring,@ 987654327@避免重复尾部。

    如果存在其他节点类型,您可能需要处理它们。

    【讨论】:

    • 这可以写得更紧凑。拿这个单线:''.join(el if isinstance(el, str) else lxml.etree.tostring(el, with_tail=False) for el in doc.xpath('/review/text/node()'))
    • 你可以随意使用tostring。
    • @Marcin:当我尝试它时,tostring 抱怨它无法序列化 _ElementStringResult
    【解决方案2】:
    from lxml import etree
    t = etree.XML(
    """<?xml version="1.0" encoding="UTF-8"?>
    <review>
      <title>Some testing stuff</title>
      <text>Some text with <extradata>data</extradata> in it.</text>
    </review>"""
    )
    (t.text + ''.join(map(etree.tostring, t))).strip()
    

    这里的诀窍是t 是可迭代的,并且在迭代时会产生所有子节点。因为etree避免了文本节点,所以还需要恢复第一个子标签之前的文本,用t.text。

    In [50]: (t.text + ''.join(map(etree.tostring, t))).strip()
    Out[50]: '<title>Some testing stuff</title>\n  <text>Some text with <extradata>data</extradata> in it.</text>'
    

    或者:

    In [6]: e = t.xpath('//text')[0]
    
    In [7]: (e.text + ''.join(map(etree.tostring, e))).strip()
    Out[7]: 'Some text with <extradata>data</extradata> in it.'
    

    【讨论】:

    • OP 想要获取特定元素的内容。在这种情况下,您的解决方案不起作用,至少不能直接起作用。我得到一个带有e = t.xpath('//text')[0] 的元素并尝试了它(''.join(map(etree.tostring, e))),但结果是'&lt;extradata&gt;data&lt;/extradata&gt; in it.'。
    • 需要测试更多案例,但你的最后一个例子对我来说很好(到目前为止)。当使用find 而不是xpath 时,它也适用于标准etree。
    【解决方案3】:

    使用lxml* 非常容易,使用parse() 和tostring() 函数:

    from  lxml.etree import parse, tostring
    

    首先你解析文档并获取你的元素(我使用的是 XPath,但你可以使用任何你想要的东西):

    doc = parse('test.xml')
    element = doc.xpath('//text')[0]
    

    tostring() 函数返回元素的文本表示:

    >>> tostring(element)
    '<text>Some <text>text</text> with <extradata>data</extradata> in it.</text>\n'
    

    但是,您不需要外部元素,因此我们可以通过简单的str.replace() 调用来删除它们:

    >>> tostring(element).replace('<%s>'%element.tag, '', 1)
    'Some <text>text</text> with <extradata>data</extradata> in it.</text>\n'
    

    请注意,str.replace() 收到 1 作为第三个参数,因此它将仅删除第一次出现的开始标记。也可以使用结束标签来做到这一点。现在,我们通过 -1 代替 1,而不是 1:

    >>> tostring(element).replace('</%s>'%element.tag, '', -1)
    '<text>Some <text>text with <extradata>data</extradata> in it.\n'
    

    当然,解决方案是一次完成所有事情:

    >>> tostring(element).replace('<%s>'%element.tag, '', 1).replace('</%s>'%element.tag, '', -1)
    'Some <text>text with <extradata>data</extradata> in it.\n'
    

    编辑:@Charles 提出了一个很好的观点:这段代码很脆弱,因为标签可以有属性。一个可能但仍然有限的解决方案是在第一个 &gt; 处拆分字符串:

    >>> tostring(element).split('>', 1)
    ['<text',
     'Some <text>text</text> with <extradata>data</extradata> in it.</text>\n']
    

    得到第二个结果字符串:

    >>> tostring(element).split('>', 1)[1]
    'Some <text>text</text> with <extradata>data</extradata> in it.</text>\n'
    

    然后拆分它:

    >>> tostring(element).split('>', 1)[1].rsplit('</', 1)
    ['Some <text>text</text> with <extradata>data</extradata> in it.', 'text>\n']
    

    终于得到了第一个结果:

    >>> tostring(element).split('>', 1)[1].rsplit('</', 1)[0]
    'Some <text>text</text> with <extradata>data</extradata> in it.'
    

    尽管如此,这段代码仍然很脆弱,因为&gt; 在 XML 中是一个完全有效的字符,即使在属性内部也是如此。

    无论如何,我必须承认MattH solution 是真正的通用解决方案。

    * 实际上,此解决方案也适用于 ElementTree,如果您不想依赖 lxml,这很好。唯一的区别是您将无法使用 XPath。

    【讨论】:

    • 文本替换在这里增加了很多脆弱性。如果您的输入文件恰好有属性?命名空间前缀?
    • 我觉得用这种方法我不会比纯正则表达式获得太多。由于开始标签至少有一个属性,所以它也很不稳定。
    • replace('&lt;/%s&gt;'%element.tag, '', -1) 应该可以工作,但我不能使用.replace('&lt;%s&gt;'%element.tag, '', 1),因为有一个或多个属性,所以我必须再次使用正则表达式(或content[content.index('&gt;'):] 周围的东西)等等。
    【解决方案4】:

    我喜欢上面@Marcin 的解决方案,但是我发现当使用他的第二个选项(转换子节点,而不是树的根)时,它不能处理实体。

    他上面的代码(修改为添加实体):

    from lxml import etree
    t = etree.XML("""<?xml version="1.0" encoding="UTF-8"?>
    <review>
      <title>Some testing stuff</title>
        <text>this &amp; that.</text>
    </review>""")
    e = t.xpath('//text')[0]
    print (e.text + ''.join(map(etree.tostring, e))).strip()
    

    返回:

    this & that.
    

    使用裸/未转义的“&”字符而不是正确的实体 ('&')。

    我的解决方案是在节点级别(而不是在所有子节点上)调用 etree.tostring,然后使用正则表达式去除开始和结束标记:

    import re
    from lxml import etree
    t = etree.XML("""<?xml version="1.0" encoding="UTF-8"?>
    <review>
      <title>Some testing stuff</title>
        <text>this &amp; that.</text>
    </review>""")
    
    e = t.xpath('//text')[0]
    xml = etree.tostring(e)
    inner = re.match('<[^>]*?>(.*)</[^>]*>\s*$', xml, flags=re.DOTALL).group(1)
    print inner
    

    产生:

    this &amp; that.
    

    我使用 re.DOTALL 来确保这适用于包含换行符的 XML。

    【讨论】:

      【解决方案5】:

      刚刚找到解决方案,很简单:

      In [31]: t = x.find('text')
      
      In [32]: t
      Out[32]: <Element text at 0xa87ed74>
      
      In [33]: list(t.itertext())
      Out[33]: ['Some text with ', 'data', ' in it.']
      
      In [34]: ''.join(_)
      Out[34]: 'Some text with data in it.'
      

      itertext 绝对是通往这里的路!

      编辑://对不起,我以为你只想要孩子之间的文字,我不好

      【讨论】:

      • 我可以用x.find('text').get_text() 得到同样的结果——我想。 但是这种方法排除了内部标签,我需要它们。
      • 这实际上并不能以任何方式解决 OP 问题。 需要维护内部标签。
      • 它确实维护了内部标签,只是不超过一个级别,请参阅我的编辑,itertext 得到一切
      • 遍历所有子元素,而不仅仅是文本。
      猜你喜欢
      • 2016-04-06
      • 2015-07-07
      • 1970-01-01
      • 2012-08-15
      • 1970-01-01
      • 2016-12-28
      • 1970-01-01
      • 2011-11-09
      • 1970-01-01
      相关资源
      最近更新 更多