Parsing XML dengan namespace dalam Python melalui 'ElementTree'

163

Saya memiliki XML berikut yang ingin saya parsing menggunakan 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>

Saya ingin menemukan semua owl:Classtag lalu mengekstraksi nilai semua rdfs:labelinstance di dalamnya. Saya menggunakan kode berikut:

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

Karena namespace, saya mendapatkan kesalahan berikut.

SyntaxError: prefix 'owl' not found in prefix map

Saya mencoba membaca dokumen di http://effbot.org/zone/element-namespaces.htm tapi saya masih tidak bisa membuatnya berfungsi karena XML di atas memiliki beberapa ruang nama bersarang.

Mohon beri tahu saya cara mengubah kode untuk menemukan semua owl:Classtag.

Sudar
sumber

Jawaban:

226

ElementTree tidak terlalu pintar tentang ruang nama. Anda perlu memberikan .find(), findall()dan iterfind()metode kamus namespace eksplisit. Ini tidak didokumentasikan dengan baik:

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

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

Awalan hanya tampak di namespacesparameter yang Anda lewati. Ini berarti Anda dapat menggunakan awalan namespace yang Anda suka; API memisahkan owl:bagian, mencari URL namespace yang sesuai di namespaceskamus, kemudian mengubah pencarian untuk mencari ekspresi XPath {http://www.w3.org/2002/07/owl}Class. Anda juga dapat menggunakan sintaks yang sama:

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

Jika Anda dapat beralih ke lxmlperpustakaan, semuanya lebih baik; pustaka yang mendukung API ElementTree yang sama, tetapi mengumpulkan ruang nama untuk Anda dalam .nsmapatribut pada elemen.

Martijn Pieters
sumber
7
Terima kasih. Tahu bagaimana saya bisa mendapatkan namespace langsung dari XML, tanpa hard-coding? Atau bagaimana saya bisa mengabaikannya? Saya sudah mencoba findall ('{*} Class') tetapi tidak akan berhasil dalam kasus saya.
Kostanos
7
Anda harus memindai pohon untuk xmlnsatribut sendiri; seperti yang dinyatakan dalam jawaban, lxmlapakah ini untuk Anda, xml.etree.ElementTreemodul tidak. Tetapi jika Anda mencoba untuk mencocokkan elemen tertentu (sudah hardcod), maka Anda juga mencoba untuk mencocokkan elemen tertentu dalam namespace tertentu. Namespace itu tidak akan berubah di antara dokumen seperti halnya nama elemen. Anda juga dapat melakukan hardcode dengan nama elemen.
Martijn Pieters
14
@ Jon: register_namespacehanya memengaruhi serialisasi, bukan pencarian.
Martijn Pieters
5
Tambahan kecil yang mungkin berguna: saat menggunakan cElementTreealih-alih ElementTree, findalltidak akan menggunakan spasi nama sebagai argumen kata kunci, melainkan sebagai argumen normal, yaitu penggunaan ctree.findall('owl:Class', namespaces).
egpbos
2
@Bludwarf: Dokumen memang menyebutkannya (sekarang, jika tidak ketika Anda menulis itu), tetapi Anda harus membacanya dengan seksama. Lihat bagian Parsing XML with Namespaces : ada contoh yang membedakan penggunaan findalltanpa dan kemudian dengan namespaceargumen, tetapi argumen tidak disebutkan sebagai salah satu argumen untuk metode metode di bagian objek Elemen .
Wilson F
57

Berikut ini cara melakukannya dengan lxml tanpa harus membuat hard-namespaces atau memindai teks untuknya (seperti yang disebutkan Martijn Pieters):

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

PEMBARUAN :

5 tahun kemudian saya masih mengalami variasi masalah ini. lxml membantu seperti yang saya tunjukkan di atas, tetapi tidak dalam setiap kasus. Para komentator mungkin memiliki poin yang valid mengenai teknik ini ketika menggabungkan dokumen, tetapi saya pikir kebanyakan orang mengalami kesulitan hanya dengan mencari dokumen.

Ini kasus lain dan bagaimana saya menanganinya:

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

xmlns tanpa awalan berarti bahwa tag yang tidak diperbaiki mendapatkan namespace default ini. Ini berarti ketika Anda mencari Tag2, Anda harus memasukkan namespace untuk menemukannya. Namun, lxml membuat entri nsmap dengan None sebagai kuncinya, dan saya tidak dapat menemukan cara untuk mencarinya. Jadi, saya membuat kamus namespace baru seperti ini

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
sumber
3
URL namespace lengkap adalah pengidentifikasi namespace yang seharusnya Anda hard-code. Awalan lokal ( owl) dapat berubah dari file ke file. Oleh karena itu melakukan apa yang disarankan jawaban ini adalah ide yang sangat buruk.
Matti Virkkunen
1
@MattiVirkkunen tepat jika definisi owl dapat berubah dari file ke file, bukankah kita harus menggunakan definisi yang didefinisikan dalam setiap file alih-alih hardcoding?
Loïc Faure-Lacroix
@ LoïcFaure-Lacroix: Biasanya pustaka XML akan membiarkan Anda abstrak bagian itu. Anda bahkan tidak perlu tahu atau peduli tentang awalan yang digunakan dalam file itu sendiri, Anda cukup mendefinisikan awalan Anda sendiri untuk tujuan parsing atau hanya menggunakan nama namespace lengkap.
Matti Virkkunen
jawaban ini membantu saya untuk setidaknya dapat menggunakan fungsi find. Tidak perlu membuat awalan Anda sendiri. Saya baru saja melakukan key = list (root.nsmap.keys ()) [0] dan kemudian menambahkan kunci sebagai awalan: root.find (f '{key}: Tag2', root.nsmap)
Eelco van Vliet
30

Catatan : Ini adalah jawaban yang berguna untuk perpustakaan standar ElementTree Python tanpa menggunakan ruang nama yang di-hardcode.

Untuk mengekstrak awalan namespace dan URI dari data XML Anda dapat menggunakan ElementTree.iterparsefungsi, hanya mem-parsing peristiwa mulai namespace ( 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#'}

Kemudian kamus dapat diteruskan sebagai argumen ke fungsi pencarian:

root.findall('owl:Class', my_namespaces)
Davide Brunato
sumber
1
Ini berguna bagi kita yang tidak memiliki akses ke lxml dan tanpa ingin namespace hardcode.
delrocco
1
Saya mendapat kesalahan: ValueError: write to closeduntuk baris ini filemy_namespaces = dict([node for _, node in ET.iterparse(StringIO(my_schema), events=['start-ns'])]). Ada ide yang salah?
Yuli
Mungkin kesalahan terkait dengan kelas io.StringIO, yang menolak string ASCII. Saya telah menguji resep saya dengan Python3. Menambahkan awalan string unicode 'u' ke string sampel berfungsi juga dengan Python 2 (2.7).
Davide Brunato
Alih-alih dict([...])Anda juga dapat menggunakan pemahaman dict.
Arminius
Alih-alih StringIO(my_schema)Anda juga bisa meletakkan nama file file XML.
JustAC0der
6

Saya telah menggunakan kode yang mirip dengan ini dan telah menemukan selalu layak membaca dokumentasi ... seperti biasa!

findall () hanya akan menemukan elemen yang merupakan anak-anak langsung dari tag saat ini . Jadi, tidak benar-benar SEMUA.

Mungkin bernilai saat Anda mencoba agar kode Anda berfungsi dengan yang berikut, terutama jika Anda berurusan dengan file xml yang besar dan kompleks sehingga sub-elemen (dll) juga disertakan. Jika Anda tahu sendiri di mana elemen berada di xml Anda, maka saya kira itu akan baik-baik saja! Hanya berpikir ini layak diingat.

root.iter()

ref: https://docs.python.org/3/library/xml.etree.elementtree.html#finding-interesting-elements "Element.findall () menemukan hanya elemen dengan tag yang merupakan anak-anak langsung dari elemen saat ini. Element.find () menemukan anak pertama dengan tag tertentu, dan Element.text mengakses konten teks elemen. Element.get () mengakses atribut elemen: "

MJM
sumber
6

Untuk mendapatkan namespace dalam format namespace, misalnya {myNameSpace}, Anda dapat melakukan hal berikut:

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

Dengan cara ini, Anda dapat menggunakannya nanti dalam kode Anda untuk menemukan node, misalnya menggunakan interpolasi string (Python 3).

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

Solusi saya didasarkan pada komentar @Martijn Pieters ':

register_namespace hanya memengaruhi serialisasi, bukan pencarian.

Jadi triknya di sini adalah menggunakan kamus yang berbeda untuk serialisasi dan untuk pencarian.

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

Sekarang, daftarkan semua ruang nama untuk parsing dan menulis:

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

Untuk mencari ( find(), findall(), iterfind()) kita perlu awalan non-kosong. Lulus fungsi-fungsi ini kamus yang dimodifikasi (di sini saya memodifikasi kamus asli, tetapi ini harus dibuat hanya setelah ruang nama terdaftar).

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

Sekarang, fungsi-fungsi dari find()keluarga dapat digunakan dengan defaultawalan:

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

tapi

tree.write(destination)

tidak menggunakan awalan untuk elemen di namespace default.

peter.slizik
sumber