web-dev-qa-db-fra.com

Module Python ElementTree: comment ignorer l'espace de noms des fichiers XML pour localiser l'élément correspondant lors de l'utilisation de la méthode "find", "findall"

Je veux utiliser la méthode de "findall" pour localiser certains éléments du fichier source XML dans le module ElementTree.

Toutefois, le fichier XML source (test.xml) a un espace de noms. Je tronque une partie du fichier XML comme exemple:

<?xml version="1.0" encoding="iso-8859-1"?>
<XML_HEADER xmlns="http://www.test.com">
    <TYPE>Updates</TYPE>
    <DATE>9/26/2012 10:30:34 AM</DATE>
    <COPYRIGHT_NOTICE>All Rights Reserved.</COPYRIGHT_NOTICE>
    <LICENSE>newlicense.htm</LICENSE>
    <DEAL_LEVEL>
        <PAID_OFF>N</PAID_OFF>
        </DEAL_LEVEL>
</XML_HEADER>

L'exemple de code Python est ci-dessous:

from xml.etree import ElementTree as ET
tree = ET.parse(r"test.xml")
el1 = tree.findall("DEAL_LEVEL/PAID_OFF") # Return None
el2 = tree.findall("{http://www.test.com}DEAL_LEVEL/{http://www.test.com}PAID_OFF") # Return <Element '{http://www.test.com}DEAL_LEVEL/PAID_OFF' at 0xb78b90>

Bien que cela puisse fonctionner, car il existe un espace de noms "{http://www.test.com}", il est très pratique d'ajouter un espace de noms devant chaque balise.

Comment puis-je ignorer l'espace de noms lorsque j'utilise les méthodes "find", "findall", etc.?

97
KevinLeng

Au lieu de modifier le document XML lui-même, il est préférable de l'analyser, puis de modifier les balises dans le résultat. De cette façon, vous pouvez gérer plusieurs espaces de noms et alias d'espaces de noms:

from StringIO import StringIO
import xml.etree.ElementTree as ET

# instead of ET.fromstring(xml)
it = ET.iterparse(StringIO(xml))
for _, el in it:
    if '}' in el.tag:
        el.tag = el.tag.split('}', 1)[1]  # strip all namespaces
root = it.root

Ceci est basé sur la discussion ici: http://bugs.python.org/issue18304

44
nonagon

Si vous supprimez l'attribut xmlns du fichier xml avant de l'analyser, il n'y aura pas d'espace de nom ajouté à chaque balise dans l'arborescence.

import re

xmlstring = re.sub(' xmlns="[^"]+"', '', xmlstring, count=1)
41
user2212280

Jusqu'à présent, les réponses inséraient explicitement la valeur de l'espace de noms dans le script. Pour une solution plus générique, je préférerais extraire l'espace de noms du xml:

import re
def get_namespace(element):
  m = re.match('\{.*\}', element.tag)
  return m.group(0) if m else ''

Et utilisez-le dans la méthode find:

namespace = get_namespace(tree.getroot())
print tree.find('./{0}parent/{0}version'.format(namespace)).text
19
wimous

Voici une extension de la réponse de nonagon, qui supprime également les attributs de l'espace de nom:

from StringIO import StringIO
import xml.etree.ElementTree as ET

# instead of ET.fromstring(xml)
it = ET.iterparse(StringIO(xml))
for _, el in it:
    if '}' in el.tag:
        el.tag = el.tag.split('}', 1)[1]  # strip all namespaces
    for at in el.attrib.keys(): # strip namespaces of attributes too
        if '}' in at:
            newat = at.split('}', 1)[1]
            el.attrib[newat] = el.attrib[at]
            del el.attrib[at]
root = it.root
11
barny

Améliorer la réponse de ericspod:

Au lieu de changer le mode d'analyse globalement, nous pouvons envelopper cela dans un objet supportant la construction with.

from xml.parsers import expat

class DisableXmlNamespaces:
    def __enter__(self):
            self.oldcreate = expat.ParserCreate
            expat.ParserCreate = lambda encoding, sep: self.oldcreate(encoding, None)
    def __exit__(self, type, value, traceback):
            expat.ParserCreate = self.oldcreate

Ceci peut alors être utilisé comme suit

import xml.etree.ElementTree as ET
with DisableXmlNamespaces():
     tree = ET.parse("test.xml")

L'avantage de cette méthode est qu'elle ne modifie aucun comportement pour le code non lié en dehors du bloc with. J'ai fini par créer ceci après avoir eu des erreurs dans des bibliothèques non liées après avoir utilisé la version d'ericspod qui utilisait également expat.

5
lijat

Vous pouvez également utiliser la structure élégante de formatage de chaîne:

ns='http://www.test.com'
el2 = tree.findall("{%s}DEAL_LEVEL/{%s}PAID_OFF" %(ns,ns))

ou, si vous êtes sûr que PAID_OFF n'apparaît que dans un niveau de l'arborescence:

el2 = tree.findall(".//{%s}PAID_OFF" % ns)
3
tzp

Si vous utilisez ElementTree et non cElementTree, vous pouvez forcer Expat à ignorer le traitement de l'espace de noms en remplaçant ParserCreate():

from xml.parsers import expat
oldcreate = expat.ParserCreate
expat.ParserCreate = lambda encoding, sep: oldcreate(encoding, None)

ElementTree essaie d'utiliser Expat en appelant ParserCreate() mais n'offre aucune option pour ne pas fournir de chaîne de séparation d'espace de nom; le code ci-dessus le fera ignorer mais il sera averti que cela pourrait casser d'autres choses.

1
ericspod

Combinons réponse de nonagon avec réponse de mzjn'a à une question connexe :

def parse_xml(xml_path: Path) -> Tuple[ET.Element, Dict[str, str]]:
    xml_iter = ET.iterparse(xml_path, events=["start-ns"])
    xml_namespaces = dict(prefix_namespace_pair for _, prefix_namespace_pair in xml_iter)
    return xml_iter.root, xml_namespaces

En utilisant cette fonction, nous:

  1. Créez un itérateur pour obtenir les deux espaces de noms et un objet d'arborescence analysé .

  2. Parcourez l'itérateur créé pour obtenir les espaces de noms dictés, nous pourrons ensuite transmettre chaque appel find() ou findall()comme indiqué par iMom .

  3. Renvoie l'objet d'élément racine et les espaces de noms de l'arborescence analysée.

Je pense que c’est la meilleure approche car il n’ya pas de manipulation d’un fichier XML source ni de la sortie analysée xml.etree.ElementTree que ce soit.

J'aimerais aussi créditer réponse de barny pour avoir fourni une pièce essentielle de ce puzzle (que vous pouvez obtenir la racine analysée à partir de l'itérateur). Jusque-là, j'ai parcouru deux fois l'arborescence XML dans mon application (une pour obtenir des espaces de noms, l'autre pour une racine).

0
z33k

Je pourrais être en retard pour cela mais je ne pense pas que re.sub soit une bonne solution.

Cependant, la réécriture xml.parsers.expat ne fonctionne pas pour les versions Python 3.x,

Le principal responsable est le xml/etree/ElementTree.py voir en bas du code source

# Import the C accelerators
try:
    # Element is going to be shadowed by the C implementation. We need to keep
    # the Python version of it accessible for some "creative" by external code
    # (see tests)
    _Element_Py = Element

    # Element, SubElement, ParseError, TreeBuilder, XMLParser
    from _elementtree import *
except ImportError:
    pass

Ce qui est un peu triste.

La solution consiste à s'en débarrasser en premier.

import _elementtree
try:
    del _elementtree.XMLParser
except AttributeError:
    # in case deleted twice
    pass
else:
    from xml.parsers import expat  # NOQA: F811
    oldcreate = expat.ParserCreate
    expat.ParserCreate = lambda encoding, sep: oldcreate(encoding, None)

Testé sur Python 3.6.

Essayez la déclaration try est utile si vous rechargez ou importez un module deux fois dans votre code et que vous obtenez des erreurs étranges telles que

  • profondeur maximale de récursion dépassée
  • AttributeError: XMLParser

en fait, le code source d'etree semble vraiment compliqué.

0
est