Розбір 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()методам , явний словник імен. Це не дуже задокументовано:

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

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

Префікси шукаються лише в namespacesпараметрі, який ви передаєте. Це означає, що ви можете використовувати будь-який префікс простору імен, який вам подобається; API розбивається на owl:частину, шукає відповідну URL-адресу простору імен у namespacesсловнику, а потім змінює пошук, щоб шукати {http://www.w3.org/2002/07/owl}Classзамість цього виразу XPath . Ви також можете використовувати той самий синтаксис, звичайно:

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

Якщо ви можете перейти до lxmlбібліотеки, все краще; ця бібліотека підтримує той самий API ElementTree, але збирає для вас простори імен в .nsmapатрибут елементів.


7
Дякую. Будь-яка ідея, як я можу отримати простір імен безпосередньо з XML, без жорсткого кодування? Або як я можу це проігнорувати? Я спробував findall ('{*} Class'), але в моєму випадку він не працював.
Костанос

7
Вам доведеться самостійно сканувати дерево на xmlnsпредмет атрибутів; як зазначено у відповіді, lxmlчи робить це для вас, xml.etree.ElementTreeмодуль не робить. Але якщо ви намагаєтеся відповідати конкретному (вже жорстко закодованому) елементу, то ви також намагаєтеся відповідати конкретному елементу в певній просторі імен. Цей простір імен не буде змінюватися між документами більше, ніж це ім'я елемента. Ви можете також жорсткий код, що з назвою елемента.
Martijn Pieters

14
@Jon: register_namespaceвпливає лише на серіалізацію, а не на пошук.
Martijn Pieters

5
Невелике доповнення , яке може бути корисно: при використанні cElementTreeзамість ElementTree, findallне братиме простір імен в якості ключового слова аргументу, а просто як нормальний аргумент, тобто використання ctree.findall('owl:Class', namespaces).
egpbos

2
@Bludwarf: Документи згадують це (зараз, якщо не тоді, коли ви це писали), але вам потрібно уважно прочитати їх. Дивіться розділ Парсинг XML з просторами імен : є приклад, який контрастує з використанням аргументу findallбез, а потім з namespaceаргументом, але аргумент не згадується як один з аргументів методу методу в розділі Об'єкт Елемент .
Вілсон F

57

Ось як це зробити за допомогою lxml без необхідності жорсткого кодування просторів імен або сканування тексту для них (як згадує Мартійн Пітерс):

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) може змінюватися з файла в файл. Тому робити те, що підказує ця відповідь, - це дійсно погана ідея.
Матті Вірккунен

1
@MattiVirkkunen саме якщо визначення сови може змінюватися від файлу до файлу, чи не слід використовувати визначення, визначене у кожному файлі, замість жорсткого кодування?
Loïc Faure-Lacroix

@ LoïcFaure-Lacroix: Зазвичай бібліотеки XML дозволять вам абстрагувати цю частину. Вам навіть не потрібно знати або піклуватися про префікс, який використовується у самому файлі, ви просто визначите свій власний префікс з метою розбору або просто використовуєте повне ім’я простору імен.
Матті Вірккунен

ця відповідь допомогла моєму принаймні мати можливість використовувати функцію пошуку. Не потрібно створювати власний префікс. Я щойно зробив key = list (root.nsmap.keys ()) [0], а потім додав ключ як префікс: root.find (f '{key}: Tag2', root.nsmap)
Eelco van Vliet

30

Примітка . Це відповідь, корисна для стандартної бібліотеки Python's 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 та не бажає жорсткого коду імен.
delrocco

1
Я отримав помилку: ValueError: write to closedдля цього рядка filemy_namespaces = dict([node for _, node in ET.iterparse(StringIO(my_schema), events=['start-ns'])]). Будь-яка ідея хоче неправильно?
Юлі

Ймовірно, помилка пов'язана з класом io.StringIO, який відмовляється від рядків ASCII. Я протестував свій рецепт з Python3. Додавання префікса рядка unicode 'u' до рядка зразка, він також працює з Python 2 (2.7).
Девіде Брунато

Замість dict([...])вас також можна використовувати розуміння диктанту.
Арміній

Замість цього 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',
}

Тепер зареєструйте всі простори імен для розбору та запису:

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

Для пошуку ( find(), findall(), iterfind()), потрібно непорожній префікс. Передайте цим функціям модифікований словник (тут я змінюю оригінальний словник, але це потрібно зробити лише після того, як будуть зареєстровані простори імен).

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

Тепер функції find()сімейства можна використовувати з defaultпрефіксом:

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

але

tree.write(destination)

не використовує жодних префіксів для елементів у просторі імен за замовчуванням.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.