How to output CDATA using ElementTree

Viewed 54194

I've discovered that cElementTree is about 30 times faster than xml.dom.minidom and I'm rewriting my XML encoding/decoding code. However, I need to output XML that contains CDATA sections and there doesn't seem to be a way to do that with ElementTree.

Can it be done?

15 Answers

After a bit of work, I found the answer myself. Looking at the ElementTree.py source code, I found there was special handling of XML comments and preprocessing instructions. What they do is create a factory function for the special element type that uses a special (non-string) tag value to differentiate it from regular elements.

def Comment(text=None):
    element = Element(Comment)
    element.text = text
    return element

Then in the _write function of ElementTree that actually outputs the XML, there's a special case handling for comments:

if tag is Comment:
    file.write("<!-- %s -->" % _escape_cdata(node.text, encoding))

In order to support CDATA sections, I create a factory function called CDATA, extended the ElementTree class and changed the _write function to handle the CDATA elements.

This still doesn't help if you want to parse an XML with CDATA sections and then output it again with the CDATA sections, but it at least allows you to create XMLs with CDATA sections programmatically, which is what I needed to do.

The implementation seems to work with both ElementTree and 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")

lxml has support for CDATA and API like ElementTree.

It's not possible AFAIK... which is a pity. Basically, ElementTree modules assume that the reader is 100% XML compliant, so it shouldn't matter if they output a section as CDATA or some other format that generates the equivalent text.

See this thread on the Python mailing list for more info. Basically, they recommend some kind of DOM-based XML library instead.

Actually this code has a bug, since you don't catch ]]> appearing in the data you are inserting as CDATA

as per Is there a way to escape a CDATA end token in xml?

you should break it into two CDATA's in that case, splitting the ]]> between the two.

basically data = data.replace("]]>", "]]]]><![CDATA[>")
(not necessarily correct, please verify)

You can override ElementTree _escape_cdata function:

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

Note that you may not need pass extra encoding param, depending on your library/python version.

Now you can write CDATA into obj.text like:

root = ET.Element('root')
body = ET.SubElement(root, 'body')
body.text = '<![CDATA[perform extra angle brackets escape for this text]]>'
print(ET.tostring(root))

and get clear CDATA node:

<root>
    <body>
        <![CDATA[perform extra angle brackets escape for this text]]>
    </body>
</root>

The DOM has (atleast in level 2) an interface DATASection, and an operation Document::createCDATASection. They are extension interfaces, supported only if an implementation supports the "xml" feature.

from xml.dom import minidom

my_xmldoc=minidom.parse(xmlfile)

my_xmldoc.createCDATASection(data)

now u have cadata node add it wherever u want....

for python3 and ElementTree you can use next reciept

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)

if you need xml as str, you can use

ET.tostring(tree)

or next hack (which almost same as code inside tostring())

fake_file = BytesIO()
tree.write(fake_file, encoding="utf-8", xml_declaration=True)
result_xml_text = str(fake_file.getvalue(), encoding="utf-8")

and get result

<?xml version='1.0' encoding='utf-8'?>
<my_name>
  <![CDATA[<p>some text</p>]]>
</my_name>

Simple way of making .xml file with CDATA sections

The main idea is that we covert the element tree to a string and call unescape on it. Once we have the string we use standard python to write a string to a file.

Based on: How to write unescaped string to a XML element with ElementTree?

Code that generates the XML file

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)

Output foo.xml

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