Как получить весь контент между двумя тегами xml в Python?
Я пытаюсь получить весь контент между открывающим тегом 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
, включая теги: Some text with <extradata>data</extradata> in it. It spans <sometag>multiple lines: <tag>one</tag>, <tag>two</tag> or more</sometag>.
В настоящее время я использую регулярные выражения, но это становится бесполезным, и мне не нравится этот подход. Я склоняюсь к решению на основе парсер XML. Я просмотрел minidom
, etree
, lxml
и BeautifulSoup
, но не смог найти решение для этого случая (целое содержимое, включая внутренние теги).
Ответы
Ответ 1
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.'
Ответ 2
Здесь что-то работает для меня и вашего образца:
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 напрямую, если они являются подклассом string/unicode (<class 'lxml.etree._ElementStringResult'>
), либо вызывает на нем etree.tostring
, если он Element
, with_tail=False
избегает дублирования хвоста.
Возможно, вам придется обрабатывать другие типы node, если они присутствуют.
Ответ 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 мы передаем -1 для замены:
>>> 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'
EDIT: @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'
затем rsplitting:
>>> 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.'
Тем не менее, этот код по-прежнему является хрупким, поскольку >
является вполне допустимым char в XML, даже внутри атрибутов.
В любом случае, я должен признать, что решение MattH является реальным общим решением.
* На самом деле это решение работает с ElementTree, что отлично, если вы не хотите зависеть от lxml. Единственное различие заключается в том, что вы не сможете использовать XPath.
Ответ 4
Мне нравится решение @Marcin выше, однако я обнаружил, что при использовании его второго варианта (преобразование под node, а не корень дерева) он не обрабатывает сущности.
Его код сверху (изменен для добавления объекта):
from lxml import etree
t = etree.XML("""<?xml version="1.0" encoding="UTF-8"?>
<review>
<title>Some testing stuff</title>
<text>this & that.</text>
</review>""")
e = t.xpath('//text')[0]
print (e.text + ''.join(map(etree.tostring, e))).strip()
возвращает:
this & that.
с голой/неэкранированной '&' вместо правильной сущности ('&').
Мое решение состояло в том, чтобы использовать для вызова etree.tostring на уровне node (а не для всех дочерних элементов), затем отмените начальный и конечный теги с помощью регулярного выражения:
import re
from lxml import etree
t = etree.XML("""<?xml version="1.0" encoding="UTF-8"?>
<review>
<title>Some testing stuff</title>
<text>this & 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 & that.
Я использовал re.DOTALL, чтобы гарантировать, что это работает для XML, содержащего строки новой строки.
Ответ 5
Просто нашел решение, довольно легко:
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
- это, безусловно, путь сюда!
Изменить://Извините, я думал, что вам нужен только текст между детьми, мой плохой