Модуль Python ElementTree: как игнорировать пространство имен файлов XML для поиска соответствующего элемента при использовании методов «find», «findall»


136

Я хочу использовать метод «findall», чтобы найти некоторые элементы исходного файла xml в модуле ElementTree.

Однако исходный xml-файл (test.xml) имеет пространство имен. Я обрезаю часть xml файла как образец:

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

Пример кода Python ниже:

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>

Хотя это может работать, поскольку существует пространство имен "{http://www.test.com}", очень неудобно добавлять пространство имен перед каждым тегом.

Как я могу игнорировать пространство имен при использовании методов «find», «findall» и так далее?


18
Это tree.findall("xmlns:DEAL_LEVEL/xmlns:PAID_OFF", namespaces={'xmlns': 'http://www.test.com'})достаточно удобно?
— iMom0

Огромное спасибо. Я пробую ваш метод, и он может работать. Это удобнее, чем у меня, но все же немного неловко. Знаете ли вы, что в модуле ElementTree нет другого подходящего метода для решения этой проблемы или такого метода нет вообще?
— КевинЛенг

Или попробуйтеtree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
— Warf

В Python 3.8 для пространства имен можно использовать подстановочный знак. stackoverflow.com/a/62117710/407651
— mzjn

Ответы:


62

Вместо изменения самого XML-документа лучше всего проанализировать его, а затем изменить теги в результате. Таким образом, вы можете обрабатывать несколько пространств имен и псевдонимов пространства имен:

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

Это основано на обсуждении здесь: http://bugs.python.org/issue18304

Обновление: rpartition вместо того, partitionчтобы убедиться, что вы получите имя тега, postfixдаже если нет пространства имен. Таким образом, вы можете сжать это:

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

2
Это. Это это это Множественные пространства имен были бы моей смертью.
— Джесс

8
Хорошо, это хорошо и более продвинутый, но все же это не так et.findall('{*}sometag'). И это также искажает само дерево элементов, а не просто «выполняет поиск, игнорируя пространства имен только в этот раз, без повторного анализа документа и т. Д., Сохраняя информацию о пространстве имен». Что ж, в этом случае вам необходимо пройти по дереву и убедиться, что узел соответствует вашим пожеланиям после удаления пространства имен.
— Томаш Гандор

1
Это работает путем удаления строки, но когда я сохраняю XML-файл, используя write (...), пространство имен исчезает из начала XML-файла xmlns = " bla ". Пожалуйста, совет
— TraceKira

@TomaszGandor: возможно, вы можете добавить пространство имен к отдельному атрибуту. Для простых тестов на удержание тегов ( содержит ли этот документ это имя тега? ) Это решение отлично подходит, и его можно сократить.
— Мартин Питерс

@TraceKira: этот метод удаляет пространства имен из проанализированного документа, и вы не можете использовать его для создания новой строки XML с пространствами имен. Либо сохраните значения пространства имен в дополнительном атрибуте (и поместите пространство имен обратно перед тем, как превратить дерево XML обратно в строку), либо выполните повторный анализ исходного источника, чтобы применить к нему изменения, основанные на извлеченном дереве.
— Мартин Питерс

48

Если вы удалите атрибут xmlns из xml перед его анализом, то к каждому тегу в дереве не будет добавлено пространство имен.

import re

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

5
Во многих случаях это сработало для меня, но затем я столкнулся с несколькими пространствами имен и псевдонимами пространств имен. Смотрите мой ответ для другого подхода, который обрабатывает эти случаи.
— nonagon

47
-1 манипулирование xml с помощью регулярного выражения перед синтаксическим анализом просто неправильно. хотя в некоторых случаях это может сработать, этот ответ не должен быть наиболее популярным и не должен использоваться в профессиональных приложениях.
— Майк

1
Помимо того факта, что использование регулярных выражений для задания синтаксического анализа XML по своей сути нецелесообразно, это не сработает для многих документов XML , поскольку оно игнорирует префиксы пространства имен, а также тот факт, что синтаксис XML допускает произвольные пробелы перед именами атрибутов (а не только пробелы) и вокруг =знака равенства.
— Мартейн Питерс

Да, это быстро и грязно, но это определенно самое элегантное решение для простых случаев использования, спасибо!
— rimkashox,

18

Ответы до сих пор явно помещают значение пространства имен в сценарий. Для более общего решения я бы предпочел извлечь пространство имен из xml:

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

И используйте его в методе поиска:

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

15
Слишком много, чтобы предполагать, что есть только одинnamespace
— Kashyap

Это не учитывает, что вложенные теги могут использовать разные пространства имен.
— Мартин Питерс

15

Вот расширение ответа nonagon, которое также удаляет пространства имен из атрибутов:

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

ОБНОВЛЕНИЕ: добавлено, list()чтобы итератор работал (необходимо для Python 3)


14

Улучшение ответа от ericspod:

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

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

Это может быть использовано следующим образом

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

Прелесть этого способа в том, что он не меняет никакого поведения для несвязанного кода вне блока with. Я закончил тем, что создал это после получения ошибок в несвязанных библиотеках после использования версии ericspod, которая также использовала expat.


Это сладко и полезно! Спас мой день! +1
— AndreasT

В Python 3.8 (не проверял с другими версиями) это не работает для меня. Глядя на исходный код, он должен работать, но кажется, что исходный код xml.etree.ElementTree.XMLParserкаким-то образом оптимизирован, а патчирование обезьян не expatимеет абсолютно никакого эффекта.
— Reinderien

О да. См. Комментарий @barny: stackoverflow.com/questions/13412496/…
— Reinderien

5

Вы также можете использовать элегантную конструкцию форматирования строки:

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

или, если вы уверены, что PAID_OFF появляется только на одном уровне дерева:

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

2

Если вы используете, ElementTreeно не cElementTreeможете заставить Expat игнорировать обработку пространства имен, заменив ParserCreate():

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

ElementTreeпытается использовать Expat, вызывая, ParserCreate()но не предоставляет возможности не предоставлять строку разделителя пространства имен, приведенный выше код приведет к его игнорированию, но предупреждаем, что это может нарушить другие вещи.


Это лучший способ, чем другие текущие ответы, так как он не зависит от обработки строк
— lijat

3
В python 3.7.2 (и, возможно, в более ранней версии) AFAICT больше невозможно избежать использования cElementTree, поэтому этот обходной путь может быть невозможен :-(
— barny

1
cElemTree устарела, но затенение типов выполняется с помощью ускорителей Си . Код C не обращается к expat, так что да, это решение не работает.
— Ericspod

@barny все еще возможно, ElementTree.fromstring(s, parser=None)я пытаюсь передать ему парсер.
— Текущая

2

Возможно, я опоздаю, но не думаю, что re.subэто хорошее решение.

Однако перезапись xml.parsers.expatне работает для версий Python 3.x,

Главный виновник - xml/etree/ElementTree.pyнижняя часть исходного кода.

# 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

Что довольно грустно.

Решение состоит в том, чтобы сначала избавиться от него.

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)

Проверено на Python 3.6.

tryОператор try полезен в случае, если где-то в вашем коде вы дважды перезагружаете или импортируете модуль, вы получаете странные ошибки, например

  • превышена максимальная глубина рекурсии
  • AttributeError: XMLParser

кстати, черт побери, исходный код etree выглядит действительно беспорядочно.


1

Давайте объединим ответ девятиугольника в с ответом mzjn к связанному с этим вопрос :

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

Используя эту функцию мы:

  1. Создайте итератор, чтобы получить как пространства имен, так и проанализированный объект дерева .

  2. Итерация над созданным итератором , чтобы получить пространство имен Сыроватские , что мы можем позже передать в каждом find()или findall()вызове в качестве мотивационных по iMom0 .

  3. Вернуть объект корневого элемента проанализированного дерева и пространства имен.

Я думаю, что это лучший подход, так как здесь нет никаких манипуляций ни с исходным XML, ни с полученным в результате проанализированным xml.etree.ElementTreeвыводом.

Я также хотел бы отметить ответ Барни, предоставив важную часть этой головоломки (которую вы можете получить проанализированный корень у итератора). До этого я фактически дважды просматривал дерево XML в своем приложении (один раз, чтобы получить пространства имен, второй - для корня).


узнал, как его использовать, но он не работает для меня, я все еще вижу пространства имен в выводе
— Taiko

1
Посмотрите комментарий iMom0 к вопросу OP . Используя эту функцию, вы получаете как проанализированный объект, так и средства для его запроса с помощью find()и findall(). Вы просто кормите эти методы диктовкой пространства именparse_xml() и используете префикс пространства имен в своих запросах. Например:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
— z33k
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.