Как вывести CDATA с помощью ElementTree

Я обнаружил, что cElementTree примерно в 30 раз быстрее, чем xml.dom.minidom, и я переписываю свой код кодирования / декодирования XML. Однако мне нужно вывести XML, содержащий разделы CDATA, и, похоже, нет способа сделать это с помощью ElementTree.

Это можно сделать?

> Мне нужно вывести XML, содержащий разделы CDATA. Почему? Это кажется странным требованием.

bortzmeyer 15.10.2008 16:14

У меня есть требование - фрагменты CDATA иногда гораздо более удобочитаемы.

grifaton 07.09.2010 02:39

@bortzmeyer Это полезно для добавления HTML в KML (файлы Google Maps XML).

logic-unit 23.06.2016 14:59
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
42
3
46 677
15
Перейти к ответу Данный вопрос помечен как решенный

Ответы 15

Это невозможно, AFAIK ... а жаль. По сути, модули ElementTree предполагают, что считыватель на 100% совместим с XML, поэтому не имеет значения, выводят ли они раздел как CDATA или какой-либо другой формат, который генерирует эквивалентный текст.

См. эта ветка в списке рассылки Python для получения дополнительной информации. По сути, вместо этого они рекомендуют какую-то XML-библиотеку на основе DOM.

Я бы не назвал это "жалостью". Для информационного набора XML (содержимого) нет никакой разницы между "<! [CDATA [&]]>" и "& amp;" ... Большинство анализаторов XML даже не сообщают вам, что было в исходном документе.

bortzmeyer 15.10.2008 16:13

Это правда, но некоторые данные могут быть сброшены и проанализированы гораздо более эффективно в формате CDATA. Поэтому очень больно не иметь возможности указать библиотеке XML, чтобы она обрабатывала это таким образом.

Dan Lenski 16.10.2008 01:02

Ссылка сейчас недоступна.

Rahul K P 30.03.2016 14:26

Спасибо. Заменено ссылкой Wayback Machine.

Dan Lenski 07.05.2020 22:09
Ответ принят как подходящий

Немного поработав, я сам нашел ответ. Глядя на исходный код 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 * статичны.

Treviño 30.12.2012 19:05

Что мне делать, если я не могу использовать _write? Значит, я не могу использовать xml.elementtree? Это ужасно.

elwc 02.01.2013 10:17

Thsio reciep не будет работать для Python 2.7 или 3.2 (и 3.3) - проверьте ответ @amaury ниже. B Фактически, новый ElementTree не имеет метода "_write", который можно было бы переопределить.

jsbueno 23.01.2013 21:00

Для etree есть элемент CDATA, который можно использовать напрямую. lxml.de/api/lxml.etree.CDATA-class.html

coderek 01.10.2015 23:20

lxml поддерживает CDATA и API, например ElementTree.

Это очень важно с точки зрения принципа «не использовать собственный XML-синтаксический анализатор».

Adam Bethke 12.03.2018 19:59

@iny Я думаю, ваша ссылка lxml не работает.

Peter Moore 09.11.2019 01:22

На самом деле в этом коде есть ошибка, поскольку вы не обнаруживаете, что ]]> появляется в данных, которые вы вставляете как 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 - в отличие от исходного рецепта. Я только что придумал еще одну вещь, более сложную, чем эта.

jsbueno 23.01.2013 20:59

Это требует обновления, чтобы добавить кодировку kwarg в _serialize_xml def.

Patrick 28.09.2014 21:58

для 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")

Kevin 15.01.2015 08:53

Вот моя версия, основанная на ответах гоули и амаури, приведенных выше. Он работает как для 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. Однако есть еще один пакет под названием lxml, который (хотя и немного медленнее) имеет во многом идентичный синтаксис с xml.etree.ElementTree. lxml может как писать, так и анализировать CDATA. Документация здесь

В итоге это сработало для меня в 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, но я думаю, что рано или поздно он сломается, возможно, в следующей версии. Вздох.

4ae1e1 05.05.2015 07:33

Пожалуйста. Имейте в виду, что при использовании ElementTree.parse вы всегда будете отображать только содержимое CDATA (без тега cdata). В моем коде: 'xmlParsedWithET = ElementTree.parse ("testing.xml")'. Я понял, как, немного изменив код, используя lxml, можно сохранить наши драгоценные теги cdata. Дайте мне знать, если вас это интересует или вам подходят только стандартные библиотеки

Kamil 07.05.2015 07:59

Я писал генератор для своего блога, и мне пришлось собрать ленту Atom 1.0. Это своего рода разовая задача (если она сломается в будущем, я всегда могу использовать виртуальную среду 3.4), поэтому взлом STL для меня приемлем.

4ae1e1 07.05.2015 08:02

Спасибо! Хорошо работает для python 3.6!

egvo 03.03.2020 15:03

для 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("&", "&amp;")
        # if "<" in text:
            # text = text.replace("<", "&lt;")
        # if ">" in text:
            # text = text.replace(">", "&gt;")
        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 15.10.2019 14:34

@mzjn спасибо, отредактировал. Вы можете использовать его, как обычно, при вставке текста в объект ET. Я имею ввиду obj.text='<![CDATA[text]]>'. "contrib version" - это версия библиотеки или конкретная библиотека версии Python (точно не знаю, где разница в количестве аргументов)

Stas Chabarov 15.10.2019 21:20

Вместо того, чтобы комментировать эти строки, я бы просто добавил if text.startswith("<![CDATA[") and text.endswith("]]>"): return text в качестве первой строки. Таким образом, вы не испортите записи, не относящиеся к cdata

QuinnFreedman 17.04.2020 22:59

Простой способ создания .xml файла с разделами CDATA

Основная идея состоит в том, что мы преобразуем дерево элементов в строку и вызываем для нее unescape. Получив строку, мы используем стандартный питон для записи строки в файл.

По материалам: Как записать неэкранированную строку в элемент XML с помощью ElementTree?

Код, генерирующий XML-файл

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 '&amp;', '&lt;', and '&gt;' 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)

Вывод foo.xml

<test1><![CDATA[Wired & Forbidden]]></test1>
<test2><![CDATA[Wired & Forbidden]]></test2>

Другие вопросы по теме