如何在Python中获取两个XML标记之间的全部内容?

15
我试图获取一个开放的XML标签和它的对应闭合标签之间的全部内容。在简单的情况下,例如以下标题title,获取内容很容易,但如果使用了混合内容并且我想要保留内部标签,那么如何获取标签之间的全部内容呢?
<?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标签之间的内容,包括任何标签:其中有一些包含<extradata>data</extradata>的文本。它跨越<sometag>多行:<tag>one</tag>,<tag>two</tag>或更多</sometag>

目前我使用正则表达式,但这种方式变得有点混乱,而且我不喜欢这种方法。我倾向于基于XML解析器的解决方案。我查看了minidometreelxmlBeautifulSoup,但没有找到针对此情况(包括内部标签的全部内容)的解决方案。

5个回答

7
这是对我和你的样本都有效的东西:
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选择 <text> 元素的所有子节点,如果它们是字符串/unicode子类 (<class 'lxml.etree._ElementStringResult'>) 直接将它们呈现为Unicode;如果它们是元素,则调用 etree.tostring 进行转换,with_tail=False 避免了尾部重复。
如果存在其他节点类型,则可能需要处理它们。

1
这段代码可以更加简洁。看看这个一行代码:''.join(el if isinstance(el, str) else lxml.etree.tostring(el, with_tail=False) for el in doc.xpath('/review/text/node()')) - Charles Duffy
你可能可以毫不区分地使用 tostring - Marcin
1
@Marcin:当我尝试时,tostring 抱怨无法序列化 _ElementStringResult - MattH

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作为参数传递给replace函数:
>>> 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提出了一个很好的观点:由于标记可能带有属性,因此此代码是脆弱的。一种可能但仍然有限的解决方案是在第一个>处拆分字符串:

>>> 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.'

然而,这段代码仍然很脆弱,因为>在XML中是一个完全有效的字符,即使在属性中也是如此。

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

* 实际上,这个解决方案也适用于ElementTree,这对于不想依赖lxml的人来说非常好。唯一的区别是你将无法使用XPath。


1
文本替换在这里增加了相当多的脆弱性。如果您的输入文件恰好具有属性?命名空间前缀? - Charles Duffy
我有这样的感觉,用这种方法不会比纯正则表达式更有效。因为开标签至少有一个属性,所以它也变得不稳定了。 - Brutus
replace('</%s>'%element.tag, '', -1) 应该可以工作,但我不能使用 .replace('<%s>'%element.tag, '', 1),因为有一个或多个属性,所以我必须再次使用正则表达式(或类似于 content[content.index('>'):] 的东西)。 - Brutus

3
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想要获取特定元素的内容。你的解决方案在这种情况下不起作用,至少不能直接使用。我使用t.xpath('//text')[0]获取了一个元素并尝试了''.join(map(etree.tostring, e)),但结果是它包含了'<extradata>data</extradata>' - brandizzi
需要在更多情况下进行测试,但是你的最后一个示例对我来说很好(到目前为止)。当使用find而不是xpath时,它也可以与标准的etree一起使用。 - Brutus

1

我喜欢@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.

使用裸露/未转义的“&”字符而不是正确的实体(“&amp;”)。
我的解决方案是在节点级别调用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。

-2

刚刚找到了解决方案,非常简单:

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在这里绝对是正确的选择!

编辑:// 对不起,我以为你只想要子元素之间的文本,我的错。


1
我认为我可以使用 x.find('text').get_text() 来获得相同的结果。但是,这种方法会排除内部标签,而我需要它们。 - Brutus
1
这并没有解决任何问题,实际上。必须保留内部标签。 - brandizzi
它确实保留了内部标签,但仅限于一个级别,参见我的编辑,itertext获取所有内容。 - dav1d
迭代遍历所有子元素,而不仅仅是文本。 - Marcin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接