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

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

8 ответов

Решение

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 словарь, затем изменяет поиск для поиска выражения XPath {http://www.w3.org/2002/07/owl}Class вместо. Вы также можете использовать тот же синтаксис самостоятельно, конечно:

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

Если вы можете переключиться на lxml библиотечные вещи лучше; эта библиотека поддерживает тот же API ElementTree, но собирает для вас пространства имен в .nsmap атрибут на элементах.

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

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

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

Я использовал подобный код для этого и обнаружил, что всегда стоит прочитать документацию... как обычно!

findall () найдет только элементы, которые являются прямыми потомками текущего тега. Так что не совсем ВСЕ.

Возможно, стоит попытаться заставить ваш код работать со следующим, особенно если вы имеете дело с большими и сложными XML-файлами, чтобы эти подэлементы (и т. Д.) Также были включены. Если вы сами знаете, где находятся элементы в вашем xml, тогда, я думаю, все будет хорошо! Просто подумал, что это стоит запомнить.

root.iter()

ref: https://docs.python.org/3/library/xml.etree.elementtree.html"Element.findall() находит только элементы с тегом, которые являются прямыми потомками текущего элемента. Element.find() находит первый дочерний элемент с определенным тегом, а Element.text обращается к текстовому содержимому элемента. Element.get() обращается к атрибутам элемента:"

Чтобы получить пространство имен в его формате, например {myNameSpace}Вы можете сделать следующее:

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

Таким образом, вы можете использовать его позже в своем коде для поиска узлов, например, используя интерполяцию строк (Python 3).

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

В основном это ответ Давиде Брунато, однако я обнаружил, что у его ответа были серьезные проблемы, поскольку пространство имен по умолчанию было пустой строкой, по крайней мере, в моей установке python 3.6. Функция, которую я извлек из его кода и которая сработала для меня, следующая:

      from io import StringIO
from xml.etree import ElementTree
def get_namespaces(xml_string):
    namespaces = dict([
            node for _, node in ElementTree.iterparse(
                StringIO(xml_string), events=['start-ns']
            )
    ])
    namespaces["ns0"] = namespaces[""]
    return namespaces

где ns0 - это просто заполнитель для пустого пространства имен, и вы можете заменить его любой случайной строкой, которая вам нравится.

Если я тогда сделаю:

      my_namespaces = get_namespaces(my_schema)
root.findall('ns0:SomeTagWithDefaultNamespace', my_namespaces)

Он также дает правильный ответ для тегов с использованием пространства имен по умолчанию.

Мое решение основано на комментарии @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', nemspaces)

но

tree.write(destination)

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

Я знаю, что опоздал на несколько лет, но я только что создал пакет, который будет обрабатывать преобразование словаря в действительный XML с пространствами имен. Пакет размещен на PyPi @ https://pypi.python.org/pypi/xmler.

Используя этот пакет, вы можете взять словарь, который выглядит так:

myDict = {
    "RootTag": {                        # The root tag. Will not necessarily be root. (see #customRoot)
        "@ns": "soapenv",           # The namespace for the RootTag. The RootTag will appear as <soapenv:RootTag ...>
        "@attrs": {                     # @attrs takes a dictionary. each key-value pair will become an attribute
            { "xmlns:soapenv": "http://schemas.xmlsoap.org/soap/envelope/" }
        },
        "childTag": {
            "@attrs": {
                "someAttribute": "colors are nice"
            },
            "grandchild": "This is a text tag"
        }
    }
}

и получить вывод XML, который выглядит следующим образом:

<soapenv:RootTag xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
    <childTag someAttribute="colors are nice">
        <grandchild>This is a text tag</grandchild>
    </childTag>
</soapenv:RootTag>

Надеюсь, что это будет полезно для людей в будущем

Другие вопросы по тегам