Python ElementTree modülü: “find”, “findall” yöntemini kullanırken eşleşen öğeyi bulmak için XML dosyalarının ad alanını yok sayma


136

ElementTree modülünde kaynak xml dosyasının bazı öğelerini bulmak için "findall" yöntemini kullanmak istiyorum.

Ancak, kaynak xml dosyasının (test.xml) ad alanı vardır. Örnek olarak xml dosyasının bir kısmını kesiyorum:

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

Örnek python kodu aşağıdadır:

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>

Çalışabilmesine rağmen, "{http://www.test.com}" bir ad alanı olduğundan, her etiketin önüne bir ad alanı eklemek çok sakıncalıdır.

"Find", "findall" vb. Yöntemlerini kullanırken ad alanını nasıl göz ardı edebilirim?


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

Çok teşekkürler. Metodunu deniyorum ve işe yarayabilir. Benimkinden daha uygun ama yine de biraz garip. Bu sorunu çözmek için ElementTree modülünde başka uygun bir yöntem olup olmadığını veya hiç böyle bir yöntem olmadığını biliyor musunuz?
KevinLeng

Veya deneyintree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
Warf

Python 3.8'de, ad alanı için bir joker karakter kullanılabilir. stackoverflow.com/a/62117710/407651
mzjn

Yanıtlar:


62

XML belgesinin kendisini değiştirmek yerine, belgeyi ayrıştırmak ve ardından sonuçtaki etiketleri değiştirmek en iyisidir. Bu şekilde birden çok ad alanını ve ad alanı diğer adını işleyebilirsiniz:

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

Bu, şu tartışmaya dayanmaktadır: http://bugs.python.org/issue18304

Güncelleme: rpartition Bunun yerine, ad alanı olmasa bile partitionetiket adını aldığınızdan emin postfixolur. Böylece yoğunlaştırabilirsiniz:

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

2
Bu. Bu bu bu. Birden fazla isim alanı benim ölümüm olacaktı.
Jess

8
Tamam, bu güzel ve daha gelişmiş, ama yine de değil et.findall('{*}sometag'). Ve aynı zamanda eleman ağacının kendisini de idare ediyor, sadece "bu kez isim alanlarını göz ardı ederek aramayı gerçekleştirin, belgeyi yeniden ayrıştırmadan vb., Ad alanı bilgilerini koruyarak" değil. Bu durumda, gözle görülür şekilde ağaçta yinelenmeli ve düğümün ad alanını kaldırdıktan sonra isteklerinizle eşleşip eşleşmediğini kendiniz görmelisiniz.
Tomasz Gandor

1
Bu dizeyi sıyırma ile çalışır ama ben yazma (...) kullanarak XML dosyasını kaydettiğinizde, xmlns = " bla " dissapears XML yalvarıyor ad alanı dissapears. Lütfen tavsiye
TraceKira

@TomaszGandor: Belki de ad alanını ayrı bir niteliğe ekleyebilirsiniz. Basit etiket sınırlama testleri için ( bu belge bu etiket adını içeriyor mu? ) Bu çözüm mükemmeldir ve kısa devre yapılabilir.
Martijn Pieters

@TraceKira: Bu teknik, ayrıştırılmış belgeden ad alanlarını kaldırır ve bunu ad alanlarıyla yeni bir XML dizesi oluşturmak için kullanamazsınız. Ad alanı değerlerini fazladan bir öznitelikte saklayın (ve XML ağacını bir dizeye dönüştürmeden önce ad alanını yeniden yerleştirin) veya soyulmuş ağaca dayalı değişiklikleri uygulamak için orijinal kaynaktan yeniden ayrıştırın.
Martijn Pieters

48

Xmlns özniteliğini ayrıştırmadan önce xml'den kaldırırsanız, ağaçtaki her etikete bir ad alanı eklenmez.

import re

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

5
Bu benim için birçok durumda çalıştı, ama sonra birden çok ad alanı ve ad alanı takma adı ile karşılaştım. Bu vakaları ele alan başka bir yaklaşım için cevabımı görün.
nonagon

47
-1 ayrıştırmadan önce normal bir ifade ile xml üzerinde işlem yapmak sadece yanlıştır. bazı durumlarda işe yarayabilse de, bu en çok oy alan cevap olmamalı ve profesyonel bir uygulamada kullanılmamalıdır.
Mike

1
XML ayrıştırma işi için normal ifade kullanmanın doğası gereği sağlam olmasının yanı sıra, ad alanı öneklerini yok sayar ve XML sözdiziminin özellik adlarından önce rastgele boşluklara izin vermesi nedeniyle pek çok XML belgesi için işe yaramaz. boşluklar) ve =eşittir işareti etrafında .
Martijn Pieters

Evet, hızlı ve kirli, ama basit kullanım durumları için kesinlikle en zarif çözüm, teşekkürler!
rimkashox

18

Şimdiye kadar verilen cevaplar, ad alanı değerini betiğe açıkça koydu. Daha genel bir çözüm için, ad alanını xml'den ayıklamak isterim:

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

Ve find yönteminde kullanın:

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

15
Sadece bir tane olduğunu varsaymak için çok fazlanamespace
Kashyap

Bu, iç içe etiketlerin farklı ad alanları kullanabileceğini dikkate almaz.
Martijn Pieters

15

Aşağıda, ad alanlarını özniteliklerden ayıran nonagon cevabının bir uzantısı verilmiştir:

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

GÜNCELLEME: list()yineleyici çalışması için eklendi (Python 3 için gerekli)


14

Ericspod tarafından cevap üzerinde iyileştirme:

Ayrıştırma modunu global olarak değiştirmek yerine bunu yapıyı destekleyen bir nesneye sarabiliriz.

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

Bu daha sonra aşağıdaki gibi kullanılabilir

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

Bu yolun güzelliği, with bloğunun dışındaki ilgisiz kodlar için herhangi bir davranışı değiştirmemesidir. Ben de expat kullanmak oldu ericspod tarafından sürümü kullandıktan sonra ilgisiz kütüphanelerde hatalar aldıktan sonra bunu yarattı.


Bu tatlı ve sağlıklı! Günümü kurtardım! +1
AndreasT

Python 3.8'de (diğer sürümlerle test etmedim) bu benim için çalışmıyor gibi görünüyor. Kaynağa bakıldığında çalışması gerekir , ancak kaynak kodunun xml.etree.ElementTree.XMLParserbir şekilde optimize edildiği ve maymun yamalarının expatkesinlikle bir etkisi olmadığı görülüyor .
Reinderien

Ah evet. @
Barny'nin yorumuna

5

Zarif dize biçimlendirme yapısını da kullanabilirsiniz:

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

veya PAID_OFF öğesinin ağaçta yalnızca bir düzeyde göründüğünden eminseniz :

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

2

Kullanıyorsanız ElementTreeve kullanmıyorsanız cElementTree, Expat'i değiştirerek ad alanı işlemeyi yok saymaya zorlayabilirsiniz ParserCreate():

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

ElementTreeExpat'ı arayarak kullanmaya çalışır, ParserCreate()ancak bir ad alanı ayırıcı dizesi sağlama seçeneği yoktur, yukarıdaki kod göz ardı edilmesine neden olur, ancak bu diğer şeyleri kırabileceği konusunda uyarılır.


Bu, dize işlemeye bağlı olmadığından diğer güncel cevaplardan daha iyi bir yoldur
lijat

3
Python 3.7.2 (ve muhtemelen daha yeni) AFAICT'de artık cElementTree kullanmaktan kaçınmak mümkün değil, bu nedenle bu geçici çözüm mümkün olmayabilir :-(
barny

1
cElemTree kullanımdan kaldırıldı, ancak C hızlandırıcılarıyla yapılan türlerin gölgelenmesi var . C kodu göçmen çağırmıyor bu yüzden evet bu çözüm bozuk.
ericspod

@barny hala mümkün, ElementTree.fromstring(s, parser=None)ayrıştırıcıyı ona aktarmaya çalışıyorum.
Est

2

Bunun için geç kalabilirim ama sanmıyorum re.sub iyi bir çözüm .

Ancak yeniden yazma xml.parsers.expat işlemi Python 3.x sürümlerinde çalışmaz,

Ana suçlu, xml/etree/ElementTree.pykaynak kodun alt kısmına bakın

# 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

Bu biraz üzücü.

Çözüm önce ondan kurtulmaktır.

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)

Python 3.6 üzerinde test edilmiştir.

Try tryifadesi, kodunuzun herhangi bir yerinde bir modülü iki kez yeniden yüklediğinizde veya içe aktardığınızda,

  • maksimum tekrarlama derinliği aşıldı
  • AttributeError: XMLParser

btw lanet etree kaynak kodu gerçekten dağınık görünüyor.


1

Nonagon'un cevabını mzjn'ın ilgili bir soruya verdiği cevap ile birleştirelim :

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

Bu işlevi kullanarak:

  1. Hem ad alanlarını hem de ayrıştırılmış ağaç nesnesini almak için bir yineleyici oluşturun .

  2. Bıkmadan yineleyici yarattı üzerinde ad daha sonra her geçirebilmesi dict almak find()veya findall()çağrı iMom0 tarafından sugested olarak .

  3. Ayrıştırılan ağacın kök öğesi nesnesini ve ad alanlarını döndürün.

Kaynak XML veya manipüle edilen herhangi bir manipülasyon olmadığından, bu en iyi yaklaşım olduğunu düşünüyorum xml.etree.ElementTree çıktı .

Ayrıca bu bulmacanın önemli bir parçasını (yineleyiciden ayrıştırılmış kök alabilirsiniz) sağlayarak barny'nin cevabını kredi vermek istiyorum . Ben aslında XML ağacı uygulamamda iki kez (bir kez ad alanları almak için, bir kök için ikinci) geçti.


nasıl kullanılacağını öğrendim, ama benim için çalışmıyor, hala çıktıdaki ad alanlarını görüyorum
taiko

1
İMom0'un OP'nin sorusu hakkındaki yorumuna bakın . Bu işlevi kullanarak hem ayrıştırılmış nesneyi hem de onu find()ve ile sorgulama yöntemlerini alırsınız findall(). Bu yöntemleri yalnızca ad alanlarının diktesi ile beslersiniz parse_xml()ve sorgularınızda ad alanının önekini kullanırsınız . Örn:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
z33k
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.