Разбор XML с пространством имен в Python через 'ElementTree'

164

У меня есть следующий 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теги.

Сударь
источник

Ответы:

227

ElementTree не слишком умён в отношении пространств имен. Вам нужно дать методам .find(), findall()and iterfind()явный словарь пространства имен. Это не очень хорошо задокументировано:

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библиотеку, то дела обстоят лучше; эта библиотека поддерживает тот же API ElementTree, но собирает для вас пространства имен в .nsmapатрибуте элементов.

Мартейн Питерс
источник
7
Спасибо. Любая идея, как я могу получить пространство имен непосредственно из XML, без жесткого его кодирования? Или как я могу это игнорировать? Я пробовал findall ('{*} Class'), но в моем случае это не сработает.
Костанос
7
Вам придется самостоятельно сканировать дерево на предмет xmlnsатрибутов; как указано в ответе, lxmlделает это для вас, xml.etree.ElementTreeмодуль не делает. Но если вы пытаетесь сопоставить определенный (уже жестко закодированный) элемент, то вы также пытаетесь сопоставить конкретный элемент в определенном пространстве имен. Это пространство имен не будет меняться между документами так же, как имя элемента. Вы можете также жестко закодировать это с именем элемента.
Мартин Питерс
14
@Jon: register_namespaceвлияет только на сериализацию, а не на поиск.
Мартин Питерс
5
Небольшое дополнение, которое может быть полезно: при использовании cElementTreeвместо ElementTree, findallне будет принимать пространства имен в качестве аргумента ключевого слова, а скорее просто как обычный аргумент, то есть использовать ctree.findall('owl:Class', namespaces).
egpbos
2
@Bludwarf: Документы упоминают об этом (сейчас, если не когда вы это написали), но вы должны внимательно прочитать их. См. Раздел « Синтаксический анализ XML с пространствами имен »: есть пример, сравнивающий использование аргумента findallбез, а затем с namespaceаргументом, но этот аргумент не упоминается в качестве одного из аргументов метода метода в разделе объекта Element .
Уилсон Ф
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ïcFaure-Lacroix: Обычно библиотеки XML позволяют вам абстрагироваться от этой части. Вам даже не нужно знать или заботиться о префиксе, используемом в самом файле, вы просто определяете свой собственный префикс для анализа или просто используете полное имя пространства имен.
Матти Вирккунен
Этот ответ помог мне, по крайней мере, использовать функцию поиска. Нет необходимости создавать свой собственный префикс. Я просто сделал key = list (root.nsmap.keys ()) [0], а затем добавил ключ в качестве префикса: root.find (f '{key}: Tag2', root.nsmap)
Eelco van Vliet
30

Примечание . Этот ответ полезен для стандартной библиотеки Python 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 () обращается к атрибутам элемента: "

MJM
источник
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)

не использует префиксы для элементов в пространстве имен по умолчанию.

peter.slizik
источник