Parsowanie XML z przestrzenią nazw w Pythonie za pośrednictwem „ElementTree”

163

Mam następujący kod XML, który chcę przeanalizować za pomocą Pythona 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>

Chcę znaleźć wszystkie owl:Classtagi, a następnie wyodrębnić wartość wszystkich rdfs:labelwystąpień w nich. Używam następującego kodu:

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

Z powodu przestrzeni nazw pojawia się następujący błąd.

SyntaxError: prefix 'owl' not found in prefix map

Próbowałem czytać dokument pod adresem http://effbot.org/zone/element-namespaces.htm, ale nadal nie mogę tego uruchomić, ponieważ powyższy kod XML ma wiele zagnieżdżonych przestrzeni nazw.

Uprzejmie daj mi znać, jak zmienić kod, aby znaleźć wszystkie owl:Classtagi.

Sudar
źródło

Odpowiedzi:

226

ElementTree nie jest zbyt inteligentny, jeśli chodzi o przestrzenie nazw. Trzeba dać .find(), findall()i iterfind()Metody wyraźne słownika przestrzeni nazw. Nie jest to dobrze udokumentowane:

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

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

Prefiksy są wyszukiwane tylko w przekazanym namespacesparametrze. Oznacza to, że możesz użyć dowolnego prefiksu przestrzeni nazw; API oddziela owl:część, wyszukuje odpowiedni adres URL przestrzeni nazw w namespacessłowniku, a następnie zmienia wyszukiwanie w celu wyszukania wyrażenia XPath {http://www.w3.org/2002/07/owl}Class. Oczywiście możesz użyć tej samej składni:

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

Jeśli możesz przełączyć się do lxmlbiblioteki , będzie lepiej; ta biblioteka obsługuje ten sam interfejs API ElementTree, ale zbiera dla Ciebie przestrzenie nazw w .nsmapatrybucie elementów.

Martijn Pieters
źródło
7
Dziękuję Ci. Masz jakiś pomysł, jak mogę uzyskać przestrzeń nazw bezpośrednio z XML, bez kodowania go na stałe? Albo jak mogę to zignorować? Próbowałem znaleźć wszystkie („{*} Class”), ale w moim przypadku to nie zadziała.
Kostanos
7
Musisz sam przeskanować drzewo w poszukiwaniu xmlnsatrybutów; jak podano w odpowiedzi, lxmlrobi to za Ciebie, xml.etree.ElementTreemoduł nie. Ale jeśli próbujesz dopasować określony (już zakodowany) element, próbujesz również dopasować określony element w określonej przestrzeni nazw. Ta przestrzeń nazw nie zmieni się między dokumentami bardziej niż nazwa elementu. Równie dobrze możesz zakodować to na stałe z nazwą elementu.
Martijn Pieters
14
@Jon: register_namespacewpływa tylko na serializację, a nie na wyszukiwanie.
Martijn Pieters
5
Mały dodatek, który może być przydatny: podczas używania cElementTreezamiast ElementTree, findallnie będzie traktować przestrzeni nazw jako argumentu słowa kluczowego, ale raczej jako zwykły argument, tj ctree.findall('owl:Class', namespaces). Use .
egpbos
2
@Bludwarf: Dokumenty wspominają o tym (teraz, jeśli nie wtedy, gdy to pisałeś), ale musisz je uważnie przeczytać bardzo uważnie. Zobacz sekcję Parsowanie XML z przestrzeniami nazw : istnieje przykład porównujący użycie argumentu findallbez, a następnie z namespaceargumentem, ale argument nie jest wymieniony jako jeden z argumentów metody metody w sekcji obiektu Element .
Wilson F
57

Oto jak to zrobić za pomocą lxml bez konieczności kodowania przestrzeni nazw lub skanowania ich tekstu (jak wspomina Martijn Pieters):

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

AKTUALIZACJA :

5 lat później wciąż napotykam różne odmiany tego problemu. lxml pomaga, jak pokazałem powyżej, ale nie w każdym przypadku. Komentatorzy mogą mieć rację co do tej techniki, jeśli chodzi o scalanie dokumentów, ale myślę, że większość ludzi ma trudności z prostym wyszukiwaniem dokumentów.

Oto inny przypadek i jak sobie z tym poradziłem:

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

xmlns bez prefiksu oznacza, że ​​tagi bez prefiksu otrzymują tę domyślną przestrzeń nazw. Oznacza to, że kiedy szukasz Tag2, musisz uwzględnić przestrzeń nazw, aby ją znaleźć. Jednak lxml tworzy wpis nsmap z None jako kluczem i nie mogłem znaleźć sposobu, aby go wyszukać. Dlatego utworzyłem nowy słownik przestrzeni nazw, taki jak ten

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
źródło
3
Pełny adres URL przestrzeni nazw to identyfikator przestrzeni nazw, który ma zostać zakodowany na stałe . Lokalny prefiks ( owl) może zmieniać się z pliku na plik. Dlatego robienie tego, co sugeruje ta odpowiedź, jest naprawdę złym pomysłem.
Matti Virkkunen
1
@MattiVirkkunen dokładnie, jeśli definicja sowy może się zmieniać z pliku do pliku, czy nie powinniśmy używać definicji zdefiniowanej w każdym pliku zamiast zakodować ją na stałe?
Loïc Faure-Lacroix
@ LoïcFaure-Lacroix: Zazwyczaj biblioteki XML pozwalają na wyodrębnienie tej części. Nie musisz nawet znać przedrostka używanego w samym pliku ani dbać o niego, po prostu definiujesz swój własny prefiks na potrzeby analizy lub po prostu używasz pełnej nazwy przestrzeni nazw.
Matti Virkkunen
ta odpowiedź pomogła mi przynajmniej w użyciu funkcji wyszukiwania. Nie ma potrzeby tworzenia własnego prefiksu. Właśnie zrobiłem key = list (root.nsmap.keys ()) [0], a następnie dodałem klucz jako prefiks: root.find (f '{key}: Tag2', root.nsmap)
Eelco van Vliet
30

Uwaga : jest to odpowiedź przydatna w przypadku standardowej biblioteki Pythona ElementTree bez używania zakodowanych na stałe przestrzeni nazw.

Aby wyodrębnić prefiksy i URI przestrzeni nazw z danych XML, możesz użyć ElementTree.iterparsefunkcji, analizując tylko zdarzenia początkowe przestrzeni nazw ( 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#'}

Następnie słownik można przekazać jako argument do funkcji wyszukiwania:

root.findall('owl:Class', my_namespaces)
Davide Brunato
źródło
1
Jest to przydatne dla tych z nas, którzy nie mają dostępu do lxml i nie chcą używać na stałe przestrzeni nazw.
delrocco
1
Otrzymałem błąd: ValueError: write to closeddla tej linii filemy_namespaces = dict([node for _, node in ET.iterparse(StringIO(my_schema), events=['start-ns'])]). Każdy pomysł chce źle?
Yuli,
Prawdopodobnie błąd jest związany z klasą io.StringIO, która odmawia ciągów ASCII. Przetestowałem mój przepis w Pythonie3. Dodanie przedrostka „u” do przykładowego ciągu znaków Unicode działa również z Pythonem 2 (2.7).
Davide Brunato,
Zamiast dict([...])ciebie możesz też użyć dyktowania ze zrozumieniem.
Arminius
Zamiast StringIO(my_schema)ciebie możesz też podać nazwę pliku XML.
JustAC0der
6

Używałem podobnego kodu do tego i stwierdziłem, że zawsze warto przeczytać dokumentację ... jak zwykle!

findall () znajdzie tylko elementy, które są bezpośrednimi dziećmi bieżącego znacznika . Więc nie do końca WSZYSTKIE.

Może warto spróbować, aby Twój kod działał z następującymi elementami, zwłaszcza jeśli masz do czynienia z dużymi i złożonymi plikami xml, tak aby uwzględnione były również elementy podrzędne (itp.). Jeśli wiesz, gdzie znajdują się elementy w Twoim xml, to chyba będzie dobrze! Pomyślałem, że warto o tym pamiętać.

root.iter()

ref: https://docs.python.org/3/library/xml.etree.elementtree.html#finding-interesting-elements "Element.findall () wyszukuje tylko elementy z tagiem, które są bezpośrednimi potomkami bieżącego elementu. Element.find () znajduje pierwsze dziecko z określonym znacznikiem, a Element.text uzyskuje dostęp do zawartości tekstowej elementu. Element.get () uzyskuje dostęp do atrybutów elementu: "

MJM
źródło
6

Aby uzyskać przestrzeń nazw w formacie przestrzeni nazw {myNameSpace}, możesz na przykład wykonać następujące czynności:

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

W ten sposób możesz użyć go później w kodzie, aby znaleźć węzły, np. Za pomocą interpolacji ciągów (Python 3).

link = root.find(f"{ns}link")
Bram Vanroy
źródło
0

Moje rozwiązanie bazuje na komentarzu @Martijn Pieters:

register_namespace wpływa tylko na serializację, a nie na wyszukiwanie.

Tak więc sztuczka polega na użyciu różnych słowników do serializacji i wyszukiwania.

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

Teraz zarejestruj wszystkie przestrzenie nazw do analizowania i pisania:

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

Do przeszukiwania ( find(), findall(), iterfind()) musimy niepusty prefiks. Przekaż tym funkcjom zmodyfikowany słownik (tutaj modyfikuję oryginalny słownik, ale trzeba to zrobić dopiero po zarejestrowaniu przestrzeni nazw).

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

Teraz funkcje z find()rodziny mogą być używane z defaultprzedrostkiem:

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

ale

tree.write(destination)

nie używa żadnych przedrostków dla elementów w domyślnej przestrzeni nazw.

peter.slizik
źródło