Mô-đun Python ElementTree: Cách bỏ qua không gian tên của các tệp XML để xác định vị trí phần tử phù hợp khi sử dụng phương thức Tìm kiếm, phát hiện ra


136

Tôi muốn sử dụng phương thức "findall" để định vị một số phần tử của tệp xml nguồn trong mô-đun ElementTree.

Tuy nhiên, tệp xml nguồn (test.xml) có không gian tên. Tôi cắt một phần của tệp xml làm mẫu:

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

Mã python mẫu dưới đây:

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>

Mặc dù nó có thể hoạt động, vì có một không gian tên "{http://www.test.com}", việc thêm một không gian tên ở trước mỗi thẻ là rất bất tiện.

Làm cách nào tôi có thể bỏ qua không gian tên khi sử dụng phương thức "find", "findall", v.v.


18
tree.findall("xmlns:DEAL_LEVEL/xmlns:PAID_OFF", namespaces={'xmlns': 'http://www.test.com'})đủ thuận tiện?
iMom0

Cảm ơn rất nhiều. Tôi thử phương pháp của bạn và nó có thể làm việc. Nó tiện lợi hơn của tôi nhưng vẫn hơi khó xử. Bạn có biết nếu không có phương pháp thích hợp nào khác trong mô-đun ElementTree để giải quyết vấn đề này hay không có phương pháp nào như vậy?
KevinLeng

Hoặc thửtree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
Warf

Trong Python 3.8, một ký tự đại diện có thể được sử dụng cho không gian tên. stackoverflow.com/a/62117710/407651
mzjn

Câu trả lời:


62

Thay vì tự sửa đổi tài liệu XML, tốt nhất là phân tích nó và sau đó sửa đổi các thẻ trong kết quả. Bằng cách này, bạn có thể xử lý nhiều không gian tên và bí danh không gian tên:

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

Điều này dựa trên cuộc thảo luận ở đây: http://bugs.python.org/su18304

Cập nhật: rpartition thay vì partitionđảm bảo bạn nhận được tên thẻ postfixngay cả khi không có không gian tên. Vì vậy, bạn có thể ngưng tụ nó:

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

2
Điều này. Điều này này này. Nhiều không gian tên sẽ là cái chết của tôi.
Jess

8
OK, điều này là tốt đẹp và cao cấp hơn, nhưng vẫn không phải et.findall('{*}sometag'). Và nó cũng đang tự xáo trộn cây phần tử, không chỉ "thực hiện tìm kiếm bỏ qua các không gian tên chỉ trong thời gian này, mà không phân tích lại tài liệu, v.v., giữ lại thông tin không gian tên". Chà, trong trường hợp đó, bạn cần phải lặp đi lặp lại qua cây và tự mình xem, nếu nút phù hợp với mong muốn của bạn sau khi loại bỏ không gian tên.
Tomasz Gandor

1
Điều này hoạt động bằng cách tước chuỗi nhưng khi tôi lưu tệp XML bằng cách sử dụng write (...) thì không gian tên sẽ biến mất khỏi sự cầu xin của XML xmlns = " bla ". Xin tư vấn
TraceKira

@TomaszGandor: có lẽ bạn có thể thêm không gian tên vào một thuộc tính riêng biệt. Đối với các thử nghiệm ngăn chặn thẻ đơn giản ( tài liệu này có chứa tên thẻ này không? ) Giải pháp này rất tuyệt và có thể được ngắn mạch.
Martijn Pieters

@TraceKira: kỹ thuật này loại bỏ các không gian tên khỏi tài liệu được phân tích cú pháp và bạn không thể sử dụng nó để tạo một chuỗi XML mới với các không gian tên. Lưu trữ các giá trị không gian tên trong một thuộc tính bổ sung (và đặt lại không gian tên trước khi biến cây XML trở lại thành chuỗi) hoặc phân tích lại từ nguồn ban đầu để áp dụng các thay đổi dựa trên cây bị tước.
Martijn Pieters

48

Nếu bạn xóa thuộc tính xmlns khỏi xml trước khi phân tích cú pháp thì sẽ không có một không gian tên được thêm vào mỗi thẻ trong cây.

import re

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

5
Điều này làm việc trong nhiều trường hợp đối với tôi, nhưng sau đó tôi chạy vào nhiều không gian tên và bí danh không gian tên. Xem câu trả lời của tôi cho một phương pháp khác xử lý các trường hợp này.
phi hình

47
-1 thao tác xml thông qua một biểu thức thông thường trước khi phân tích cú pháp là sai. mặc dù nó có thể hoạt động trong một số trường hợp, đây không phải là câu trả lời được bình chọn hàng đầu và không nên được sử dụng trong một ứng dụng chuyên nghiệp.
Mike

1
Ngoài việc sử dụng regex cho công việc phân tích cú pháp XML vốn không có cơ sở, thì điều này sẽ không hiệu quả với nhiều tài liệu XML , vì nó bỏ qua các tiền tố không gian tên và thực tế là cú pháp XML cho phép khoảng trắng tùy ý trước các tên thuộc tính (không chỉ khoảng trắng) và xung quanh =dấu bằng.
Martijn Pieters

Vâng, nó nhanh và bẩn, nhưng nó chắc chắn là giải pháp thanh lịch nhất cho các trường hợp sử dụng đơn giản, cảm ơn!
rimkashox

18

Các câu trả lời cho đến nay rõ ràng đặt giá trị không gian tên trong tập lệnh. Đối với một giải pháp chung chung hơn, tôi muốn trích xuất không gian tên từ xml:

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

Và sử dụng nó trong phương pháp tìm:

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

15
Quá nhiều để cho rằng chỉ có mộtnamespace
Kashyap

Điều này không tính đến việc các thẻ lồng nhau có thể sử dụng các không gian tên khác nhau.
Martijn Pieters

15

Đây là phần mở rộng cho câu trả lời của nonagon, cũng loại bỏ không gian tên các thuộc tính:

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

CẬP NHẬT: đã thêm list()để iterator hoạt động (cần thiết cho Python 3)


14

Cải thiện câu trả lời của ericspod:

Thay vì thay đổi chế độ phân tích cú pháp trên toàn cầu, chúng ta có thể gói nó trong một đối tượng hỗ trợ cấu trúc.

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

Điều này sau đó có thể được sử dụng như sau

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

Cái hay của cách này là nó không thay đổi bất kỳ hành vi nào đối với mã không liên quan bên ngoài khối có. Tôi đã kết thúc việc tạo này sau khi gặp lỗi trong các thư viện không liên quan sau khi sử dụng phiên bản bởi ericspod cũng đã sử dụng expat.


Điều này thật ngọt ngào và lành mạnh! Cứu ngày của tôi! +1
AndreasT

Trong Python 3.8 (chưa được thử nghiệm với các phiên bản khác), điều này dường như không hoạt động đối với tôi. Nhìn vào nguồn nó sẽ hoạt động, nhưng có vẻ như mã nguồn xml.etree.ElementTree.XMLParserđược tối ưu hóa bằng cách nào đó và việc vá khỉ expathoàn toàn không có tác dụng.
Reinderien

Ồ thật tuyệt vời. Xem bình luận của @
barny

5

Bạn cũng có thể sử dụng cấu trúc định dạng chuỗi thanh lịch:

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

hoặc, nếu bạn chắc chắn rằng PAID_OFF chỉ xuất hiện ở một cấp độ trong cây:

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

2

Nếu bạn đang sử dụng ElementTreevà không, cElementTreebạn có thể buộc Expat bỏ qua xử lý không gian tên bằng cách thay thế ParserCreate():

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

ElementTreecố gắng sử dụng Expat bằng cách gọi ParserCreate()nhưng không cung cấp tùy chọn nào để không cung cấp chuỗi phân tách không gian tên, đoạn mã trên sẽ khiến nó bị bỏ qua nhưng được cảnh báo rằng điều này có thể phá vỡ những thứ khác.


Đây là một cách tốt hơn so với các câu trả lời hiện tại khác vì nó không phụ thuộc vào xử lý chuỗi
lijat

3
Trong python 3.7.2 (và có thể eariler) AFAICT nó không còn có thể để tránh sử dụng cElementTree, vì vậy việc này có thể không thực hiện được :-(
Barny

1
cElemTree bị phản đối nhưng có được shadowing các loại đang được thực hiện với tăng tốc C . Mã C không gọi vào nước ngoài nên có giải pháp này bị hỏng.
ericspod

@ barny vẫn có thể, ElementTree.fromstring(s, parser=None)tôi đang cố gắng chuyển trình phân tích cú pháp cho nó.
est

2

Tôi có thể bị trễ vì điều này nhưng tôi không nghĩ re.sublà một giải pháp tốt.

Tuy nhiên, việc viết lại xml.parsers.expatkhông hoạt động đối với các phiên bản Python 3.x,

Thủ phạm chính là phần xml/etree/ElementTree.pydưới cùng của mã nguồ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

Thật là buồn.

Giải pháp là loại bỏ nó trước.

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)

Đã thử nghiệm trên Python 3.6.

tryCâu lệnh try hữu ích trong trường hợp ở đâu đó trong mã của bạn, bạn tải lại hoặc nhập mô-đun hai lần, bạn gặp một số lỗi lạ như

  • vượt quá độ sâu đệ quy tối đa
  • AttributionError: XMLParser

btw chết tiệt mã nguồn etree trông thực sự lộn xộn.


1

Hãy kết hợp câu trả lời nonagon của với câu trả lời của mzjn cho một câu hỏi liên quan :

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

Sử dụng chức năng này, chúng tôi:

  1. Tạo một trình vòng lặp để có được cả hai không gian tên và một đối tượng cây được phân tích cú pháp .

  2. Lặp lại qua trình lặp được tạo để có được các không gian tên mà sau này chúng ta có thể vượt qua trong mỗi find()hoặc findall()gọi là bị chặn bởi iMom0 .

  3. Trả về đối tượng phần tử gốc của cây được phân tích cú pháp và không gian tên.

Tôi nghĩ rằng đây là cách tiếp cận tốt nhất xung quanh vì không có thao tác nào với XML nguồn hoặc dẫn đến kết quả được phân tích cú pháp xml.etree.ElementTreebất cứ điều gì liên quan.

Tôi cũng muốn ghi nhận câu trả lời của barny bằng cách cung cấp một phần thiết yếu của câu đố này (rằng bạn có thể lấy gốc được phân tích cú pháp từ trình vòng lặp). Cho đến khi tôi thực sự duyệt qua cây XML hai lần trong ứng dụng của mình (một lần để có được không gian tên, lần thứ hai cho một gốc).


tìm ra cách sử dụng nó, nhưng nó không hiệu quả với tôi, tôi vẫn thấy các không gian tên trong đầu ra
taiko

1
Nhìn vào bình luận của iMom0 cho câu hỏi của OP . Sử dụng chức năng này, bạn nhận được cả đối tượng được phân tích cú pháp và phương tiện để truy vấn nó với find()findall(). Bạn chỉ cần cung cấp các phương thức đó với chính tả của không gian tên parse_xml()và sử dụng tiền tố của không gian tên trong các truy vấn của bạn. Ví dụ:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
z33k
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.