Modul Python ElementTree: Cara mengabaikan namespace file XML untuk menemukan elemen yang cocok saat menggunakan metode "find", "findall"

136

Saya ingin menggunakan metode "findall" untuk menemukan beberapa elemen dari file xml sumber dalam modul ElementTree.

Namun, file xml sumber (test.xml) memiliki namespace. Saya memotong sebagian file xml sebagai sampel:

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

Contoh kode python di bawah ini:

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>

Meskipun bisa berfungsi, karena ada namespace "{http://www.test.com}", sangat tidak nyaman untuk menambahkan namespace di depan setiap tag.

Bagaimana saya bisa mengabaikan namespace ketika menggunakan metode "find", "findall" dan sebagainya?

KevinLeng
sumber
18
Apakah tree.findall("xmlns:DEAL_LEVEL/xmlns:PAID_OFF", namespaces={'xmlns': 'http://www.test.com'})cukup nyaman?
iMom0
Terima kasih banyak. Saya mencoba metode Anda dan itu bisa berhasil. Ini lebih nyaman daripada milikku tapi masih agak canggung. Apakah Anda tahu jika tidak ada metode lain yang tepat dalam modul ElementTree untuk menyelesaikan masalah ini atau tidak ada metode seperti itu sama sekali?
KevinLeng
Atau cobatree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
Warf
Dalam Python 3.8, wildcard dapat digunakan untuk namespace. stackoverflow.com/a/62117710/407651
mzjn

Jawaban:

62

Alih-alih memodifikasi dokumen XML itu sendiri, yang terbaik adalah menguraikannya dan kemudian memodifikasi tag di hasilnya. Dengan cara ini Anda dapat menangani beberapa ruang nama dan alias namespace:

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

Ini didasarkan pada diskusi di sini: http://bugs.python.org/issue18304

Pembaruan: rpartition alih-alih partitionmemastikan Anda mendapatkan nama tag postfixmeskipun tidak ada namespace. Dengan demikian Anda bisa menyingkatnya:

for _, el in it:
    _, _, el.tag = el.tag.rpartition('}') # strip ns
nonagon
sumber
2
Ini. Ini ini ini. Banyak ruang nama akan menjadi kematian saya.
Jess
8
OKE, ini bagus dan lebih maju, tapi tetap saja tidak et.findall('{*}sometag'). Dan itu juga merusak pohon elemen itu sendiri, bukan hanya "melakukan pencarian mengabaikan ruang nama saja, tanpa mem-parsing ulang dokumen dll, mempertahankan informasi namespace". Nah, untuk kasus ini Anda perlu mengamati untuk beralih melalui pohon, dan lihat sendiri, jika simpul cocok dengan keinginan Anda setelah menghapus namespace.
Tomasz Gandor
1
Ini berfungsi dengan menghilangkan senar tetapi ketika saya menyimpan file XML menggunakan write (...) namespace menghilang dari memohon XML xmlns = " bla " menghilang. Mohon saran
TraceKira
@TomaszGandor: Anda bisa menambahkan namespace ke atribut terpisah, mungkin. Untuk tes penahanan tag sederhana ( apakah dokumen ini mengandung nama tag ini? ) Solusi ini bagus dan dapat dihubung pendek.
Martijn Pieters
@ Trakira: teknik ini menghapus ruang nama dari dokumen yang diuraikan, dan Anda tidak bisa menggunakannya untuk membuat string XML baru dengan ruang nama. Baik menyimpan nilai namespace dalam atribut tambahan (dan masukkan namespace kembali sebelum mengubah pohon XML kembali menjadi string) atau parsing ulang dari sumber asli untuk menerapkan perubahan yang didasarkan pada pohon yang dilucuti.
Martijn Pieters
48

Jika Anda menghapus atribut xmlns dari xml sebelum menguraikannya maka tidak akan ada namespace yang ditambahkan ke setiap tag di pohon.

import re

xmlstring = re.sub(' xmlns="[^"]+"', '', xmlstring, count=1)
pengguna2212280
sumber
5
Ini bekerja dalam banyak kasus untuk saya, tetapi kemudian saya mengalami beberapa ruang nama dan alias namespace. Lihat jawaban saya untuk pendekatan lain yang menangani kasus-kasus ini.
nonagon
47
-1 memanipulasi xml melalui ekspresi reguler sebelum parsing salah. meskipun mungkin berhasil dalam beberapa kasus, ini seharusnya tidak menjadi jawaban pilihan utama dan tidak boleh digunakan dalam aplikasi profesional.
Mike
1
Terlepas dari kenyataan bahwa menggunakan regex untuk pekerjaan parsing XML secara inheren tidak sehat, ini tidak akan berfungsi untuk banyak dokumen XML , karena mengabaikan awalan namespace, dan fakta bahwa sintaks XML memungkinkan spasi kosong sebelum spasi nama atribut (bukan hanya spasi) dan di sekitar =tanda sama dengan.
Martijn Pieters
Ya, ini cepat dan kotor, tapi ini jelas merupakan solusi paling elegan untuk kasing sederhana, terima kasih!
rimkashox
18

Jawaban sejauh ini secara eksplisit menempatkan nilai namespace dalam skrip. Untuk solusi yang lebih umum, saya lebih suka mengekstrak namespace dari xml:

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

Dan gunakan dalam metode find:

namespace = get_namespace(tree.getroot())
print tree.find('./{0}parent/{0}version'.format(namespace)).text
Wimous
sumber
15
Terlalu banyak untuk berasumsi bahwa hanya ada satunamespace
Kashyap
Ini tidak memperhitungkan bahwa tag bersarang dapat menggunakan ruang nama yang berbeda.
Martijn Pieters
15

Berikut ini adalah ekstensi untuk jawaban nonagon, yang juga menghilangkan atribut namespaces dari:

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

UPDATE: ditambahkan list()agar iterator berfungsi (diperlukan untuk Python 3)

barny
sumber
14

Memperbaiki jawaban oleh ericspod:

Alih-alih mengubah mode parse secara global, kita dapat membungkusnya dalam objek yang mendukung konstruksi.

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

Ini kemudian dapat digunakan sebagai berikut

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

Keindahan cara ini adalah tidak mengubah perilaku apa pun untuk kode yang tidak terkait di luar blok with. Saya akhirnya membuat ini setelah mendapatkan kesalahan di perpustakaan yang tidak terkait setelah menggunakan versi oleh ericspod yang juga kebetulan menggunakan expat.

lijat
sumber
Ini manis dan sehat! Selamatkan hari saya! +1
AndreasT
Dalam Python 3.8 (belum diuji dengan versi lain) ini sepertinya tidak berfungsi untuk saya. Melihat sumber itu harusnya berfungsi, tetapi tampaknya kode sumbernya xml.etree.ElementTree.XMLParserentah bagaimana dioptimalkan dan monyet-patching expatsama sekali tidak berpengaruh.
Reinderien
Ah ya Lihat komentar @ barny: stackoverflow.com/questions/13412496/…
Reinderien
5

Anda dapat menggunakan konstruksi pemformatan string yang elegan juga:

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

atau, jika Anda yakin bahwa PAID_OFF hanya muncul di satu tingkat di hierarki:

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

Jika Anda menggunakan ElementTreedan tidak, cElementTreeAnda dapat memaksa Expat untuk mengabaikan pemrosesan namespace dengan mengganti ParserCreate():

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

ElementTreemencoba menggunakan Expat dengan memanggil ParserCreate()tetapi tidak memberikan opsi untuk tidak memberikan string pemisah namespace, kode di atas akan menyebabkannya diabaikan tetapi diperingatkan ini dapat merusak hal lain.

ericspod
sumber
Ini adalah cara yang lebih baik daripada jawaban saat ini karena tidak tergantung pada pemrosesan string
lijat
3
Dalam python 3.7.2 (dan mungkin eariler) AFAICT tidak lagi mungkin untuk menghindari menggunakan cElementTree, jadi solusi ini tidak mungkin :-(
barny
1
cElemTree sudah ditinggalkan tetapi ada membayangi dari jenis yang dilakukan dengan C akselerator . Kode C tidak memanggil ekspat jadi ya solusi ini rusak.
ericspod
@ Barny itu masih mungkin, ElementTree.fromstring(s, parser=None)saya mencoba mengoper parser untuk itu.
Est
2

Saya mungkin terlambat untuk ini, tetapi saya rasa re.subitu bukan solusi yang baik.

Namun penulisan ulang xml.parsers.expattidak berfungsi untuk versi Python 3.x,

Penyebab utamanya adalah xml/etree/ElementTree.pylihat di bagian bawah kode sumber

# 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

Yang agak menyedihkan.

Solusinya adalah dengan menyingkirkannya terlebih dahulu.

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)

Diuji dengan Python 3.6.

tryPernyataan coba berguna jika suatu saat dalam kode Anda Anda memuat ulang atau mengimpor modul dua kali Anda mendapatkan beberapa kesalahan aneh seperti

  • kedalaman rekursi maksimum terlampaui
  • AttributeError: XMLParser

Sialan kode sumber etree terlihat sangat berantakan.

Est
sumber
1

Mari kita gabungkan jawaban nonagon dengan jawaban mzjn untuk pertanyaan terkait :

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

Menggunakan fungsi ini kita:

  1. Buat iterator untuk mendapatkan ruang nama dan objek pohon yang diuraikan .

  2. Iterate atas iterator yang dibuat untuk mendapatkan dict namespaces yang nantinya bisa kita lewati di masing find()- masing atau findall()dipanggil sebagai disarankan oleh iMom0 .

  3. Kembalikan objek elemen root root dan ruang nama elemen.

Saya pikir ini adalah pendekatan terbaik di sekitar karena tidak ada manipulasi baik dari sumber XML atau hasil xml.etree.ElementTreekeluaran yang diuraikan apa pun yang terlibat.

Saya juga ingin menghargai jawaban barny dengan memberikan bagian penting dari teka-teki ini (bahwa Anda bisa mendapatkan root yang diurai dari iterator). Sampai saya benar-benar melintasi pohon XML dua kali di aplikasi saya (satu kali untuk mendapatkan ruang nama, kedua untuk root).

z33k
sumber
menemukan cara menggunakannya, tetapi tidak berfungsi untuk saya, saya masih melihat namespaces di output
taiko
1
Lihatlah komentar iMom0 untuk pertanyaan OP . Dengan menggunakan fungsi ini Anda mendapatkan objek parsing dan sarana untuk menanyakannya dengan find()dan findall(). Anda cukup memberi makan metode tersebut dengan dict namespaces dari parse_xml()dan menggunakan awalan namespace di kueri Anda. Misalnya:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
z33k