使用Python的xml.etree查找元素的起始和结束字符偏移量

7 投票
5 回答
7282 浏览
提问于 2025-04-17 06:13

我有一些XML数据,看起来像这样:

<xml>
The captial of <place pid="1">South Africa</place> is <place>Pretoria</place>.
</xml>

我想提取以下内容:

  1. 当前在etree中提供的XML元素。
  2. 文档中开始和结束标签之间的完整纯文本。
  3. 每个开始元素在纯文本中的位置,以字符偏移量表示。

现在最重要的是(3);etree可以很好地提供(1)。

我找不到直接实现(3)的方法,但我希望通过遍历文档树中的元素,返回很多小字符串,然后重新组合,从而提供(2)和(3)。不过,获取根节点的.text属性只返回根节点和第一个元素之间的文本,比如“首都是”。

使用SAX来实现(1)可能需要重新实现很多已经写过的东西,比如在minidom和etree中。对于这个代码要放入的包,使用lxml不是一个选项。有人能帮忙吗?

5 个回答

1

(3) 可以通过 XMLParser.CurrentByteIndex 来实现,方法如下:

import xml.etree.ElementTree as ET

class MyTreeBuilder(ET.TreeBuilder):
    def start(self, tag, attrs):
        print(parser.parser.CurrentByteIndex)
        ET.TreeBuilder.start(self, tag, attrs)

builder = MyTreeBuilder()
parser = ET.XMLParser(target=builder)
builder.parser = parser
tree = ET.parse('test.xml', parser=parser)

另外,可以参考 这个回答,它提供了一个SAX的替代方案。不过要注意,字节索引和字符索引是不一样的,在Python中可能没有简单的方法将字节索引转换为字符索引。(也可以查看 这里。)

如果想要获取字符的偏移量而不是字节的偏移量,有一个(虽然不太优雅的)解决办法,就是将字节重新编码为字符。假设实际的编码是utf8:

import xml.etree.ElementTree as ET

class MyTreeBuilder(ET.TreeBuilder):
    def start(self, tag, attrs):
        print(parser.parser.CurrentByteIndex)
        ET.TreeBuilder.start(self, tag, attrs)

builder = MyTreeBuilder()
parser = ET.XMLParser(target=builder)
builder.parser = parser
with open('test.xml', 'rb') as f:
    parser.feed(f.read().decode('latin1').encode('utf8'))
3

你需要关注一下 .tail 属性和 .text 属性:.text 是在开始标签后面直接的文本,而 .tail 是在结束标签后面直接的文本。这会帮你得到你所说的“很多小字符串”。

小提示:你可以使用 etree.iterwalk(elem)(这个和 etree.iterparse() 做的事情一样,不过是针对已经存在的树)来遍历开始标签和结束标签。想法是这样的:

for event, elem in etree.iterwalk(xml_elem, events=('start', 'end')):
    if event == 'start':
        # it's a start tag
        print 'starting element', elem.tag
        print elem.text
    elif event == 'end':
        # it's an end tag
        print 'ending element', elem.tag
        if elem is not xml_elem:
            # dont' want the text trailing xml_elem
            print elem.tail

我想你可以自己完成剩下的部分吧?

注意:.text.tail 可能会是 None,所以如果你想把它们连接起来,你需要注意这一点(比如可以用 (elem.text or ''))。

如果你对 sax 有了解(或者有现成的 sax 代码可以用),lxml 允许你 从一个元素或树中生成 sax 事件

lxml.sax.saxify(elem, handler)

在提取元素中的所有文本时,还有一些其他的东西可以关注:.itertext() 方法,xpath 表达式 .//text()(lxml 让你可以从 xpath 表达式中返回“智能字符串”:它们可以让你检查这些字符串属于哪个元素等等)。

5

iterparse() 函数可以在 xml.etree 中找到:

import xml.etree.cElementTree as etree

for event, elem in etree.iterparse(file, events=('start', 'end')):
    if event == 'start':
       print(elem.tag) # use only tag name and attributes here
    elif event == 'end':
       # elem children elements, elem.text, elem.tail are available
       if elem.text is not None and elem.tail is not None:
          print(repr(elem.tail))

另一个选择是重写 start()data()end() 方法,这些方法属于 etree.TreeBuilder()

from xml.etree.ElementTree import XMLParser, TreeBuilder

class MyTreeBuilder(TreeBuilder):

    def start(self, tag, attrs):
        print("&lt;%s>" % tag)
        return TreeBuilder.start(self, tag, attrs)

    def data(self, data):
        print(repr(data))
        TreeBuilder.data(self, data)

    def end(self, tag):
        return TreeBuilder.end(self, tag)

text = """<xml>
The captial of <place pid="1">South Africa</place> is <place>Pretoria</place>.
</xml>"""

# ElementTree.fromstring()
parser = XMLParser(target=MyTreeBuilder())
parser.feed(text)
root = parser.close() # return an ordinary Element

输出

<xml>
'\nThe captial of '
<place>
'South Africa'
' is '
<place>
'Pretoria'
'.\n'

撰写回答