Использование Python xml.etree для поиска начальных и конечных смещений символов
У меня есть данные XML, которые выглядят так:
<xml>
The captial of <place pid="1">South Africa</place> is <place>Pretoria</place>.
</xml>
Я хотел бы иметь возможность извлечь:
- Элементы XML, как они в настоящее время предоставляются в etree.
- Полный текст документа между начальным и конечным тегами.
- Расположение в текстовом формате каждого начального элемента в виде смещения символа.
(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("<%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'