การแยก XML กับเนมสเปซใน Python ผ่าน 'ElementTree'


163

ฉันมี XML ต่อไปนี้ซึ่งฉันต้องการแยกวิเคราะห์โดยใช้ 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>

ฉันต้องการค้นหาowl:Classแท็กทั้งหมดแล้วแยกค่าของrdfs:labelอินสแตนซ์ทั้งหมดที่อยู่ในนั้น ฉันกำลังใช้รหัสต่อไปนี้:

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

เนื่องจากเนมสเปซฉันได้รับข้อผิดพลาดต่อไปนี้

SyntaxError: prefix 'owl' not found in prefix map

ฉันพยายามอ่านเอกสารที่http://effbot.org/zone/element-namespaces.htmแต่ฉันยังไม่สามารถใช้งานได้เนื่องจาก XML ด้านบนมีเนมสเปซซ้อนกันหลายตัว

กรุณาแจ้งให้เราทราบวิธีเปลี่ยนรหัสเพื่อค้นหาowl:Classแท็กทั้งหมด

คำตอบ:


226

ElementTree ไม่ฉลาดเกินไปเกี่ยวกับเนมสเปซ คุณจำเป็นต้องให้.find(), findall()และiterfind()วิธีการพจนานุกรม namespace อย่างชัดเจน นี่ไม่ใช่เอกสารที่ดีมาก:

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

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

คำนำหน้าจะค้นหาเฉพาะในnamespacesพารามิเตอร์ที่คุณส่งผ่านซึ่งหมายความว่าคุณสามารถใช้คำนำหน้าเนมสเปซที่คุณต้องการ; API แยกowl:ส่วนค้นหา URL เนมสเปซที่สอดคล้องกันในnamespacesพจนานุกรมจากนั้นเปลี่ยนการค้นหาเพื่อค้นหานิพจน์ XPath {http://www.w3.org/2002/07/owl}Classแทน คุณสามารถใช้ไวยากรณ์เดียวกันได้ด้วยตัวเองเช่นกัน:

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

หากคุณสามารถเปลี่ยนไปใช้lxmlห้องสมุดได้ดีกว่า ไลบรารีนั้นสนับสนุน ElementTree API เดียวกัน แต่จะรวบรวมเนมสเปซให้คุณใน.nsmapแอตทริบิวต์ขององค์ประกอบ


7
ขอบคุณ. ความคิดใดที่ฉันจะได้รับ namespace โดยตรงจาก XML โดยไม่ต้องเข้ารหัสยาก? หรือฉันจะเพิกเฉยได้อย่างไร ฉันได้ลอง findall ('{*} Class') แล้ว แต่มันก็ใช้งานไม่ได้ในกรณีของฉัน
Kostanos

7
คุณต้องสแกนต้นไม้เพื่อหาxmlnsแอตทริบิวต์ด้วยตัวเอง ตามที่ระบุไว้ในคำตอบlxmlทำสิ่งนี้ให้คุณxml.etree.ElementTreeโมดูลไม่ได้ แต่ถ้าคุณพยายามจับคู่องค์ประกอบที่เฉพาะเจาะจง (ฮาร์ดโค้ดแล้ว) คุณก็จะพยายามจับคู่องค์ประกอบเฉพาะในเนมสเปซที่ระบุด้วย เนมสเปซนั้นจะไม่เปลี่ยนแปลงระหว่างเอกสารมากกว่าชื่อองค์ประกอบ คุณอาจ hardcode ที่มีชื่อองค์ประกอบ
Martijn Pieters

14
@ จอน: register_namespaceมีผลเฉพาะการทำให้เป็นอันดับไม่ใช่การค้นหา
Martijn Pieters

5
นอกจากนี้ขนาดเล็กที่อาจจะมีประโยชน์: เมื่อใช้cElementTreeแทนElementTree, findallจะไม่ใช้ namespaces เป็นอาร์กิวเมนต์คำหลัก ctree.findall('owl:Class', namespaces)แต่เพียงเป็นอาร์กิวเมนต์ปกติเช่นการใช้งาน
egpbos

2
@ Bludwarf: เอกสารพูดถึงมัน (ตอนนี้ถ้าไม่ใช่ตอนที่คุณเขียนมัน) แต่คุณต้องอ่านพวกมันให้ละเอียด ดูส่วนการแยกวิเคราะห์ XML กับเนมสเปซ : มีตัวอย่างที่ขัดแย้งกับการใช้แบบfindallไม่ใช้แล้วกับnamespaceอาร์กิวเมนต์ แต่อาร์กิวเมนต์ไม่ได้กล่าวถึงว่าเป็นหนึ่งในอาร์กิวเมนต์ของวิธีการในส่วนวัตถุองค์ประกอบ
Wilson F

57

ต่อไปนี้เป็นวิธีทำด้วย lxml โดยไม่ต้องเขียนโค้ดเนมสเปซหรือสแกนข้อความให้พวกเขา (ตามที่ Martijn Pieters กล่าวถึง):

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

อัปเดต :

5 ปีต่อมาฉันยังคงพบกับความหลากหลายของปัญหานี้ lxml ช่วยตามที่ฉันแสดงข้างต้น แต่ไม่ได้ในทุกกรณี ผู้แสดงความคิดเห็นอาจมีจุดที่ถูกต้องเกี่ยวกับเทคนิคนี้เมื่อมันมารวมเอกสาร แต่ฉันคิดว่าคนส่วนใหญ่มีปัญหาเพียงแค่ค้นหาเอกสาร

นี่เป็นอีกกรณีหนึ่งและฉันจัดการได้อย่างไร:

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

xmlns ที่ไม่มีคำนำหน้าหมายความว่าแท็กที่ไม่มีคำนำหน้าจะได้รับเนมสเปซเริ่มต้นนี้ ซึ่งหมายความว่าเมื่อคุณค้นหา Tag2 คุณจะต้องรวมเนมสเปซเพื่อค้นหา อย่างไรก็ตาม lxml สร้างรายการ nsmap โดยไม่มี None เป็นกุญแจและฉันไม่สามารถหาวิธีได้ ดังนั้นฉันจึงสร้างพจนานุกรมเนมสเปซใหม่เช่นนี้

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)

3
URL เนมสเปซแบบเต็มคือตัวระบุเนมสเปซที่คุณควรจะใช้รหัสยาก คำนำหน้าท้องถิ่น ( owl) สามารถเปลี่ยนจากไฟล์เป็นไฟล์ ดังนั้นการทำสิ่งที่คำตอบนี้เสนอจึงเป็นความคิดที่เลวจริงๆ
Matti Virkkunen

1
@MattiVirkkunen ถ้าคำจำกัดความของนกฮูกสามารถเปลี่ยนจากไฟล์เป็นไฟล์ได้เราไม่ควรใช้คำจำกัดความที่กำหนดไว้ในแต่ละไฟล์แทนที่จะเป็นฮาร์ดโค้ดหรือไม่
Loïc Faure-Lacroix

@ LoïcFaure-Lacroix: โดยปกติแล้วไลบรารี XML จะช่วยให้คุณสามารถแยกส่วนนั้นออกได้ คุณไม่จำเป็นต้องรู้หรือใส่ใจเกี่ยวกับคำนำหน้าที่ใช้ในไฟล์เองคุณเพียงแค่กำหนดคำนำหน้าของคุณเองเพื่อการวิเคราะห์คำหรือใช้ชื่อเนมสเปซแบบเต็ม
Matti Virkkunen

คำตอบนี้ช่วยฉันอย่างน้อยก็สามารถใช้ฟังก์ชันค้นหา ไม่จำเป็นต้องสร้างคำนำหน้าของคุณเอง ฉันเพิ่งทำรายการ key = (root.nsmap.keys ()) [0] แล้วเพิ่มคีย์เป็นคำนำหน้า: root.find (f '{key}: Tag2', root.nsmap)
Eelco van Vliet

30

หมายเหตุ : นี่เป็นคำตอบที่มีประโยชน์สำหรับไลบรารี่ ElementTree มาตรฐานของไพ ธ อนโดยไม่ต้องใช้เนมสเปซฮาร์ดโค้ด

ในการแยกคำนำหน้าของเนมสเปซและ URI จากข้อมูล XML คุณสามารถใช้ElementTree.iterparseฟังก์ชันโดยแยกวิเคราะห์เฉพาะเหตุการณ์เริ่มต้นของเนมสเปซ ( 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#'}

จากนั้นพจนานุกรมสามารถส่งผ่านเป็นอาร์กิวเมนต์ไปยังฟังก์ชันการค้นหา:

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

1
สิ่งนี้มีประโยชน์สำหรับพวกเราที่ไม่สามารถเข้าถึง lxml และไม่ต้องการ hardcode namespace
delrocco

1
ฉันได้รับข้อผิดพลาด: ValueError: write to closedสำหรับบรรทัดfilemy_namespaces = dict([node for _, node in ET.iterparse(StringIO(my_schema), events=['start-ns'])])นี้ ความคิดใดที่ต้องการผิดหรือเปล่า?
Yuli

อาจเป็นข้อผิดพลาดที่เกี่ยวข้องกับคลาส io.StringIO ที่ปฏิเสธสตริง ASCII ฉันทดสอบสูตรด้วย Python3 แล้ว การเพิ่มคำนำหน้าสตริง Unicode 'u' ให้กับสตริงตัวอย่างที่ใช้งานได้กับ Python 2 (2.7)
Davide Brunato

แทนที่จะเป็นเช่นdict([...])นั้นคุณสามารถใช้ความเข้าใจแบบ dict
Arminius

แทนที่จะStringIO(my_schema)คุณยังสามารถใส่ชื่อไฟล์ของไฟล์ XML ที่
JustAC0der

6

ฉันใช้รหัสที่คล้ายกันนี้และพบว่ามันคุ้มค่าที่จะอ่านเอกสาร ... ตามปกติ!

findall () จะพบว่าองค์ประกอบที่มีเด็กโดยตรงของแท็กปัจจุบัน ดังนั้นไม่ใช่ทั้งหมด

มันอาจจะคุ้มค่ากับคุณในขณะที่พยายามให้โค้ดทำงานกับสิ่งต่อไปนี้โดยเฉพาะอย่างยิ่งหากคุณกำลังจัดการกับไฟล์ xml ขนาดใหญ่และซับซ้อนเพื่อให้องค์ประกอบย่อย (ฯลฯ ) รวมอยู่ด้วย หากคุณรู้ว่าตัวเองอยู่ที่ไหนองค์ประกอบอยู่ใน xml ของคุณฉันคิดว่ามันจะไม่เป็นไร! แค่คิดว่านี่เป็นสิ่งที่ควรค่าแก่การจดจำ

root.iter()

ref: https://docs.python.org/3/library/xml.etree.elementtree.html#finding-interesting-elements "Element.findall () ค้นหาองค์ประกอบที่มีแท็กซึ่งเป็นองค์ประกอบย่อยโดยตรงขององค์ประกอบปัจจุบัน Element.find () ค้นหาชายด์แรกที่มีแท็กเฉพาะและ Element.text เข้าถึงเนื้อหาข้อความขององค์ประกอบ Element.get () เข้าถึงแอตทริบิวต์ขององค์ประกอบ: "


6

ในการรับเนมสเปซในรูปแบบเนมสเปซเช่น{myNameSpace}คุณสามารถทำสิ่งต่อไปนี้:

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

วิธีนี้คุณสามารถใช้ในภายหลังในรหัสของคุณเพื่อค้นหาโหนดเช่นใช้การแก้ไขสตริง (Python 3)

link = root.find(f"{ns}link")

0

โซลูชันของฉันใช้ความคิดเห็นของ @Martijn Pieters:

register_namespace มีผลกับการทำให้เป็นอนุกรมเท่านั้นไม่ใช่การค้นหา

ดังนั้นเคล็ดลับที่นี่คือการใช้พจนานุกรมที่แตกต่างกันสำหรับการทำให้เป็นอันดับและสำหรับการค้นหา

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

ตอนนี้ลงทะเบียน namespaces ทั้งหมดสำหรับการแยกและการเขียน:

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

สำหรับการค้นหา ( find(), findall(), iterfind()) เราต้องมีคำนำหน้าไม่ว่างเปล่า ผ่านฟังก์ชั่นเหล่านี้พจนานุกรมที่ปรับเปลี่ยน (ที่นี่ฉันแก้ไขพจนานุกรมต้นฉบับ แต่ต้องทำหลังจากที่ลงทะเบียน namespaces เท่านั้น)

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

ตอนนี้ฟังก์ชั่นจากfind()ตระกูลสามารถใช้กับdefaultคำนำหน้า:

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

แต่

tree.write(destination)

ไม่ใช้ส่วนนำหน้าใด ๆ สำหรับองค์ประกอบในเนมสเปซเริ่มต้น

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