Использование Python xml.etree для поиска начальных и конечных смещений символов

У меня есть данные XML, которые выглядят так:

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

Я хотел бы иметь возможность извлечь:

  1. Элементы XML, как они в настоящее время предоставляются в etree.
  2. Полный текст документа между начальным и конечным тегами.
  3. Расположение в текстовом формате каждого начального элемента в виде смещения символа.

(3) является наиболее важным требованием прямо сейчас; Этри обеспечивает (1) штраф.

Я не вижу никакого способа сделать (3) напрямую, но надеялся, что итерации по элементам в дереве документа вернут много маленьких строк, которые могут быть повторно собраны, что обеспечит (2) и (3). Однако запрос.text корневого узла возвращает только текст между корневым узлом и первым элементом, например, "Столица".

Выполнение (1) с SAX может включать в себя реализацию того, что уже было написано много раз, например, minidom и etree. Использование lxml не вариант для пакета, в который должен войти этот код. Кто-нибудь может помочь?

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'

Вам нужно посмотреть на .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'
Другие вопросы по тегам