Modulo Python ElementTree: come ignorare lo spazio dei nomi dei file XML per individuare l'elemento corrispondente quando si utilizza il metodo "find", "findall"


136

Voglio usare il metodo di "findall" per individuare alcuni elementi del file XML di origine nel modulo ElementTree.

Tuttavia, il file XML di origine (test.xml) ha spazio dei nomi. Troncare parte del file XML come esempio:

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

Il codice di esempio di Python è di seguito:

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>

Sebbene possa funzionare, poiché esiste uno spazio dei nomi "{http://www.test.com}", è molto scomodo aggiungere uno spazio dei nomi davanti a ciascun tag.

Come posso ignorare lo spazio dei nomi quando uso il metodo di "trova", "trova tutto" e così via?


18
È tree.findall("xmlns:DEAL_LEVEL/xmlns:PAID_OFF", namespaces={'xmlns': 'http://www.test.com'})abbastanza conveniente?
iMom0

Grazie mille. Provo il tuo metodo e può funzionare. È più conveniente del mio ma è ancora un po 'imbarazzante. Sai se non esiste un altro metodo appropriato nel modulo ElementTree per risolvere questo problema o non esiste affatto tale metodo?
KevinLeng,

Oppure provatree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
Warf,

In Python 3.8, è possibile utilizzare un carattere jolly per lo spazio dei nomi. stackoverflow.com/a/62117710/407651
mzjn

Risposte:


62

Invece di modificare il documento XML stesso, è meglio analizzarlo e quindi modificare i tag nel risultato. In questo modo è possibile gestire più spazi dei nomi e alias dello spazio dei nomi:

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

Questo si basa sulla discussione qui: http://bugs.python.org/issue18304

Aggiornamento: rpartition invece di partitionassicurarti di ottenere il nome del tag postfixanche se non c'è spazio dei nomi. Quindi potresti condensarlo:

for _, el in it:
    _, _, el.tag = el.tag.rpartition('}') # strip ns

2
Questo. Questo questo questo. Più spazi dei nomi sarebbero stati la mia morte.
Jess,

8
OK, questo è bello e più avanzato, ma ancora non lo è et.findall('{*}sometag'). E sta anche distruggendo l'albero degli elementi stesso, non solo "esegui la ricerca ignorando gli spazi dei nomi proprio questa volta, senza ripetere l'analisi del documento, ecc., Mantenendo le informazioni dello spazio dei nomi". Bene, per quel caso è necessario osservare ripetutamente l'albero e vedere di persona se il nodo soddisfa i tuoi desideri dopo aver rimosso lo spazio dei nomi.
Tomasz Gandor,

1
Questo funziona rimuovendo la stringa ma quando salvo il file XML usando write (...) lo spazio dei nomi scompare dall'accattonaggio dell'XMLns = " bla " scompare. Per favore, consigli
TraceKira,

@TomaszGandor: potresti aggiungere lo spazio dei nomi a un attributo separato, forse. Per semplici test di contenimento dei tag ( questo documento contiene questo nome di tag? ) Questa soluzione è eccezionale e può essere messa in corto circuito.
Martijn Pieters

@TraceKira: questa tecnica rimuove gli spazi dei nomi dal documento analizzato e non puoi usarli per creare una nuova stringa XML con spazi dei nomi. Memorizzare i valori dello spazio dei nomi in un attributo aggiuntivo (e reinserire lo spazio dei nomi prima di trasformare nuovamente l'albero XML in una stringa) o analizzare nuovamente l'origine originale per applicare le modifiche a quello basato sull'albero rimosso.
Martijn Pieters

48

Se rimuovi l'attributo xmlns dall'xml prima di analizzarlo, non ci sarà uno spazio dei nomi anteposto a ciascun tag nella struttura.

import re

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

5
Ciò ha funzionato in molti casi per me, ma poi mi sono imbattuto in più spazi dei nomi e alias dello spazio dei nomi. Vedi la mia risposta per un altro approccio che gestisce questi casi.
nonagon,

47
-1 manipolare l'xml tramite un'espressione regolare prima dell'analisi è semplicemente sbagliato. sebbene possa funzionare in alcuni casi, questa non dovrebbe essere la risposta più votata e non dovrebbe essere utilizzata in un'applicazione professionale.
Mike,

1
A parte il fatto che l'utilizzo di una regex per un processo di analisi XML è intrinsecamente non corretto, ciò non funzionerà per molti documenti XML , poiché ignora i prefissi dello spazio dei nomi e il fatto che la sintassi XML consente spazi bianchi arbitrari prima dei nomi degli attributi (non solo spazi) e attorno al =segno di uguale.
Martijn Pieters

Sì, è veloce e sporco, ma è sicuramente la soluzione più elegante per casi d'uso semplici, grazie!
rimkashox,

18

Le risposte finora hanno esplicitamente inserito il valore dello spazio dei nomi nello script. Per una soluzione più generica, preferirei estrarre lo spazio dei nomi dall'xml:

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

E usalo nel metodo find:

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

15
Troppo da presumere che ce n'è solo unonamespace
Kashyap,

Ciò non tiene conto del fatto che i tag nidificati possono utilizzare spazi dei nomi diversi.
Martijn Pieters

15

Ecco un'estensione alla risposta di nonagon, che elimina anche gli spazi dei nomi dagli attributi:

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

AGGIORNAMENTO: aggiunto in list()modo che l'iteratore funzioni (necessario per Python 3)


14

Migliorare la risposta di ericspod:

Invece di cambiare la modalità di analisi a livello globale, possiamo racchiuderlo in un oggetto che supporta il costrutto 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

Questo può quindi essere usato come segue

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

Il bello di questo modo è che non cambia alcun comportamento per il codice non correlato al di fuori del blocco with. Ho finito per crearlo dopo aver ricevuto errori nelle librerie non correlate dopo aver utilizzato la versione di ericspod, che è stata utilizzata anche da expat.


Questo è dolce e salutare! Mi hai salvato la giornata! +1
AndreasT

In Python 3.8 (non ho testato con altre versioni) questo non sembra funzionare per me. Guardando la fonte dovrebbe funzionare, ma sembra che il codice sorgente xml.etree.ElementTree.XMLParsersia in qualche modo ottimizzato e l'applicazione di patch scimmia non expatha assolutamente alcun effetto.
Reinderien,

Ah sì. Vedere @ di Barny commento: stackoverflow.com/questions/13412496/...
Reinderien

5

Puoi anche usare l'elegante costrutto di formattazione delle stringhe:

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

oppure, se sei sicuro che PAID_OFF appare solo in un livello nella struttura:

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

2

Se stai utilizzando ElementTreee non cElementTreepuoi forzare Expat a ignorare l'elaborazione dello spazio dei nomi sostituendo ParserCreate():

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

ElementTreetenta di usare Expat chiamando ParserCreate()ma non fornisce alcuna opzione per non fornire una stringa di separazione dello spazio dei nomi, il codice sopra lo farà ignorare ma ti avviserà che ciò potrebbe interrompere altre cose.


Questo è un modo migliore rispetto ad altre risposte attuali in quanto non dipende dall'elaborazione delle stringhe
lijat

3
In Python 3.7.2 (e possibilmente eariler) AFAICT non è più possibile evitare di usare cElementTree, quindi questa soluzione alternativa potrebbe non essere possibile :-(
pessimo

1
cElemTree è deprecato ma c'è shadowing dei tipi viene fatto con C acceleratori . Il codice C non sta chiamando in expat quindi sì, questa soluzione è rotta.
ericspod

@barny è ancora possibile, ElementTree.fromstring(s, parser=None)sto provando a passarci il parser.
est

2

Potrei essere in ritardo per questo, ma non credo re.subsia una buona soluzione.

Tuttavia, la riscrittura xml.parsers.expatnon funziona per le versioni 3.x di Python,

Il principale colpevole è la parte xml/etree/ElementTree.pyinferiore del codice sorgente

# 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

Che è un po 'triste.

La soluzione è di sbarazzarsene prima.

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)

Testato su Python 3.6.

L' tryistruzione Try è utile nel caso in cui da qualche parte nel tuo codice ricarichi o importi un modulo due volte ottieni strani errori come

  • superata la profondità massima di ricorsione
  • AttributeError: XMLParser

tra l'altro il codice sorgente di etree sembra davvero disordinato.


1

Uniamo la risposta di nonagon con la risposta di mzjn a una domanda correlata :

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 questa funzione noi:

  1. Crea un iteratore per ottenere sia gli spazi dei nomi sia un oggetto albero analizzato .

  2. Scorri sull'iteratore creato per ottenere il comando degli spazi dei nomi che possiamo in seguito passare in ciascuno find()o findall()chiamare come suggerito da iMom0 .

  3. Restituisce l'oggetto e gli spazi dei nomi dell'elemento radice dell'albero analizzato.

Penso che questo sia l'approccio migliore in quanto non vi è alcuna manipolazione di un XML di origine o di un risultato analizzato risultante di xml.etree.ElementTreequalunque tipo.

Vorrei anche dare credito alla risposta di Barny fornendo un pezzo essenziale di questo puzzle (che puoi ottenere la radice analizzata dall'iteratore). Fino a quel momento in realtà ho attraversato due volte l'albero XML nella mia applicazione (una volta per ottenere spazi dei nomi, secondo per una radice).


ho scoperto come usarlo, ma non funziona per me, vedo ancora gli spazi dei nomi nell'output
taiko

1
Guarda il commento di iMom0 alla domanda di OP . Usando questa funzione ottieni sia l'oggetto analizzato che i mezzi per interrogarlo con find()e findall(). Devi semplicemente alimentare questi metodi con il comando dello spazio dei nomi parse_xml()e utilizzare il prefisso dello spazio dei nomi nelle tue query. Ad esempio:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
z33k,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.