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



我有如下XML数据:

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

我希望能够提取:

  1. 当前在树中提供的XML元素。
  2. 文档的完整纯文本,介于开始和结束标记之间。
  3. 每个开始元素的纯文本中的位置,作为字符偏移量。

(3)是目前最重要的要求;树提供(1)fine。

我无法直接执行(3),但希望通过迭代文档树中的元素将返回许多可以重新组装的小字符串,从而提供(2)和(3)。然而,请求根节点的。text只返回根节点和第一个元素之间的文本,例如:的首都。

用SAX实现(1)可能涉及实现很多已经写过很多次的东西,例如minidom和treree。对于要放入这段代码的包来说,使用lxml不是一个选项。有人能帮忙吗?

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))

另一个选项是覆盖etree.TreeBuilder()start(), data(), end()方法:

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'

您需要查看.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表达式返回"智能字符串":它们允许您检查它们属于哪个元素等…)。

(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'))

(2)很容易使用SAX,参见下面的代码片段

from xml.sax.handler import ContentHandler
import xml.sax
import sys
class textHandler(ContentHandler):
    def characters(self, ch):
        sys.stdout.write(ch.encode("Latin-1"))
parser = xml.sax.make_parser()
handler = textHandler()
parser.setContentHandler(handler)
parser.parse("test.xml")

或本书中的例1-1:bookhandler.py http://oreilly.com/catalog/pythonxml/chapter/ch01.html

(3)是棘手的,咨询这个线程,它是Java,但应该有类似的东西在Python SAX api我如何得到一个xml标记的正确的开始/结束位置与SAX?

您可以使用Pawpaw:

轻松完成所有这些操作。

代码:

import sys
sys.modules['_elementtree'] = None
import xml.etree.ElementTree as ET
from pawpaw import Ito, visualization, xml
text = """<xml>
The captial of <place pid="1">South Africa</place> is <place>Pretoria</place>.
</xml>"""
root = ET.fromstring(text, parser=xml.XmlParser())
print('1. ET elements:n')
print(elements := root.findall('.//'))
print()
print('2. Full plain text of document between start and end tags:n')
start_tag = root.ito.find('*[d:start_tag]')
end_tag = root.ito.find('*[d:end_tag]')
ito = Ito(text, start_tag.stop, end_tag.start)
print(f'{ito:%substr!r}')
print()
print('3. Character offsets of plain text of each element:n')
for e in elements:
    plain_text = e.ito.find('*[d:text]')
    print(f'{plain_text:%span: "%substr"}')
print()
输出:

1. ET elements:
[<Element 'place' at 0x1b0ffx203a0>, <Element 'place' at 0x1b0ffx21240>]
2. Full plain text of document between start and end tags:
'nThe captial of <place pid="1">South Africa</place> is <place>Pretoria</place>.n'
3. Character offsets of plain text of each element:
(36, 48) "South Africa"
(67, 75) "Pretoria"

好处:使用Pawpaw,您可以获得任何 xml段的字符偏移量,例如:

  • 元素
  • 性>名称空间
  • 标记
  • 等。

例子:

v_tree = visualization.pepo.Tree()
print(v_tree.dumps(root.ito))
输出:

(0, 91) 'element' : '<xml>nThe captial o…ia</place>.n</xml>'
├──(0, 5) 'start_tag' : '<xml>'
│  └──(1, 4) 'tag' : 'xml'
│     └──(1, 4) 'name' : 'xml'
├──(5, 21) 'text' : 'nThe captial of '
├──(21, 56) 'element' : '<place pid="1">South Africa</place>'
│  ├──(21, 36) 'start_tag' : '<place pid="1">'
│  │  ├──(22, 27) 'tag' : 'place'
│  │  │  └──(22, 27) 'name' : 'place'
│  │  └──(28, 35) 'attributes' : 'pid="1"'
│  │     └──(28, 31) 'attribute' : 'pid="1"'
│  │        ├──(28, 31) 'tag' : 'pid'
│  │        │  └──(28, 31) 'name' : 'pid'
│  │        └──(33, 34) 'value' : '1'
│  ├──(36, 48) 'text' : 'South Africa'
│  └──(48, 56) 'end_tag' : '</place>'
│     └──(50, 55) 'tag' : 'place'
│        └──(50, 55) 'name' : 'place'
├──(56, 60) 'text': ' is '
├──(60, 83) 'element' : '<place>Pretoria</place>'
│  ├──(60, 67) 'start_tag' : '<place>'
│  │  └──(61, 66) 'tag' : 'place'
│  │     └──(61, 66) 'name' : 'place'
│  ├──(67, 75) 'text' : 'Pretoria'
│  └──(75, 83) 'end_tag' : '</place>'
│     └──(77, 82) 'tag' : 'place'
│        └──(77, 82) 'name' : 'place'
├──(83, 85) 'text': '.n'
└──(85, 91) 'end_tag' : '</xml>'
   └──(87, 90) 'tag' : 'xml'
      └──(87, 90) 'name' : 'xml'

最新更新