โมดูล Python ElementTree: วิธีละเว้นเนมสเปซของไฟล์ XML เพื่อค้นหาองค์ประกอบที่ตรงกันเมื่อใช้เมธอด "find", "findall"


140

ฉันต้องการใช้เมธอด "findall" เพื่อค้นหาองค์ประกอบบางอย่างของไฟล์ xml ต้นทางในโมดูล ElementTree

อย่างไรก็ตามไฟล์ xml ต้นทาง (test.xml) มีเนมสเปซ ฉันตัดส่วนของไฟล์ xml เป็นตัวอย่าง:

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

โค้ด python ตัวอย่างอยู่ด้านล่าง:

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>

แม้ว่าจะสามารถใช้งานได้เนื่องจากมีเนมสเปซ "{http://www.test.com}" แต่การเพิ่มเนมสเปซด้านหน้าแต่ละแท็กจึงไม่สะดวก

ฉันจะเพิกเฉยต่อเนมสเปซได้อย่างไรเมื่อใช้เมธอด "find" "findall" และอื่น ๆ


18
คือtree.findall("xmlns:DEAL_LEVEL/xmlns:PAID_OFF", namespaces={'xmlns': 'http://www.test.com'})พอสะดวก?
iMom0

ขอบคุณมาก ๆ. ฉันลองใช้วิธีของคุณแล้วมันได้ผล สะดวกกว่าของฉัน แต่ก็ยังอึดอัดเล็กน้อย คุณรู้หรือไม่ว่าไม่มีวิธีการอื่นที่เหมาะสมในโมดูล ElementTree เพื่อแก้ปัญหานี้หรือไม่มีวิธีการดังกล่าวเลย?
KevinLeng

หรือลองtree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
Warf

ใน Python 3.8 สามารถใช้สัญลักษณ์แทนสำหรับเนมสเปซได้ stackoverflow.com/a/62117710/407651
mzjn

คำตอบ:


63

แทนที่จะแก้ไขเอกสาร XML เองควรแยกวิเคราะห์แล้วแก้ไขแท็กในผลลัพธ์ ด้วยวิธีนี้คุณสามารถจัดการหลายเนมสเปซและนามแฝงเนมสเปซ:

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

อ้างอิงจากการสนทนาที่นี่: http://bugs.python.org/issue18304

อัปเดต: rpartitionแทนที่จะpartitionตรวจสอบว่าคุณได้รับชื่อแท็กpostfixแม้ว่าจะไม่มีเนมสเปซก็ตาม ดังนั้นคุณสามารถย่อ:

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

3
นี้. นี้นี้นี้ การเว้นวรรคชื่อหลายชื่อจะทำให้ฉันตาย
Jess

8
ตกลงนี้เป็นสิ่งที่ดีและทันสมัยมากขึ้น et.findall('{*}sometag')แต่ก็ยังไม่ได้ และมันยังทำให้ต้นไม้องค์ประกอบยุ่งเหยิงด้วยไม่ใช่แค่ "ทำการค้นหาโดยไม่สนใจเนมสเปซในครั้งนี้โดยไม่ต้องแยกวิเคราะห์เอกสารซ้ำ ฯลฯ โดยยังคงรักษาข้อมูลเนมสเปซไว้" สำหรับกรณีนั้นคุณจะต้องวนซ้ำตามต้นไม้และดูด้วยตัวคุณเองว่าโหนดตรงกับความต้องการของคุณหรือไม่หลังจากลบเนมสเปซ
Tomasz Gandor

1
ซึ่งทำงานได้โดยการแยกสตริง แต่เมื่อฉันบันทึกไฟล์ XML โดยใช้ write (... ) เนมสเปซจะหายไปจากการขอ XML xmlns = " bla " หายไป กรุณาแนะนำ
TraceKira

@TomaszGandor: คุณสามารถเพิ่มเนมสเปซลงในแอตทริบิวต์แยกต่างหากได้ สำหรับการทดสอบการบรรจุแท็กอย่างง่าย ( เอกสารนี้มีชื่อแท็กนี้หรือไม่ ) วิธีนี้ดีมากและสามารถลัดวงจรได้
Martijn Pieters

@TraceKira: เทคนิคนี้จะลบเนมสเปซออกจากเอกสารที่แยกวิเคราะห์และคุณไม่สามารถใช้เพื่อสร้างสตริง XML ใหม่ด้วยเนมสเปซ จัดเก็บค่าเนมสเปซในแอตทริบิวต์เพิ่มเติม (และใส่เนมสเปซกลับเข้าไปก่อนที่จะเปลี่ยนโครงสร้าง XML กลับเป็นสตริง) หรือแยกวิเคราะห์ใหม่จากแหล่งที่มาเดิมเพื่อใช้การเปลี่ยนแปลงกับสิ่งนั้นโดยยึดตามโครงสร้างที่ถูกปล้น
Martijn Pieters

46

หากคุณลบแอตทริบิวต์ xmlns ออกจาก xml ก่อนที่จะแยกวิเคราะห์จะไม่มีเนมสเปซนำหน้าแต่ละแท็กในโครงสร้าง

import re

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

5
สิ่งนี้ใช้ได้ผลในหลาย ๆ กรณีสำหรับฉัน แต่แล้วฉันก็พบกับหลายเนมสเปซและนามแฝงเนมสเปซ ดูคำตอบของฉันสำหรับแนวทางอื่นที่จัดการกับกรณีเหล่านี้
nonagon

49
-1 การจัดการ xml ผ่านนิพจน์ทั่วไปก่อนการแยกวิเคราะห์จะผิด แม้ว่าอาจได้ผลในบางกรณี แต่ก็ไม่ควรเป็นคำตอบที่ได้รับการโหวตสูงสุดและไม่ควรใช้ในแอปพลิเคชันระดับมืออาชีพ
Mike

1
นอกเหนือจากข้อเท็จจริงที่ว่าการใช้ regex สำหรับงานแยกวิเคราะห์ XML นั้นไม่เป็นผลโดยเนื้อแท้แล้วสิ่งนี้จะไม่ได้ผลกับเอกสาร XML จำนวนมากเนื่องจากไม่สนใจคำนำหน้าเนมสเปซและความจริงที่ว่าไวยากรณ์ XML อนุญาตให้มีช่องว่างโดยพลการก่อนชื่อแอตทริบิวต์ (ไม่ใช่แค่ ช่องว่าง) และรอบ ๆ=เครื่องหมายเท่ากับ
Martijn Pieters

ใช่มันเร็วและสกปรก แต่มันเป็นทางออกที่ดีที่สุดสำหรับกรณีการใช้งานที่เรียบง่ายขอบคุณ!
rimkashox

19

คำตอบจนถึงตอนนี้ใส่ค่าเนมสเปซในสคริปต์อย่างชัดเจน สำหรับวิธีแก้ปัญหาทั่วไปฉันอยากจะแยกเนมสเปซจาก xml:

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

และใช้ในวิธีการค้นหา:

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

15
มากเกินไปที่จะคิดว่ามีเพียงหนึ่งเดียวnamespace
Kashyap

สิ่งนี้ไม่ได้คำนึงถึงว่าแท็กที่ซ้อนกันสามารถใช้เนมสเปซที่แตกต่างกันได้
Martijn Pieters

15

นี่คือส่วนขยายของคำตอบของ nonagon ซึ่งจะทำให้เนมสเปซปิดคุณสมบัติ:

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: เพิ่มlist()เพื่อให้ตัววนซ้ำทำงานได้ (จำเป็นสำหรับ Python 3)


14

การปรับปรุงคำตอบโดย ericspod:

แทนที่จะเปลี่ยนโหมดแยกวิเคราะห์ทั่วโลกเราสามารถรวมสิ่งนี้ไว้ในวัตถุที่สนับสนุนโครงสร้างด้วย

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

จากนั้นสามารถใช้งานได้ดังนี้

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

ความสวยงามของวิธีนี้คือการไม่เปลี่ยนแปลงพฤติกรรมใด ๆ สำหรับรหัสที่ไม่เกี่ยวข้องนอกบล็อกด้วย ฉันลงเอยด้วยการสร้างสิ่งนี้หลังจากได้รับข้อผิดพลาดในไลบรารีที่ไม่เกี่ยวข้องหลังจากใช้เวอร์ชันโดย ericspod ซึ่งเกิดขึ้นกับการใช้ expat


หวานและดีต่อสุขภาพ! บันทึกวันของฉัน! +1
AndreasT

ใน Python 3.8 (ยังไม่ได้ทดสอบกับเวอร์ชันอื่น) สิ่งนี้ดูเหมือนจะไม่ได้ผลสำหรับฉัน ดูที่แหล่งที่มามันควรจะใช้งานได้ แต่ดูเหมือนว่าซอร์สโค้ดสำหรับxml.etree.ElementTree.XMLParserจะได้รับการปรับให้เหมาะสมและการแก้ไขลิงexpatไม่มีผลอย่างแน่นอน
Reinderien

อ่าใช่ ดูความคิดเห็นของ @ barny: stackoverflow.com/questions/13412496/…
Reinderien

5

คุณสามารถใช้โครงสร้างการจัดรูปแบบสตริงที่สวยงามได้เช่นกัน:

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

หรือหากคุณแน่ใจว่าPAID_OFFปรากฏในระดับเดียวในโครงสร้างต้นไม้:

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

2

หากคุณกำลังใช้งานElementTreeและไม่cElementTreeสามารถบังคับให้ Expat ละเว้นการประมวลผลเนมสเปซได้โดยแทนที่ParserCreate():

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

ElementTreeพยายามใช้ Expat โดยการโทรParserCreate()แต่ไม่มีตัวเลือกที่จะไม่ระบุสตริงตัวคั่นเนมสเปซโค้ดด้านบนจะทำให้ไม่สนใจ แต่ขอเตือนว่าสิ่งนี้อาจทำลายสิ่งอื่นได้


นี่เป็นวิธีที่ดีกว่าคำตอบอื่น ๆ ในปัจจุบันเนื่องจากไม่ได้ขึ้นอยู่กับการประมวลผลสตริง
ลิจัต

3
ใน python 3.7.2 (และอาจเป็น eariler) AFAICT ไม่สามารถหลีกเลี่ยงการใช้ cElementTree ได้อีกต่อไปดังนั้นวิธีแก้ปัญหานี้อาจไม่สามารถทำได้ :-(
barny

1
cElemTree จะเลิก แต่มีเงาของประเภทที่ถูกทำกับเร่ง C รหัส C ไม่ได้เรียกไปยัง expat ดังนั้นใช่วิธีนี้เสีย
ericspod

@barny ยังคงเป็นไปได้ElementTree.fromstring(s, parser=None)ฉันกำลังพยายามส่ง parser ไป
ประมาณ

2

ฉันอาจจะสายสำหรับเรื่องนี้ แต่ฉันไม่คิดว่าre.subเป็นทางออกที่ดี

อย่างไรก็ตามการเขียนซ้ำใช้xml.parsers.expatไม่ได้กับเวอร์ชัน Python 3.x

ผู้ร้ายหลักคือxml/etree/ElementTree.pyดูด้านล่างของซอร์สโค้ด

# 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

ซึ่งค่อนข้างน่าเศร้า

วิธีแก้คือกำจัดมันก่อน

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

tryคำสั่งลองมีประโยชน์ในกรณีที่โค้ดของคุณโหลดซ้ำหรือนำเข้าโมดูลสองครั้งคุณจะได้รับข้อผิดพลาดแปลก ๆ เช่น

  • เกินความลึกของการเรียกซ้ำสูงสุด
  • AttributeError: XMLParser

btw แช่งซอร์สโค้ด etree ดูยุ่งมาก


2

ใน python 3.5 คุณสามารถส่งเนมสเปซเป็นอาร์กิวเมนต์ในfind(). ตัวอย่างเช่น ,

ns= {'xml_test':'http://www.test.com'}
tree = ET.parse(r"test.xml")
el1 = tree.findall("xml_test:DEAL_LEVEL/xml_test:PAID_OFF",ns)

ลิงค์เอกสาร: - https://docs.python.org/3.5/library/xml.etree.elementtree.html#parsing-xml-with-namespaces


ดูเหมือนว่าจะรองรับใน 3.4 ด้วย: docs.python.org/3.4/library/…
Josh Hansen

สำหรับใครก็ตามที่ใช้ Python เวอร์ชันสูงพอสมควรนี่คือคำตอบที่ถูกต้อง
Josh Hansen

1

ลองรวมคำตอบของ nonagonกับคำตอบของ mzjn สำหรับคำถามที่เกี่ยวข้อง :

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

การใช้ฟังก์ชันนี้เรา:

  1. สร้าง iterator ที่จะได้รับทั้ง namespaces และวัตถุต้นไม้แยกวิเคราะห์

  2. ทำซ้ำบนตัววนซ้ำที่สร้างขึ้นเพื่อรับเนมสเปซเขียนตามที่เราสามารถส่งผ่านในแต่ละรายการfind()หรือfindall()เรียกโดย iMom0ได้ในภายหลัง

  3. ส่งคืนอ็อบเจ็กต์องค์ประกอบรากของต้นไม้ที่แยกวิเคราะห์และเนมสเปซ

ฉันคิดว่านี่เป็นแนวทางที่ดีที่สุดเนื่องจากไม่มีการจัดการ XML ต้นทางหรือผลลัพธ์ที่แยกวิเคราะห์xml.etree.ElementTreeใด ๆ ที่เกี่ยวข้อง

ฉันต้องการให้เครดิตคำตอบของ barnyด้วยการให้ส่วนสำคัญของปริศนานี้ (ที่คุณสามารถแยกวิเคราะห์รูทได้จากตัววนซ้ำ) จนกว่าฉันจะสำรวจแผนผัง XML สองครั้งในแอปพลิเคชันของฉัน (หนึ่งครั้งเพื่อรับเนมสเปซอันดับที่สองสำหรับรูท)


พบวิธีใช้งาน แต่มันไม่ได้ผลสำหรับฉันฉันยังคงเห็นเนมสเปซในผลลัพธ์
taiko

1
ดูความคิดเห็น iMom0 ของคำถามของ การใช้ฟังก์ชั่นนี้คุณจะได้รับทั้งวัตถุที่แยกกันและวิธีการที่จะสอบถามด้วยและfind() findall()คุณเพียงป้อนวิธีการเหล่านั้นด้วยคำสั่งของเนมสเปซparse_xml()และใช้คำนำหน้าของเนมสเปซในการสืบค้นของคุณ เช่น:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
z33k

0

บังเอิญเจอคำตอบที่นี่: XSD เงื่อนไขประเภทการกำหนดค่าเริ่มต้นประเภทความสับสน?

<?xml version="1.0" encoding="UTF-8"?>
<persons xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="test.xsd">
    <person version="1">
        <firstname>toto</firstname>
        <lastname>tutu</lastname>
    </person>
</persons>

ดูเพิ่มเติมที่: https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation

ใช้ได้ผลสำหรับฉัน ฉันเรียกขั้นตอนการตรวจสอบ XML ในแอปพลิเคชันของฉัน แต่ฉันต้องการดูการเน้นย้ำการตรวจสอบความถูกต้องและการเติมข้อความอัตโนมัติใน PyCharm อย่างรวดเร็วเมื่อแก้ไข XML noNamespaceSchemaLocationแอตทริบิวต์นี้ทำในสิ่งที่ฉันต้องการ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.