Analyser XML avec un espace de noms en Python via 'ElementTree'

164

J'ai le XML suivant que je veux analyser en utilisant Python ElementTree:

<rdf:RDF xml:base="http://dbpedia.org/ontology/"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:owl="http://www.w3.org/2002/07/owl#"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
    xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
    xmlns="http://dbpedia.org/ontology/">

    <owl:Class rdf:about="http://dbpedia.org/ontology/BasketballLeague">
        <rdfs:label xml:lang="en">basketball league</rdfs:label>
        <rdfs:comment xml:lang="en">
          a group of sports teams that compete against each other
          in Basketball
        </rdfs:comment>
    </owl:Class>

</rdf:RDF>

Je veux trouver toutes les owl:Classbalises, puis extraire la valeur de toutes les rdfs:labelinstances qu'elles contiennent. J'utilise le code suivant:

tree = ET.parse("filename")
root = tree.getroot()
root.findall('owl:Class')

En raison de l'espace de noms, j'obtiens l'erreur suivante.

SyntaxError: prefix 'owl' not found in prefix map

J'ai essayé de lire le document à http://effbot.org/zone/element-namespaces.htm mais je ne suis toujours pas en mesure de faire fonctionner cela car le XML ci-dessus a plusieurs espaces de noms imbriqués.

Veuillez me faire savoir comment modifier le code pour trouver toutes les owl:Classbalises.

Sudar
la source

Réponses:

227

ElementTree n'est pas trop intelligent sur les espaces de noms. Vous devez donner aux méthodes .find(), findall()et iterfind()un dictionnaire d'espace de noms explicite. Ceci n'est pas très bien documenté:

namespaces = {'owl': 'http://www.w3.org/2002/07/owl#'} # add more as needed

root.findall('owl:Class', namespaces)

Les préfixes ne sont recherchés que dans le namespacesparamètre que vous passez. Cela signifie que vous pouvez utiliser n'importe quel préfixe d'espace de noms que vous souhaitez; l'API sépare la owl:partie, recherche l'URL d'espace de noms correspondante dans le namespacesdictionnaire, puis modifie la recherche pour rechercher l'expression XPath à la {http://www.w3.org/2002/07/owl}Classplace. Vous pouvez bien sûr utiliser la même syntaxe vous-même:

root.findall('{http://www.w3.org/2002/07/owl#}Class')

Si vous pouvez passer à la lxmlbibliothèque, les choses vont mieux; cette bibliothèque prend en charge la même API ElementTree, mais collecte les espaces de noms pour vous dans un .nsmapattribut sur les éléments.

Martijn Pieters
la source
7
Je vous remercie. Une idée comment puis-je obtenir l'espace de noms directement à partir de XML, sans le coder en dur? Ou comment puis-je l'ignorer? J'ai essayé findall ('{*} Class') mais cela ne fonctionnera pas dans mon cas.
Kostanos
7
Vous devrez parcourir vous-même l'arborescence pour les xmlnsattributs; comme indiqué dans la réponse, le lxmlfait pour vous, le xml.etree.ElementTreemodule ne le fait pas. Mais si vous essayez de faire correspondre un élément spécifique (déjà codé en dur), vous essayez également de faire correspondre un élément spécifique dans un espace de noms spécifique. Cet espace de noms ne changera pas plus entre les documents que le nom de l'élément. Vous pouvez également coder en dur avec le nom de l'élément.
Martijn Pieters
14
@Jon: n'influence register_namespaceque la sérialisation, pas la recherche.
Martijn Pieters
5
Petit ajout qui peut être utile: lors de l'utilisation à la cElementTreeplace de ElementTree, findallne prendra pas les espaces de noms comme argument mot-clé, mais plutôt simplement comme argument normal, c'est-à-dire use ctree.findall('owl:Class', namespaces).
egpbos
2
@Bludwarf: Les documents le mentionnent (maintenant, sinon quand vous l'avez écrit), mais vous devez les lire attentivement. Voir la section Analyse XML avec des espaces de noms : il existe un exemple contrastant l'utilisation de findallsans et ensuite avec l' namespaceargument, mais l'argument n'est pas mentionné comme l'un des arguments de la méthode méthode dans la section Objet élément .
Wilson F
57

Voici comment faire cela avec lxml sans avoir à coder en dur les espaces de noms ou à analyser le texte pour eux (comme le mentionne Martijn Pieters):

from lxml import etree
tree = etree.parse("filename")
root = tree.getroot()
root.findall('owl:Class', root.nsmap)

MISE À JOUR :

5 ans plus tard, je rencontre toujours des variantes de ce problème. lxml aide comme je l'ai montré ci-dessus, mais pas dans tous les cas. Les commentateurs ont peut-être un point valable concernant cette technique lorsqu'il s'agit de fusionner des documents, mais je pense que la plupart des gens ont du mal à simplement rechercher des documents.

Voici un autre cas et comment je l'ai géré:

<?xml version="1.0" ?><Tag1 xmlns="http://www.mynamespace.com/prefix">
<Tag2>content</Tag2></Tag1>

xmlns sans préfixe signifie que les balises sans préfixe obtiennent cet espace de noms par défaut. Cela signifie que lorsque vous recherchez Tag2, vous devez inclure l'espace de noms pour le trouver. Cependant, lxml crée une entrée nsmap avec Aucun comme clé, et je n'ai pas trouvé de moyen de la rechercher. J'ai donc créé un nouveau dictionnaire d'espaces de noms comme celui-ci

namespaces = {}
# response uses a default namespace, and tags don't mention it
# create a new ns map using an identifier of our choice
for k,v in root.nsmap.iteritems():
    if not k:
        namespaces['myprefix'] = v
e = root.find('myprefix:Tag2', namespaces)
Brad Dre
la source
3
L'URL complète de l'espace de noms est l'identifiant de l'espace de noms que vous êtes censé coder en dur. Le préfixe local ( owl) peut changer de fichier en fichier. Par conséquent, faire ce que cette réponse suggère est une très mauvaise idée.
Matti Virkkunen
1
@MattiVirkkunen exactement si la définition du hibou peut changer de fichier en fichier, ne devrions-nous pas utiliser la définition définie dans chaque fichier au lieu de la coder en dur?
Loïc Faure-Lacroix
@ LoïcFaure-Lacroix: Habituellement, les bibliothèques XML vous permettent de résumer cette partie. Vous n'avez même pas besoin de connaître ou de vous soucier du préfixe utilisé dans le fichier lui-même, vous définissez simplement votre propre préfixe dans le but d'analyser ou utilisez simplement le nom complet de l'espace de noms.
Matti Virkkunen
cette réponse m'a aidé à au moins pouvoir utiliser la fonction de recherche. Pas besoin de créer votre propre préfixe. J'ai juste fait key = list (root.nsmap.keys ()) [0] puis j'ai ajouté la clé comme préfixe: root.find (f '{key}: Tag2', root.nsmap)
Eelco van Vliet
30

Remarque : Ceci est une réponse utile pour la bibliothèque standard ElementTree de Python sans utiliser d'espaces de noms codés en dur.

Pour extraire les préfixes et l'URI de l'espace de noms des données XML, vous pouvez utiliser la ElementTree.iterparsefonction, en analysant uniquement les événements de démarrage de l'espace de noms ( start-ns ):

>>> from io import StringIO
>>> from xml.etree import ElementTree
>>> my_schema = u'''<rdf:RDF xml:base="http://dbpedia.org/ontology/"
...     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
...     xmlns:owl="http://www.w3.org/2002/07/owl#"
...     xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
...     xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
...     xmlns="http://dbpedia.org/ontology/">
... 
...     <owl:Class rdf:about="http://dbpedia.org/ontology/BasketballLeague">
...         <rdfs:label xml:lang="en">basketball league</rdfs:label>
...         <rdfs:comment xml:lang="en">
...           a group of sports teams that compete against each other
...           in Basketball
...         </rdfs:comment>
...     </owl:Class>
... 
... </rdf:RDF>'''
>>> my_namespaces = dict([
...     node for _, node in ElementTree.iterparse(
...         StringIO(my_schema), events=['start-ns']
...     )
... ])
>>> from pprint import pprint
>>> pprint(my_namespaces)
{'': 'http://dbpedia.org/ontology/',
 'owl': 'http://www.w3.org/2002/07/owl#',
 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
 'xsd': 'http://www.w3.org/2001/XMLSchema#'}

Ensuite, le dictionnaire peut être passé en argument aux fonctions de recherche:

root.findall('owl:Class', my_namespaces)
Davide Brunato
la source
1
Ceci est utile pour ceux d'entre nous sans accès à lxml et sans vouloir coder en dur l'espace de noms.
delrocco
1
J'ai eu l'erreur: ValueError: write to closedpour cette ligne filemy_namespaces = dict([node for _, node in ET.iterparse(StringIO(my_schema), events=['start-ns'])]). Une idée veut mal?
Yuli le
L'erreur est probablement liée à la classe io.StringIO, qui refuse les chaînes ASCII. J'avais testé ma recette avec Python3. L'ajout du préfixe de chaîne Unicode 'u' à la chaîne d'exemple fonctionne également avec Python 2 (2.7).
Davide Brunato
Au lieu de cela, dict([...])vous pouvez également utiliser la compréhension de dict.
Arminius
Au lieu de cela, StringIO(my_schema)vous pouvez également mettre le nom de fichier du fichier XML.
JustAC0der
6

J'ai utilisé un code similaire à celui-ci et j'ai trouvé qu'il valait toujours la peine de lire la documentation ... comme d'habitude!

findall () ne trouvera que les éléments qui sont des enfants directs de la balise courante . Donc, pas vraiment TOUS.

Cela peut valoir la peine d'essayer de faire fonctionner votre code avec les éléments suivants, surtout si vous avez affaire à des fichiers XML volumineux et complexes afin que ces sous-sous-éléments (etc.) soient également inclus. Si vous savez vous-même où se trouvent les éléments dans votre xml, alors je suppose que ça ira! Je pensais juste que cela valait la peine de se souvenir.

root.iter()

ref: https://docs.python.org/3/library/xml.etree.elementtree.html#finding-interesting-elements "Element.findall () ne trouve que les éléments avec une balise qui sont des enfants directs de l'élément courant. Element.find () trouve le premier enfant avec une balise particulière, et Element.text accède au contenu texte de l'élément. Element.get () accède aux attributs de l'élément: "

MJM
la source
6

Pour obtenir l'espace de noms dans son format d'espace de noms, par exemple {myNameSpace}, vous pouvez faire ce qui suit:

root = tree.getroot()
ns = re.match(r'{.*}', root.tag).group(0)

De cette façon, vous pouvez l'utiliser plus tard dans votre code pour trouver des nœuds, par exemple en utilisant une interpolation de chaîne (Python 3).

link = root.find(f"{ns}link")
Bram Vanroy
la source
0

Ma solution est basée sur le commentaire de @Martijn Pieters:

register_namespace influence uniquement la sérialisation, pas la recherche.

L'astuce ici est donc d'utiliser différents dictionnaires pour la sérialisation et la recherche.

namespaces = {
    '': 'http://www.example.com/default-schema',
    'spec': 'http://www.example.com/specialized-schema',
}

Maintenant, enregistrez tous les espaces de noms pour l'analyse et l'écriture:

for name, value in namespaces.iteritems():
    ET.register_namespace(name, value)

Pour la recherche ( find(), findall(), iterfind()) nous avons besoin d' un préfixe non vide. Passez à ces fonctions un dictionnaire modifié (ici je modifie le dictionnaire original, mais cela ne doit être fait qu'après l'enregistrement des espaces de noms).

self.namespaces['default'] = self.namespaces['']

Désormais, les fonctions de la find()famille peuvent être utilisées avec le defaultpréfixe:

print root.find('default:myelem', namespaces)

mais

tree.write(destination)

n'utilise aucun préfixe pour les éléments de l'espace de noms par défaut.

peter.slizik
la source