Analizando XML con espacio de nombres en Python a través de 'ElementTree'

163

Tengo el siguiente XML que quiero analizar usando 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>

Quiero encontrar todas las owl:Classetiquetas y luego extraer el valor de todas las rdfs:labelinstancias dentro de ellas. Estoy usando el siguiente código:

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

Debido al espacio de nombres, recibo el siguiente error.

SyntaxError: prefix 'owl' not found in prefix map

Intenté leer el documento en http://effbot.org/zone/element-namespaces.htm pero todavía no puedo hacer que esto funcione ya que el XML anterior tiene múltiples espacios de nombres anidados.

Por favor, hágame saber cómo cambiar el código para encontrar todas las owl:Classetiquetas.

Sudar
fuente

Respuestas:

226

ElementTree no es demasiado inteligente sobre los espacios de nombres. Es necesario dar el .find(), findall()y iterfind()los métodos de un diccionario de espacio de nombres explícito. Esto no está muy bien documentado:

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

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

Los prefijos solo se buscan en el namespacesparámetro que ingresa. Esto significa que puede usar cualquier prefijo de espacio de nombres que desee; la API se divide de la owl:parte, busca la URL del espacio de nombres correspondiente en el namespacesdiccionario, luego cambia la búsqueda para buscar la expresión XPath en su {http://www.w3.org/2002/07/owl}Classlugar. También puede usar la misma sintaxis, por supuesto:

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

Si puede cambiar a la lxmlbiblioteca, las cosas están mejor; esa biblioteca admite la misma API de ElementTree, pero recopila espacios de nombres para usted en un .nsmapatributo en los elementos.

Martijn Pieters
fuente
77
Gracias. ¿Alguna idea de cómo puedo obtener el espacio de nombres directamente desde XML, sin codificarlo? ¿O cómo puedo ignorarlo? He intentado con Findall ('{*} Class') pero no funcionará en mi caso.
Kostanos
77
Tendría que escanear el árbol en busca de xmlnsatributos usted mismo; como se indica en la respuesta, lxmlhace esto por usted, el xml.etree.ElementTreemódulo no. Pero si está tratando de hacer coincidir un elemento específico (ya codificado), también está tratando de hacer coincidir un elemento específico en un espacio de nombres específico. Ese espacio de nombres no va a cambiar entre documentos más que el nombre del elemento. También puede codificar eso con el nombre del elemento.
Martijn Pieters
14
@ Jon: register_namespacesolo influye en la serialización, no en la búsqueda.
Martijn Pieters
55
Pequeña adición que puede ser útil: cuando se usa en cElementTreelugar de ElementTree, findallno tomará espacios de nombres como argumento de palabra clave, sino simplemente como un argumento normal, es decir, uso ctree.findall('owl:Class', namespaces).
egpbos
2
@Bludwarf: Los documentos lo mencionan (ahora, si no cuando escribiste eso), pero tienes que leerlos muy cuidadosamente. Consulte la sección Análisis de XML con espacios de nombres : hay un ejemplo que contrasta el uso de findallsin y luego con el namespaceargumento, pero el argumento no se menciona como uno de los argumentos del método del método en la sección del objeto Elemento .
Wilson F
57

Aquí le mostramos cómo hacer esto con lxml sin tener que codificar los espacios de nombres o escanear el texto en busca de ellos (como menciona Martijn Pieters):

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

ACTUALIZACIÓN :

5 años después todavía me encuentro con variaciones de este problema. lxml ayuda como mostré arriba, pero no en todos los casos. Los comentaristas pueden tener un punto válido con respecto a esta técnica cuando se trata de fusionar documentos, pero creo que la mayoría de las personas tienen dificultades simplemente buscando documentos.

Aquí hay otro caso y cómo lo manejé:

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

xmlns sin prefijo significa que las etiquetas no prefijadas obtienen este espacio de nombres predeterminado. Esto significa que cuando busca Tag2, debe incluir el espacio de nombres para encontrarlo. Sin embargo, lxml crea una entrada nsmap con None como clave, y no pude encontrar una manera de buscarla. Entonces, creé un nuevo diccionario de espacio de nombres como este

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
fuente
3
La URL completa del espacio de nombres es el identificador del espacio de nombres que se supone que debe codificar. El prefijo local ( owl) puede cambiar de un archivo a otro. Por lo tanto, hacer lo que sugiere esta respuesta es una muy mala idea.
Matti Virkkunen
1
@MattiVirkkunen exactamente si la definición de búho puede cambiar de un archivo a otro, ¿no deberíamos usar la definición definida en cada archivo en lugar de codificarla?
Loïc Faure-Lacroix
@ LoïcFaure-Lacroix: por lo general, las bibliotecas XML le permitirán abstraer esa parte. Ni siquiera necesita saber o preocuparse por el prefijo utilizado en el archivo en sí, solo define su propio prefijo con el fin de analizar o simplemente usar el nombre de espacio de nombres completo.
Matti Virkkunen
Esta respuesta me ayudó al menos a poder utilizar la función de búsqueda. No es necesario crear su propio prefijo. Acabo de hacer key = list (root.nsmap.keys ()) [0] y luego agregué la clave como prefijo: root.find (f '{key}: Tag2', root.nsmap)
Eelco van Vliet
30

Nota : Esta es una respuesta útil para la biblioteca estándar ElementTree de Python sin usar espacios de nombres codificados.

Para extraer los prefijos y el URI del espacio de nombres de los datos XML, puede usar la ElementTree.iterparsefunción, analizando solo los eventos de inicio del espacio de nombres ( 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#'}

Luego, el diccionario se puede pasar como argumento a las funciones de búsqueda:

root.findall('owl:Class', my_namespaces)
Davide Brunato
fuente
1
Esto es útil para aquellos de nosotros sin acceso a lxml y sin querer codificar el espacio de nombres.
delrocco
1
Recibí el error: ValueError: write to closedpara esta línea filemy_namespaces = dict([node for _, node in ET.iterparse(StringIO(my_schema), events=['start-ns'])]). Alguna idea quiere mal?
Yuli
Probablemente el error esté relacionado con la clase io.StringIO, que rechaza las cadenas ASCII. Había probado mi receta con Python3. Al agregar el prefijo de cadena Unicode 'u' a la cadena de muestra, también funciona con Python 2 (2.7).
Davide Brunato
En lugar de dict([...])usted también puede usar la comprensión dict.
Arminius
En lugar de StringIO(my_schema)usted también puede poner el nombre del archivo XML.
JustAC0der
6

He estado usando un código similar a este y he descubierto que siempre vale la pena leer la documentación ... ¡como siempre!

findall () solo encontrará elementos que son hijos directos de la etiqueta actual . Entonces, no TODOS.

Puede valer la pena intentar que su código funcione con lo siguiente, especialmente si se trata de archivos xml grandes y complejos para que también se incluyan esos subelementos (etc.). Si sabes dónde están los elementos en tu xml, ¡entonces supongo que estará bien! Solo pensé que valía la pena recordarlo.

root.iter()

ref: https://docs.python.org/3/library/xml.etree.elementtree.html#finding-interesting-elements "Element.findall () encuentra solo elementos con una etiqueta que son hijos directos del elemento actual. Element.find () encuentra el primer elemento secundario con una etiqueta particular y Element.text accede al contenido de texto del elemento. Element.get () accede a los atributos del elemento: "

MJM
fuente
6

Para obtener el espacio de nombres en su formato de espacio de nombres, por ejemplo {myNameSpace}, puede hacer lo siguiente:

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

De esta manera, puede usarlo más adelante en su código para buscar nodos, por ejemplo, usando la interpolación de cadenas (Python 3).

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

Mi solución se basa en el comentario de @Martijn Pieters:

register_namespace solo influye en la serialización, no en la búsqueda.

Entonces, el truco aquí es usar diferentes diccionarios para la serialización y la búsqueda.

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

Ahora, registre todos los espacios de nombres para analizar y escribir:

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

Para buscar ( find(), findall(), iterfind()) necesitamos un prefijo no vacío. Pase estas funciones a un diccionario modificado (aquí modifico el diccionario original, pero esto debe hacerse solo después de registrar los espacios de nombres).

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

Ahora, las funciones de la find()familia se pueden usar con el defaultprefijo:

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

pero

tree.write(destination)

no utiliza ningún prefijo para elementos en el espacio de nombres predeterminado.

peter.slizik
fuente