web-dev-qa-db-fra.com

Comment sortir CDATA en utilisant ElementTree

J'ai découvert que cElementTree est environ 30 fois plus rapide que xml.dom.minidom et je suis en train de réécrire mon code d'encodage/décodage XML. Cependant, je dois sortir du XML contenant des sections CDATA et il ne semble pas y avoir de moyen de le faire avec ElementTree.

Cela peut-il être fait?

36
gooli

Après un peu de travail, j'ai trouvé la réponse moi-même. En regardant le code source ElementTree.py, j'ai trouvé qu'il y avait un traitement spécial des commentaires XML et des instructions de prétraitement. Ce qu'ils font est de créer une fonction d'usine pour le type d'élément spécial qui utilise une valeur de balise spéciale (non chaîne) pour la différencier des éléments normaux.

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

Ensuite, dans la fonction _write de ElementTree qui génère le XML, il existe une gestion de casse spéciale pour les commentaires:

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

Afin de prendre en charge les sections CDATA, j'ai créé une fonction d'usine nommée CDATA, étendu la classe ElementTree et modifié la fonction _write afin de gérer les éléments CDATA.

Cela n'aide toujours pas si vous voulez analyser un XML avec des sections CDATA puis le ré-exporter avec les sections CDATA, mais vous permet au moins de créer des XML avec des sections CDATA par programme, ce que je devais faire.

L'implémentation semble fonctionner à la fois avec ElementTree et 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 __== "__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")
24
gooli

lxml supporte CDATA et une API comme ElementTree.

16
iny

Voici une variante de la solution de Gooli qui fonctionne pour 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 __== "__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")
10
Amaury

En réalité, ce code a un bogue, car vous ne voyez pas ]]> apparaître dans les données que vous insérez en tant que CDATA

selon Y at-il un moyen d'échapper à un jeton de fin CDATA en XML?

dans ce cas, vous devriez le diviser en deux CDATA, en séparant le ]]> entre les deux.

fondamentalement data = data.replace("]]>", "]]]]><![CDATA[>")
(pas nécessairement correct, veuillez vérifier)

6
Andraz Tori

Ce n'est pas possible, autant que je sache ... ce qui est dommage. Fondamentalement, les modules ElementTree supposent que le lecteur est conforme à 100% à XML. Par conséquent, peu importe s'ils produisent une section au format CDATA ou à un autre format générant le texte équivalent.

Voir ce sujet sur la liste de diffusion Python pour plus d’informations. En gros, ils recommandent plutôt une sorte de bibliothèque XML basée sur DOM.

6
Dan Lenski

Je ne sais pas si les versions précédentes du code proposé fonctionnaient très bien et si le module ElementTree a été mis à jour, mais j'ai rencontré des problèmes pour utiliser cette astuce:

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

Le problème avec cette approche est qu’après avoir passé cette exception, le sérialiseur la traite à nouveau comme une balise normale. Je recevais quelque chose comme:

<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>

Et bien sûr, nous savons que cela ne causera que beaucoup d’erreurs. Pourquoi cela se passait-il?

La réponse est dans ce petit gars:

return etree._original_serialize_xml(write, elem, qnames, namespaces)

Nous ne voulons pas examiner le code une nouvelle fois via la fonction de sérialisation d'origine si nous avons piégé notre CDATA et que nous l'avons passé avec succès à travers . Par conséquent, dans le bloc "if", nous ne devons restituer la fonction de sérialisation d'origine que lorsque CDATA n'était pas là. Nous manquions "else" avant de retourner la fonction originale.

De plus, dans ma version du module ElementTree, la fonction serialize demandait désespérément l'argument "short_empty_element". Donc, la version la plus récente que je recommanderais ressemble à ceci (aussi avec "queue"):

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!")

La sortie que j'ai eu était:

<Element 'Database' at 0x10062e228>
<Element '![CDATA[' at 0x1021cc9a8>

<?xml version='1.0' encoding='utf-8'?>
<text>
This is just some sample text.
</text>


yay!

Je vous souhaite le même résultat!

5
Kamil

Cela a fini par travailler pour moi dans Python 2.7. Semblable à la réponse d'Amaury.

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
4
zlalanne

J'ai découvert un moyen de faire fonctionner CDATA en utilisant les commentaires:

node.append(etree.Comment(' --><![CDATA[' + data.replace(']]>', ']]]]><![CDATA[>') + ']]><!-- '))
1
user3155571

Le DOM a (au moins au niveau 2) une interface DATASection et une opération Document :: createCDATASection. Ce sont des interfaces d'extension , Prises en charge uniquement si une implémentation prend en charge la fonctionnalité "Xml".

depuis xml.dom import minidom

my_xmldoc = minidom.parse (fichier xml)

my_xmldoc.createCDATASection (données)

maintenant vous avez le noeud de cadata l'ajouter où vous voulez ....

1
johnpaultthomas

La solution acceptée ne peut pas fonctionner avec Python 2.7. Cependant, il existe un autre paquet appelé lxml qui (bien que légèrement plus lent) partageait une syntaxe en grande partie identique avec le xml.etree.ElementTree. lxml est capable d'écrire et d'analyser CDATA. Documentation ici

1
elwc

Je suis arrivé ici à la recherche d'un moyen "d'analyser un XML avec des sections CDATA, puis de le sortir à nouveau avec les sections CDATA". 

J'ai pu faire cela (peut-être que lxml a été mis à jour depuis ce post?) Avec ce qui suit: (c'est un peu difficile - désolé ;-). Quelqu'un d’autre aurait peut-être un meilleur moyen de trouver les sections CDATA par programme, mais j’étais trop paresseux.

 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",)
0
tom stratton

Voici ma version qui est basée sur les réponses de Gooli et d'Amaury ci-dessus. Cela fonctionne à la fois pour ElementTree 1.2.6 et 1.3.0, qui utilisent des méthodes très différentes pour le faire.

Notez que gooli ne fonctionne pas avec 1.3.0, ce qui semble être la norme actuelle dans Python 2.7.x.

Notez également que cette version n'utilise pas non plus la méthode CDATA () gooli.

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)
0
Michael

pour python3 et ElementTree, vous pouvez utiliser le prochain récepteur 

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)

si vous avez besoin de xml en str, vous pouvez utiliser 

ET.tostring(tree)

ou le hack suivant (qui correspond presque au code dans tostring())

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

et obtenir le résultat 

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