Módulo Python ElementTree: cómo ignorar el espacio de nombres de los archivos XML para localizar elementos coincidentes cuando se utiliza el método "buscar", "findall"

136

Quiero utilizar el método de "findall" para localizar algunos elementos del archivo xml de origen en el módulo ElementTree.

Sin embargo, el archivo xml de origen (test.xml) tiene espacio de nombres. Trunco ​​parte del archivo xml como muestra:

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

El código de Python de muestra está abajo:

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>

Aunque puede funcionar, porque hay un espacio de nombres "{http://www.test.com}", es muy inconveniente agregar un espacio de nombres frente a cada etiqueta.

¿Cómo puedo ignorar el espacio de nombres cuando uso el método de "buscar", "findall", etc.

KevinLeng
fuente
18
¿Es lo tree.findall("xmlns:DEAL_LEVEL/xmlns:PAID_OFF", namespaces={'xmlns': 'http://www.test.com'})suficientemente conveniente?
iMom0
Muchas gracias. Pruebo tu método y puede funcionar. Es más conveniente que el mío, pero sigue siendo un poco incómodo. ¿Sabe si no hay otro método adecuado en el módulo ElementTree para resolver este problema o no existe tal método?
KevinLeng
O pruebatree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
Warf
En Python 3.8, se puede usar un comodín para el espacio de nombres. stackoverflow.com/a/62117710/407651
mzjn

Respuestas:

62

En lugar de modificar el documento XML en sí, es mejor analizarlo y luego modificar las etiquetas en el resultado. De esta manera puede manejar múltiples espacios de nombres y alias de espacios de nombres:

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

Esto se basa en la discusión aquí: http://bugs.python.org/issue18304

Actualización: en rpartition lugar de partitionasegurarse de obtener el nombre de la etiqueta postfixincluso si no hay espacio de nombres. Así podrías condensarlo:

for _, el in it:
    _, _, el.tag = el.tag.rpartition('}') # strip ns
nonagon
fuente
2
Esta. Esto esto esto. Múltiples espacios de nombres iban a ser la muerte para mí.
Jess
8
OK, esto es bueno y más avanzado, pero aún así no lo es et.findall('{*}sometag'). Y también está destruyendo el árbol de elementos en sí, no solo "realiza la búsqueda ignorando los espacios de nombres en este momento, sin volver a analizar el documento, etc., conservando la información del espacio de nombres". Bueno, para ese caso, es necesario que recorras el árbol y compruebes por ti mismo si el nodo coincide con tus deseos después de eliminar el espacio de nombres.
Tomasz Gandor
1
Esto funciona eliminando la cadena, pero cuando guardo el archivo XML usando escribir (...) el espacio de nombres desaparece de la petición del XML xmlns = " bla " desaparece. Por favor consejo
TraceKira
@TomaszGandor: tal vez podría agregar el espacio de nombres a un atributo separado. Para pruebas simples de contención de etiquetas ( ¿este documento contiene el nombre de esta etiqueta? ), Esta solución es excelente y se puede cortocircuitar.
Martijn Pieters
@TraceKira: esta técnica elimina los espacios de nombres del documento analizado, y no puede usar eso para crear una nueva cadena XML con espacios de nombres. Almacene los valores del espacio de nombres en un atributo adicional (y vuelva a colocar el espacio de nombres antes de volver a convertir el árbol XML en una cadena) o vuelva a analizar desde la fuente original para aplicar los cambios basados ​​en el árbol despojado.
Martijn Pieters
48

Si elimina el atributo xmlns del xml antes de analizarlo, entonces no habrá un espacio de nombres antepuesto a cada etiqueta en el árbol.

import re

xmlstring = re.sub(' xmlns="[^"]+"', '', xmlstring, count=1)
usuario2212280
fuente
55
Esto funcionó en muchos casos para mí, pero luego me encontré con múltiples espacios de nombres y alias de espacios de nombres. Vea mi respuesta para otro enfoque que maneja estos casos.
nonagon
47
-1 manipular el xml a través de una expresión regular antes de analizar es incorrecto. aunque podría funcionar en algunos casos, esta no debería ser la respuesta más votada y no debería usarse en una aplicación profesional.
Mike
1
Además del hecho de que el uso de una expresión regular para un trabajo de análisis XML es inherentemente incorrecto, esto no funcionará para muchos documentos XML , porque ignora los prefijos de espacio de nombres y el hecho de que la sintaxis XML permite espacios en blanco arbitrarios antes de los nombres de atributos (no solo espacios) y alrededor del =signo igual.
Martijn Pieters
Sí, es rápido y sucio, pero definitivamente es la solución más elegante para casos de uso simples, ¡gracias!
rimkashox
18

Las respuestas hasta ahora ponen explícitamente el valor del espacio de nombres en el script. Para una solución más genérica, prefiero extraer el espacio de nombres del xml:

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

Y úsalo en el método de búsqueda:

namespace = get_namespace(tree.getroot())
print tree.find('./{0}parent/{0}version'.format(namespace)).text
wimous
fuente
15
Demasiado para suponer que solo hay unonamespace
Kashyap
Esto no tiene en cuenta que las etiquetas anidadas pueden usar diferentes espacios de nombres.
Martijn Pieters
15

Aquí hay una extensión de la respuesta de nonagon, que también elimina los espacios de nombres de los atributos:

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

ACTUALIZACIÓN: agregado list()para que el iterador funcione (necesario para Python 3)

granero
fuente
14

Mejorando la respuesta de ericspod:

En lugar de cambiar el modo de análisis globalmente, podemos envolver esto en un objeto que soporte la construcción 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

Esto se puede usar de la siguiente manera

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

La belleza de esta manera es que no cambia ningún comportamiento para el código no relacionado fuera del bloque with. Terminé creando esto después de obtener errores en bibliotecas no relacionadas después de usar la versión de ericspod que también usaba expat.

lijat
fuente
¡Esto es dulce y saludable! Me salvó el día! +1
AndreasT
En Python 3.8 (no lo he probado con otras versiones) esto no parece funcionar para mí. En cuanto a la fuente, debería funcionar, pero parece que el código fuente de xml.etree.ElementTree.XMLParseralguna manera está optimizado y el parche de mono no expattiene absolutamente ningún efecto.
Reinderien
Ah sí. Vea el comentario de @ barny: stackoverflow.com/questions/13412496/…
Reinderien
5

También puede usar la elegante construcción de formato de cadena:

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

o, si está seguro de que PAID_OFF solo aparece en un nivel en el árbol:

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

Si está utilizando ElementTreey no cElementTreepuede forzar a Expat a ignorar el procesamiento del espacio de nombres reemplazando ParserCreate():

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

ElementTreeintenta usar Expat llamando ParserCreate()pero no proporciona ninguna opción para no proporcionar una cadena de separación de espacio de nombres, el código anterior hará que se ignore pero se le advierte que esto podría romper otras cosas.

ericspod
fuente
Esta es una mejor manera que otras respuestas actuales, ya que no depende del procesamiento de cadenas
lijat
3
En Python 3.7.2 (y posiblemente en el auricular) AFAICT ya no es posible evitar el uso de cElementTree, por lo que esta solución puede no ser posible :-(
barny
1
cElemTree está en desuso pero hay ensombrecimiento de tipos está haciendo con C aceleradores . El código C no está llamando a expatriados, así que sí, esta solución está rota.
ericspod
@barny todavía es posible, ElementTree.fromstring(s, parser=None)estoy tratando de pasarle un analizador.
Est
2

Podría llegar tarde a esto, pero no creo que re.subsea ​​una buena solución.

Sin embargo, la reescritura xml.parsers.expatno funciona para las versiones de Python 3.x,

El principal culpable es la xml/etree/ElementTree.pyparte inferior del código fuente.

# 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

Lo cual es un poco triste.

La solución es deshacerse de él primero.

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)

Probado en Python 3.6.

La trydeclaración de prueba es útil en caso de que en algún lugar de su código vuelva a cargar o importe un módulo dos veces y obtenga errores extraños como

  • profundidad de recursión máxima excedida
  • AttributeError: XMLParser

Por cierto, el código fuente de etree parece realmente desordenado.

est
fuente
1

Vamos a combinar la respuesta de nonagon con la respuesta de mzjn a una pregunta relacionada :

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

Usando esta función nosotros:

  1. Cree un iterador para obtener espacios de nombres y un objeto de árbol analizado .

  2. Itere sobre el iterador creado para obtener los espacios de nombres dictados que luego podemos pasar en cada uno find()o findall()llamar como lo sugiere iMom0 .

  3. Devuelve el objeto del elemento raíz del árbol analizado y los espacios de nombres.

Creo que este es el mejor enfoque en todos los aspectos, ya que no hay manipulación de un código fuente XML o de la xml.etree.ElementTreesalida analizada resultante de ningún tipo.

También me gustaría dar crédito a la respuesta de Barny al proporcionar una pieza esencial de este rompecabezas (que puede obtener la raíz analizada del iterador). Hasta ese momento, atravesé dos veces el árbol XML en mi aplicación (una vez para obtener espacios de nombres, la segunda para una raíz).

z33k
fuente
descubrí cómo usarlo, pero no funciona para mí, todavía veo los espacios de nombres en la salida
taiko
1
Mire el comentario de iMom0 a la pregunta de OP . Usando esta función obtienes tanto el objeto analizado como los medios para consultarlo con find()y findall(). Simplemente alimente esos métodos con el dict de espacios de nombresparse_xml() y use el prefijo del espacio de nombres en sus consultas. Por ejemplo:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
z33k