Moduł Python ElementTree: Jak zignorować przestrzeń nazw plików XML, aby zlokalizować pasujący element, używając metody „find”, „findall”

136

Chcę użyć metody „findall”, aby zlokalizować niektóre elementy źródłowego pliku xml w module ElementTree.

Jednak źródłowy plik xml (test.xml) ma przestrzeń nazw. Obcinam część pliku xml jako przykład:

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

Przykładowy kod w Pythonie znajduje się poniżej:

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>

Chociaż może to działać, ponieważ istnieje przestrzeń nazw „{http://www.test.com}”, dodawanie przestrzeni nazw przed każdym tagiem jest bardzo niewygodne.

Jak mogę zignorować przestrzeń nazw, używając metody „znajdź”, „znajdź wszystko” i tak dalej?

KevinLeng
źródło
18
Czy jest tree.findall("xmlns:DEAL_LEVEL/xmlns:PAID_OFF", namespaces={'xmlns': 'http://www.test.com'})wystarczająco wygodny?
iMom0
Dziękuję bardzo. Spróbuję twojej metody i może zadziałać. Jest to wygodniejsze niż moje, ale nadal jest trochę niezręczne. Czy wiesz, czy w module ElementTree nie ma innej właściwej metody rozwiązania tego problemu, czy w ogóle nie ma takiej metody?
KevinLeng
Lub spróbujtree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
Warf
W Pythonie 3.8 jako przestrzeń nazw można użyć symbolu wieloznacznego. stackoverflow.com/a/62117710/407651
mzjn

Odpowiedzi:

62

Zamiast modyfikować sam dokument XML, najlepiej go przeanalizować, a następnie zmodyfikować tagi w wyniku. W ten sposób możesz obsłużyć wiele przestrzeni nazw i aliasów przestrzeni nazw:

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

Jest to oparte na dyskusji tutaj: http://bugs.python.org/issue18304

Aktualizacja: rpartition zamiast partitionzapewniać, że otrzymujesz nazwę tagu, postfixnawet jeśli nie ma przestrzeni nazw. Tak więc możesz to skondensować:

for _, el in it:
    _, _, el.tag = el.tag.rpartition('}') # strip ns
nonagon
źródło
2
To. To to to. Wiele przestrzeni z nazwiskami oznaczało moją śmierć.
Jess,
8
OK, to jest fajne i bardziej zaawansowane, ale nadal nie jest et.findall('{*}sometag'). Jest to również zniekształcenie samego drzewa elementów, a nie tylko „wykonanie wyszukiwania ignorując przestrzenie nazw tylko tym razem, bez ponownego analizowania dokumentu itp., Zachowując informacje o przestrzeni nazw”. Cóż, w takim przypadku musisz obserwować iterację po drzewie i samemu przekonać się, czy węzeł spełnia twoje życzenia po usunięciu przestrzeni nazw.
Tomasz Gandor
1
Działa to przez obcinanie łańcucha, ale kiedy zapisuję plik XML za pomocą write (...), przestrzeń nazw znika od początku XML xmlns = " bla " znika. Proszę o radę
TraceKira
@TomaszGandor: być może mógłbyś dodać przestrzeń nazw do oddzielnego atrybutu. W przypadku prostych testów zawierania tagów ( czy ten dokument zawiera tę nazwę tagu? ) To rozwiązanie jest świetne i można je skrócić.
Martijn Pieters
@TraceKira: ta technika usuwa przestrzenie nazw z przeanalizowanego dokumentu i nie można jej użyć do utworzenia nowego ciągu XML z przestrzeniami nazw. Albo zapisz wartości przestrzeni nazw w dodatkowym atrybucie (i umieść przestrzeń nazw z powrotem przed przekształceniem drzewa XML z powrotem w łańcuch) lub przeprowadź ponownie analizę z oryginalnego źródła, aby zastosować zmiany w tym opartym na usuniętym drzewie.
Martijn Pieters
48

Jeśli usuniesz atrybut xmlns z xml przed jego przeanalizowaniem, nie będzie przestrzeni nazw dołączonej do każdego znacznika w drzewie.

import re

xmlstring = re.sub(' xmlns="[^"]+"', '', xmlstring, count=1)
user2212280
źródło
5
W wielu przypadkach działało to dla mnie, ale potem natknąłem się na wiele przestrzeni nazw i aliasów przestrzeni nazw. Zobacz moją odpowiedź na inne podejście, które obsługuje te przypadki.
nonagon
47
-1 manipulowanie plikiem XML za pomocą wyrażenia regularnego przed analizą jest po prostu nieprawidłowe. chociaż może to zadziałać w niektórych przypadkach, nie powinna to być odpowiedź najczęściej wybierana i nie powinna być używana w profesjonalnej aplikacji.
Mike
1
Pomijając fakt, że użycie wyrażenia regularnego do zadania analizowania XML jest z natury niewłaściwe, nie będzie to działać w przypadku wielu dokumentów XML , ponieważ ignoruje przedrostki przestrzeni nazw oraz fakt, że składnia XML umożliwia stosowanie dowolnych białych znaków przed nazwami atrybutów (nie tylko spacje) i wokół =znaku równości.
Martijn Pieters
Tak, jest szybki i brudny, ale jest to zdecydowanie najbardziej eleganckie rozwiązanie dla prostych przypadków użycia, dzięki!
rimkashox
18

Odpowiedzi do tej pory wyraźnie umieszczały wartość przestrzeni nazw w skrypcie. Aby uzyskać bardziej ogólne rozwiązanie, wolałbym wyodrębnić przestrzeń nazw z xml:

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

I użyj go w metodzie find:

namespace = get_namespace(tree.getroot())
print tree.find('./{0}parent/{0}version'.format(namespace)).text
nikczemny
źródło
15
Zbyt wiele, by zakładać, że jest tylko jedennamespace
Kashyap
Nie uwzględnia to, że zagnieżdżone tagi mogą używać różnych przestrzeni nazw.
Martijn Pieters
15

Oto rozszerzenie odpowiedzi nieokąta, które również usuwa przestrzenie nazw z atrybutów:

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

AKTUALIZACJA: dodano, list()aby iterator działał (wymagany dla Pythona 3)

barny
źródło
14

Poprawa odpowiedzi przez ericspod:

Zamiast globalnej zmiany trybu parsowania, możemy umieścić to w obiekcie obsługującym konstrukcję 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

Można to następnie wykorzystać w następujący sposób

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

Piękno tego sposobu polega na tym, że nie zmienia on żadnego zachowania dla niepowiązanego kodu poza blokiem with. Skończyło się na tym, że utworzyłem to po otrzymaniu błędów w niepowiązanych bibliotekach po użyciu wersji przez ericspod, która również korzystała z expata.

lijat
źródło
To jest słodkie i zdrowe! Uratowałem mój dzień! +1
AndreasT
W Pythonie 3.8 (nie testowałem z innymi wersjami) wydaje mi się, że to nie działa. Patrząc na źródło, powinno działać, ale wydaje się, że kod źródłowy xml.etree.ElementTree.XMLParserjest w jakiś sposób zoptymalizowany, a łatanie małpy expatnie ma absolutnie żadnego efektu.
Reinderien
O tak. Zobacz komentarz @
barny
5

Możesz również użyć eleganckiej konstrukcji formatowania ciągów:

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

lub, jeśli masz pewność, że PAID_OFF pojawia się tylko na jednym poziomie w drzewie:

el2 = tree.findall(".//{%s}PAID_OFF" % ns)
tzp
źródło
2

Jeśli używasz ElementTreei nie cElementTree, możesz zmusić Expat do ignorowania przetwarzania przestrzeni nazw, zastępując ParserCreate():

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

ElementTreepróbuje użyć Expat, wywołując, ParserCreate()ale nie daje opcji, aby nie podawać ciągu separatora przestrzeni nazw, powyższy kod spowoduje zignorowanie go, ale ostrzegamy, że może to zepsuć inne rzeczy.

ericspod
źródło
Jest to lepszy sposób niż inne aktualne odpowiedzi, ponieważ nie zależy od przetwarzania napisów
lijat
3
W Pythonie 3.7.2 (i prawdopodobnie Eariler) AFAICT nie można już uniknąć używania cElementTree, więc to obejście może nie być możliwe :-(
barny
1
cElemTree jest przestarzała, ale tam jest cień rodzajów robione z akceleratorów C . Kod C nie wywołuje expata, więc tak, to rozwiązanie jest zepsute.
ericspod
@barny to nadal możliwe, ElementTree.fromstring(s, parser=None)próbuję przekazać do niego parser.
est
2

Mogę się na to spóźnić, ale nie sądzę, żeby re.subbyło to dobre rozwiązanie.

Jednak przepisywanie xml.parsers.expatnie działa dla wersji Python 3.x,

Głównym winowajcą jest xml/etree/ElementTree.pypatrz dół kodu źródłowego

# 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

Co jest trochę smutne.

Rozwiązaniem jest pozbycie się go najpierw.

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)

Przetestowano w Pythonie 3.6.

tryInstrukcja try jest przydatna w przypadku, gdy gdzieś w kodzie ponownie załadujesz lub zaimportujesz moduł dwukrotnie, otrzymujesz dziwne błędy, takie jak

  • przekroczono maksymalną głębokość rekurencji
  • AttributeError: XMLParser

do cholery, kod źródłowy etree wygląda naprawdę niechlujnie.

est
źródło
1

Połączmy odpowiedź nonagona z odpowiedzią mzjn na powiązane pytanie :

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

Korzystając z tej funkcji:

  1. Utwórz iterator, aby uzyskać zarówno przestrzenie nazw, jak i przeanalizowany obiekt drzewa .

  2. Iteruj po utworzonym iteratorze, aby uzyskać przestrzenie nazw, które możemy później przekazać find()lub findall()wywołać zgodnie z sugestią iMom0 .

  3. Zwraca obiekt elementu głównego i przestrzenie nazw przeanalizowanego drzewa.

Myślę, że jest to najlepsze podejście dookoła, ponieważ nie ma żadnej manipulacji ani źródłowym XML ani wynikowym przeanalizowanym xml.etree.ElementTreewyjściem.

Chciałbym również podziękować odpowiedzi Barny'ego za dostarczenie niezbędnego elementu tej układanki (że można uzyskać przeanalizowany korzeń z iteratora). Do tego czasu dwukrotnie przeszedłem drzewo XML w mojej aplikacji (raz, aby uzyskać przestrzenie nazw, drugi dla katalogu głównego).

z33k
źródło
dowiedziałem się, jak go używać, ale to nie działa, nadal widzę przestrzenie nazw na wyjściu
taiko
1
Spójrz na komentarz iMom0 do pytania OP . Używając tej funkcji, otrzymujesz zarówno analizowany obiekt, jak i środki do wykonywania zapytań za pomocą find()i findall(). Po prostu zasilasz te metody dyktatem przestrzeni nazw z parse_xml()i używasz prefiksu przestrzeni nazw w zapytaniach. Np .:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
z33k