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»

136

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

Cependant, 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 peu pratique d'ajouter un espace de noms devant chaque balise.

Comment puis-je ignorer l'espace de noms lors de l'utilisation de la méthode "find", "findall" et ainsi de suite?

KevinLeng
la source
18
Est-ce tree.findall("xmlns:DEAL_LEVEL/xmlns:PAID_OFF", namespaces={'xmlns': 'http://www.test.com'})assez pratique?
iMom0
Merci beaucoup. J'essaye ta méthode et ça peut marcher. C'est plus pratique que le mien mais c'est quand même un peu gênant. Savez-vous s'il n'y a pas d'autre méthode appropriée dans le module ElementTree pour résoudre ce problème ou s'il n'existe aucune méthode de ce type?
KevinLeng
Ou essayeztree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
Warf
Dans Python 3.8, un caractère générique peut être utilisé pour l'espace de noms. stackoverflow.com/a/62117710/407651
mzjn

Réponses:

62

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 io import StringIO  # for Python 2 import from StringIO instead
import xml.etree.ElementTree as ET

# instead of ET.fromstring(xml)
it = ET.iterparse(StringIO(xml))
for _, el in it:
    prefix, has_namespace, postfix = el.tag.partition('}')
    if has_namespace:
        el.tag = postfix  # strip all namespaces
root = it.root

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

Mise à jour: rpartition au lieu de partitions'assure que vous obtenez le nom de la balise postfixmême s'il n'y a pas d'espace de noms. Ainsi vous pourriez le condenser:

for _, el in it:
    _, _, el.tag = el.tag.rpartition('}') # strip ns
nonagon
la source
2
Ce. Ceci ceci ceci. Les espaces de noms multiples allaient être la mort de moi.
Jess
8
OK, c'est sympa et plus avancé, mais ce n'est toujours pas le cas et.findall('{*}sometag'). Et cela modifie aussi l'arborescence des éléments elle-même, pas simplement "effectuer la recherche en ignorant les espaces de noms juste cette fois, sans réanalyser le document, etc., en conservant les informations d'espace de noms". Eh bien, dans ce cas, vous devez de manière observable parcourir l'arborescence et voir par vous-même si le nœud correspond à vos souhaits après avoir supprimé l'espace de noms.
Tomasz Gandor
1
Cela fonctionne en supprimant la chaîne, mais lorsque j'enregistre le fichier XML en utilisant write (...), l'espace de noms disparaît de la mendicité du XML xmlns = " bla " disparaît. S'il vous plaît conseils
TraceKira
@TomaszGandor: vous pourriez peut-être ajouter l'espace de noms à un attribut séparé. Pour les tests simples de confinement de balises ( ce document contient-il ce nom de balise? ), Cette solution est excellente et peut être court-circuitée.
Martijn Pieters
@TraceKira: cette technique supprime les espaces de noms du document analysé, et vous ne pouvez pas l'utiliser pour créer une nouvelle chaîne XML avec des espaces de noms. Soit stocker les valeurs de l'espace de noms dans un attribut supplémentaire (et remettre l'espace de noms avant de reconvertir l'arborescence XML en chaîne), soit effectuer une nouvelle analyse à partir de la source d'origine pour appliquer les modifications à celle basée sur l'arborescence dépouillée.
Martijn Pieters
48

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

import re

xmlstring = re.sub(' xmlns="[^"]+"', '', xmlstring, count=1)
user2212280
la source
5
Cela a fonctionné dans de nombreux cas pour moi, mais j'ai ensuite rencontré plusieurs espaces de noms et alias d'espaces de noms. Voir ma réponse pour une autre approche qui gère ces cas.
nonagon
47
-1 manipuler le XML via une expression régulière avant l'analyse est tout simplement faux. bien que cela puisse fonctionner dans certains cas, cela ne devrait pas être la réponse la plus votée et ne devrait pas être utilisée dans une application professionnelle.
Mike
1
Outre le fait que l'utilisation d'une expression régulière pour un travail d'analyse XML est intrinsèquement défectueuse, cela ne fonctionnera pas pour de nombreux documents XML , car elle ignore les préfixes d'espace de noms, et le fait que la syntaxe XML autorise des espaces arbitraires avant les noms d'attributs (pas seulement espaces) et autour du =signe égal.
Martijn Pieters
Oui, c'est rapide et sale, mais c'est certainement la solution la plus élégante pour les cas d'utilisation simples, merci!
rimkashox
18

Les réponses jusqu'à présent mettent 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 de recherche:

namespace = get_namespace(tree.getroot())
print tree.find('./{0}parent/{0}version'.format(namespace)).text
wimous
la source
15
Trop de supposer qu'il n'y en a qu'unnamespace
Kashyap
Cela ne tient pas compte du fait que les balises imbriquées peuvent utiliser des espaces de noms différents.
Martijn Pieters
15

Voici une extension de la réponse de nonagon, qui supprime également les espaces de noms des attributs:

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

UPDATE: ajouté list()pour que l'itérateur fonctionne (nécessaire pour Python 3)

barny
la source
14

Améliorer la réponse par ericspod:

Au lieu de changer le mode d'analyse globalement, nous pouvons envelopper cela dans un objet prenant en charge 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

Cela peut ensuite être utilisé comme suit

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

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

lijat
la source
C'est doux ET sain! J'ai sauvé ma journée! +1
AndreasT
En Python 3.8 (je n'ai pas testé avec d'autres versions) cela ne semble pas fonctionner pour moi. En regardant la source, cela devrait fonctionner, mais il semble que le code source pour xml.etree.ElementTree.XMLParserest en quelque sorte optimisé et le monkey-patching expatn'a absolument aucun effet.
Reinderien
Ah ouais. Voir le commentaire de @ barny: stackoverflow.com/questions/13412496/…
Reinderien
5

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

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 qu'à un seul niveau de l'arborescence:

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

Si vous utilisez ElementTreeet non, cElementTreevous 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)

ElementTreeessaie d'utiliser Expat en appelant ParserCreate()mais ne fournit aucune option pour ne pas fournir de chaîne de séparation d'espace de noms, le code ci-dessus le fera ignorer mais sachez que cela pourrait casser d'autres choses.

ericspod
la source
C'est un meilleur moyen que les autres réponses actuelles car cela ne dépend pas du traitement des chaînes
lijat
3
En python 3.7.2 (et éventuellement eariler) AFAICT, il n'est plus possible d'éviter d'utiliser cElementTree, donc cette solution de contournement peut ne pas être possible :-(
barny
1
cElemTree est dépréciée mais il y a observation des types se fait avec des accélérateurs C . Le code C n'appelle pas les expatriés, donc oui, cette solution est cassée.
ericspod
@barny c'est encore possible, ElementTree.fromstring(s, parser=None)j'essaye de lui passer un analyseur.
est
2

Je suis peut-être en retard pour cela, mais je ne pense pas que ce re.subsoit une bonne solution.

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

Le principal coupable est la xml/etree/ElementTree.pyvue 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 est de s'en débarrasser d'abord.

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.

L' tryinstruction Try est utile au cas où quelque part dans votre code vous rechargez ou importez un module deux fois, vous obtenez des erreurs étranges comme

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

btw damn le code source etree semble vraiment désordonné.

est
la source
1

Unissons la réponse de nonagon avec la réponse de mzjn à 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. Itérez sur l'itérateur créé pour obtenir les espaces de noms dict que nous pouvons plus tard passer dans chacun find()ou findall()appeler comme suggéré par iMom0 .

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

Je pense que c'est la meilleure approche tout autour car il n'y a aucune manipulation ni d'un XML source ni de la xml.etree.ElementTreesortie analysée résultante impliquée.

Je voudrais également attribuer à la réponse de Barny une pièce essentielle de ce puzzle (que vous pouvez obtenir la racine analysée de l'itérateur). Jusque-là, j'ai parcouru l'arborescence XML deux fois dans mon application (une fois pour obtenir des espaces de noms, une seconde pour une racine).

z33k
la source
a découvert comment l'utiliser, mais cela ne fonctionne pas pour moi, je vois toujours les espaces de noms dans la sortie
taiko
1
Regardez le commentaire d'iMom0 à la question d'OP . En utilisant cette fonction, vous obtenez à la fois l'objet analysé et les moyens de l'interroger avec find()et findall(). Vous alimentez simplement ces méthodes avec le dict des espaces de nomsparse_xml() et utilisez le préfixe de l'espace de noms dans vos requêtes. Exemple:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
z33k le