Я обнаружил, что cElementTree примерно в 30 раз быстрее, чем xml.dom.minidom, и я переписываю свой код кодирования / декодирования XML. Однако мне нужно вывести XML, содержащий разделы CDATA, и, похоже, нет способа сделать это с помощью ElementTree.
Это можно сделать?
У меня есть требование - фрагменты CDATA иногда гораздо более удобочитаемы.
@bortzmeyer Это полезно для добавления HTML в KML (файлы Google Maps XML).






Это невозможно, AFAIK ... а жаль. По сути, модули ElementTree предполагают, что считыватель на 100% совместим с XML, поэтому не имеет значения, выводят ли они раздел как CDATA или какой-либо другой формат, который генерирует эквивалентный текст.
См. эта ветка в списке рассылки Python для получения дополнительной информации. По сути, вместо этого они рекомендуют какую-то XML-библиотеку на основе DOM.
Я бы не назвал это "жалостью". Для информационного набора XML (содержимого) нет никакой разницы между "<! [CDATA [&]]>" и "& amp;" ... Большинство анализаторов XML даже не сообщают вам, что было в исходном документе.
Это правда, но некоторые данные могут быть сброшены и проанализированы гораздо более эффективно в формате CDATA. Поэтому очень больно не иметь возможности указать библиотеке XML, чтобы она обрабатывала это таким образом.
Ссылка сейчас недоступна.
Спасибо. Заменено ссылкой Wayback Machine.
Немного поработав, я сам нашел ответ. Глядя на исходный код ElementTree.py, я обнаружил, что есть специальная обработка комментариев XML и инструкций предварительной обработки. Что они делают, так это создают фабричную функцию для особого типа элемента, которая использует специальное (не строковое) значение тега, чтобы отличать его от обычных элементов.
def Comment(text=None):
element = Element(Comment)
element.text = text
return element
Затем в функции _write ElementTree, которая фактически выводит XML, есть особая обработка комментариев для комментариев:
if tag is Comment:
file.write("<!-- %s -->" % _escape_cdata(node.text, encoding))
Для поддержки разделов CDATA я создал фабричную функцию CDATA, расширил класс ElementTree и изменил функцию _write для обработки элементов CDATA.
Это по-прежнему не помогает, если вы хотите проанализировать XML с разделами CDATA, а затем снова вывести его с разделами CDATA, но это, по крайней мере, позволяет вам создавать XML с разделами CDATA программно, что мне и нужно было сделать.
Кажется, что реализация работает как с ElementTree, так и с cElementTree.
import elementtree.ElementTree as etree
#~ import cElementTree as etree
def CDATA(text=None):
element = etree.Element(CDATA)
element.text = text
return element
class ElementTreeCDATA(etree.ElementTree):
def _write(self, file, node, encoding, namespaces):
if node.tag is CDATA:
text = node.text.encode(encoding)
file.write("\n<![CDATA[%s]]>\n" % text)
else:
etree.ElementTree._write(self, file, node, encoding, namespaces)
if __name__ == "__main__":
import sys
text = """
<?xml version='1.0' encoding='utf-8'?>
<text>
This is just some sample text.
</text>
"""
e = etree.Element("data")
cdata = CDATA(text)
e.append(cdata)
et = ElementTreeCDATA(e)
et.write(sys.stdout, "utf-8")
Это больше не кажется возможным, поскольку функции метода записи нет, а _serialize * статичны.
Что мне делать, если я не могу использовать _write? Значит, я не могу использовать xml.elementtree? Это ужасно.
Thsio reciep не будет работать для Python 2.7 или 3.2 (и 3.3) - проверьте ответ @amaury ниже. B Фактически, новый ElementTree не имеет метода "_write", который можно было бы переопределить.
Для etree есть элемент CDATA, который можно использовать напрямую. lxml.de/api/lxml.etree.CDATA-class.html
На самом деле в этом коде есть ошибка, поскольку вы не обнаруживаете, что ]]> появляется в данных, которые вы вставляете как CDATA.
согласно Есть ли способ избежать конечного токена CDATA в xml?
в этом случае вы должны разбить его на два CDATA, разделив ]]> между ними.
в основном data = data.replace("]]>", "]]]]><![CDATA[>")
(не обязательно правильно, проверьте, пожалуйста)
DOM имеет (по крайней мере, на уровне 2) интерфейс DATASection и операция Document :: createCDATASection. Они есть интерфейсы расширения, поддерживаются только в том случае, если реализация поддерживает Функция "xml".
из xml.dom import minidom
my_xmldoc = minidom.parse (xmlfile)
my_xmldoc.createCDATASection (данные)
теперь у вас есть узел cadata, добавьте его куда хотите ....
Вот вариант решения gooli, который работает для python 3.2:
import xml.etree.ElementTree as etree
def CDATA(text=None):
element = etree.Element('![CDATA[')
element.text = text
return element
etree._original_serialize_xml = etree._serialize_xml
def _serialize_xml(write, elem, qnames, namespaces):
if elem.tag == '![CDATA[':
write("\n<%s%s]]>\n" % (
elem.tag, elem.text))
return
return etree._original_serialize_xml(
write, elem, qnames, namespaces)
etree._serialize_xml = etree._serialize['xml'] = _serialize_xml
if __name__ == "__main__":
import sys
text = """
<?xml version='1.0' encoding='utf-8'?>
<text>
This is just some sample text.
</text>
"""
e = etree.Element("data")
cdata = CDATA(text)
e.append(cdata)
et = etree.ElementTree(e)
et.write(sys.stdout.buffer.raw, "utf-8")
Это должно работать и для Python 2.7 - в отличие от исходного рецепта. Я только что придумал еще одну вещь, более сложную, чем эта.
Это требует обновления, чтобы добавить кодировку kwarg в _serialize_xml def.
для python 2.7 добавьте аргумент кодировки в сигнатуру сериализации. изменить def _serialize_xml(write, elem, qnames, namespaces): на def _serialize_xml(write, elem, encoding, qnames, namespaces): изменить write, elem, qnames, namespaces) на write, elem, encoding, qnames, namespaces) изменить et.write(sys.stdout.buffer.raw, "utf-8") на et.write(sys.stdout, "utf-8")
Вот моя версия, основанная на ответах гоули и амаури, приведенных выше. Он работает как для ElementTree 1.2.6, так и для 1.3.0, которые используют для этого очень разные методы.
Обратите внимание, что gooli не работает с 1.3.0, которая, кажется, является текущим стандартом в Python 2.7.x.
Также обратите внимание, что эта версия также не использует используемый gooli метод CDATA ().
import xml.etree.cElementTree as ET
class ElementTreeCDATA(ET.ElementTree):
"""Subclass of ElementTree which handles CDATA blocks reasonably"""
def _write(self, file, node, encoding, namespaces):
"""This method is for ElementTree <= 1.2.6"""
if node.tag == '![CDATA[':
text = node.text.encode(encoding)
file.write("\n<![CDATA[%s]]>\n" % text)
else:
ET.ElementTree._write(self, file, node, encoding, namespaces)
def _serialize_xml(write, elem, qnames, namespaces):
"""This method is for ElementTree >= 1.3.0"""
if elem.tag == '![CDATA[':
write("\n<![CDATA[%s]]>\n" % elem.text)
else:
ET._serialize_xml(write, elem, qnames, namespaces)
Я пришел сюда в поисках способа «проанализировать XML с разделами CDATA и затем снова вывести его с разделами CDATA».
Мне удалось это сделать (возможно, lxml был обновлен после этого поста?) Со следующим: (это немного грубо - извините ;-). У кого-то может быть лучший способ программно найти разделы CDATA, но я был слишком ленив.
parser = etree.XMLParser(encoding='utf-8') # my original xml was utf-8 and that was a lot of the problem
tree = etree.parse(ppath, parser)
for cdat in tree.findall('./ProjectXMPMetadata'): # the tag where my CDATA lives
cdat.text = etree.CDATA(cdat.text)
# other stuff here
tree.write(opath, encoding = "UTF-8",)
В итоге это сработало для меня в Python 2.7. Подобно ответу Амори.
import xml.etree.ElementTree as ET
ET._original_serialize_xml = ET._serialize_xml
def _serialize_xml(write, elem, encoding, qnames, namespaces):
if elem.tag == '![CDATA[':
write("<%s%s]]>%s" % (elem.tag, elem.text, elem.tail))
return
return ET._original_serialize_xml(
write, elem, encoding, qnames, namespaces)
ET._serialize_xml = ET._serialize['xml'] = _serialize_xml
Я обнаружил способ заставить CDATA работать с помощью комментариев:
node.append(etree.Comment(' --><![CDATA[' + data.replace(']]>', ']]]]><![CDATA[>') + ']]><!-- '))
Решение:
import xml.etree.ElementTree as ElementTree
def CDATA(text=None):
element = ElementTree.Element('![CDATA[')
element.text = text
return element
ElementTree._original_serialize_xml = ElementTree._serialize_xml
def _serialize_xml(write, elem, qnames, namespaces,short_empty_elements, **kwargs):
if elem.tag == '![CDATA[':
write("\n<{}{}]]>\n".format(elem.tag, elem.text))
if elem.tail:
write(_escape_cdata(elem.tail))
else:
return ElementTree._original_serialize_xml(write, elem, qnames, namespaces,short_empty_elements, **kwargs)
ElementTree._serialize_xml = ElementTree._serialize['xml'] = _serialize_xml
if __name__ == "__main__":
import sys
text = """
<?xml version='1.0' encoding='utf-8'?>
<text>
This is just some sample text.
</text>
"""
e = ElementTree.Element("data")
cdata = CDATA(text)
root.append(cdata)
Фон:
Я не знаю, работали ли предыдущие версии предлагаемого кода очень хорошо и был ли обновлен модуль ElementTree, но я столкнулся с проблемами при использовании этого трюка:
etree._original_serialize_xml = etree._serialize_xml
def _serialize_xml(write, elem, qnames, namespaces):
if elem.tag == '![CDATA[':
write("\n<%s%s]]>\n" % (
elem.tag, elem.text))
return
return etree._original_serialize_xml(
write, elem, qnames, namespaces)
etree._serialize_xml = etree._serialize['xml'] = _serialize_xml
Проблема с этим подходом заключается в том, что после передачи этого исключения сериализатор снова обрабатывает его как обычный тег. Я получал что-то вроде:
<textContent>
<![CDATA[this was the code I wanted to put inside of CDATA]]>
<![CDATA[>this was the code I wanted to put inside of CDATA</![CDATA[>
</textContent>
И, конечно, мы знаем, что это вызовет лишь множество ошибок. Но почему это происходило?
Ответ в этом маленьком парне:
return etree._original_serialize_xml(write, elem, qnames, namespaces)
Мы не хотим еще раз проверять код с помощью исходной функции сериализации, если мы перехватили наш CDATA и успешно передали его. Следовательно, в блоке «if» мы должны возвращать исходную функцию сериализации только тогда, когда там не было CDATA. Перед возвратом исходной функции нам не хватало «else».
Более того, в моей версии модуля ElementTree функция сериализации отчаянно запрашивала аргумент «short_empty_element». Итак, самая последняя версия, которую я бы порекомендовал, выглядит так (также с «хвостом»):
from xml.etree import ElementTree
from xml import etree
#in order to test it you have to create testing.xml file in the folder with the script
xmlParsedWithET = ElementTree.parse("testing.xml")
root = xmlParsedWithET.getroot()
def CDATA(text=None):
element = ElementTree.Element('![CDATA[')
element.text = text
return element
ElementTree._original_serialize_xml = ElementTree._serialize_xml
def _serialize_xml(write, elem, qnames, namespaces,short_empty_elements, **kwargs):
if elem.tag == '![CDATA[':
write("\n<{}{}]]>\n".format(elem.tag, elem.text))
if elem.tail:
write(_escape_cdata(elem.tail))
else:
return ElementTree._original_serialize_xml(write, elem, qnames, namespaces,short_empty_elements, **kwargs)
ElementTree._serialize_xml = ElementTree._serialize['xml'] = _serialize_xml
text = """
<?xml version='1.0' encoding='utf-8'?>
<text>
This is just some sample text.
</text>
"""
e = ElementTree.Element("data")
cdata = CDATA(text)
root.append(cdata)
#tests
print(root)
print(root.getchildren()[0])
print(root.getchildren()[0].text + "\n\nyay!")
Я получил следующий результат:
<Element 'Database' at 0x10062e228>
<Element '![CDATA[' at 0x1021cc9a8>
<?xml version='1.0' encoding='utf-8'?>
<text>
This is just some sample text.
</text>
yay!
Желаю вам такого же результата!
Спасибо! Ваше решение отлично работает для меня в Python 3.4.3, и действительно интересно, что вы опубликовали его только вчера, а оно мне нужно сегодня. Не тестировал в 3.5, но я думаю, что рано или поздно он сломается, возможно, в следующей версии. Вздох.
Пожалуйста. Имейте в виду, что при использовании ElementTree.parse вы всегда будете отображать только содержимое CDATA (без тега cdata). В моем коде: 'xmlParsedWithET = ElementTree.parse ("testing.xml")'. Я понял, как, немного изменив код, используя lxml, можно сохранить наши драгоценные теги cdata. Дайте мне знать, если вас это интересует или вам подходят только стандартные библиотеки
Я писал генератор для своего блога, и мне пришлось собрать ленту Atom 1.0. Это своего рода разовая задача (если она сломается в будущем, я всегда могу использовать виртуальную среду 3.4), поэтому взлом STL для меня приемлем.
Спасибо! Хорошо работает для python 3.6!
для python3 и ElementTree вы можете использовать следующий рецепт
import xml.etree.ElementTree as ET
ET._original_serialize_xml = ET._serialize_xml
def serialize_xml_with_CDATA(write, elem, qnames, namespaces, short_empty_elements, **kwargs):
if elem.tag == 'CDATA':
write("<![CDATA[{}]]>".format(elem.text))
return
return ET._original_serialize_xml(write, elem, qnames, namespaces, short_empty_elements, **kwargs)
ET._serialize_xml = ET._serialize['xml'] = serialize_xml_with_CDATA
def CDATA(text):
element = ET.Element("CDATA")
element.text = text
return element
my_xml = ET.Element("my_name")
my_xml.append(CDATA("<p>some text</p>")
tree = ElementTree(my_xml)
если вам нужен xml как str, вы можете использовать
ET.tostring(tree)
или следующий взлом (который почти такой же, как код внутри tostring())
fake_file = BytesIO()
tree.write(fake_file, encoding = "utf-8", xml_declaration=True)
result_xml_text = str(fake_file.getvalue(), encoding = "utf-8")
и получить результат
<?xml version='1.0' encoding='utf-8'?>
<my_name>
<![CDATA[<p>some text</p>]]>
</my_name>
Вы можете переопределить функцию ElementTree _escape_cdata:
import xml.etree.ElementTree as ET
def _escape_cdata(text, encoding):
try:
if "&" in text:
text = text.replace("&", "&")
# if "<" in text:
# text = text.replace("<", "<")
# if ">" in text:
# text = text.replace(">", ">")
return text
except TypeError:
raise TypeError(
"cannot serialize %r (type %s)" % (text, type(text).__name__)
)
ET._escape_cdata = _escape_cdata
Обратите внимание, что вам может не понадобиться передавать дополнительный параметр encoding, в зависимости от версии вашей библиотеки / python.
Теперь вы можете записать CDATA в obj.text, например:
root = ET.Element('root')
body = ET.SubElement(root, 'body')
body.text = '<![CDATA[perform extra angle brackets escape for this text]]>'
print(ET.tostring(root))
и получим чистый узел CDATA:
<root>
<body>
<![CDATA[perform extra angle brackets escape for this text]]>
</body>
</root>
Как именно использовать это для вывода разделов CDATA? Что такое "дополнительная версия"?
@mzjn спасибо, отредактировал. Вы можете использовать его, как обычно, при вставке текста в объект ET. Я имею ввиду obj.text='<![CDATA[text]]>'. "contrib version" - это версия библиотеки или конкретная библиотека версии Python (точно не знаю, где разница в количестве аргументов)
Вместо того, чтобы комментировать эти строки, я бы просто добавил if text.startswith("<![CDATA[") and text.endswith("]]>"): return text в качестве первой строки. Таким образом, вы не испортите записи, не относящиеся к cdata
Основная идея состоит в том, что мы преобразуем дерево элементов в строку и вызываем для нее unescape. Получив строку, мы используем стандартный питон для записи строки в файл.
По материалам: Как записать неэкранированную строку в элемент XML с помощью ElementTree?
import xml.etree.ElementTree as ET
from xml.sax.saxutils import unescape
# defining the tree structure
element1 = ET.Element('test1')
element1.text = '<![CDATA[Wired & Forbidden]]>'
# & and <> are in a weird format
string1 = ET.tostring(element1).decode()
print(string1)
# now they are not weird anymore
# more formally, we unescape '&', '<', and '>' in a string of data
# from https://docs.python.org/3.8/library/xml.sax.utils.html#xml.sax.saxutils.unescape
string1 = unescape(string1)
print(string1)
element2 = ET.Element('test2')
element2.text = '<![CDATA[Wired & Forbidden]]>'
string2 = unescape(ET.tostring(element2).decode())
print(string2)
# make the xml file and open in append mode
with open('foo.xml', 'a') as f:
f.write(string1 + '\n')
f.write(string2)
<test1><![CDATA[Wired & Forbidden]]></test1>
<test2><![CDATA[Wired & Forbidden]]></test2>
> Мне нужно вывести XML, содержащий разделы CDATA. Почему? Это кажется странным требованием.