Модуль 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» и так далее?

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

Barny
источник
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.

lijat
источник
Это сладко и полезно! Спас мой день! +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)
TZP
источник
2

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

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

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

ericspod
источник
Это лучший способ, чем другие текущие ответы, так как он не зависит от обработки строк
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 в своем приложении (один раз, чтобы получить пространства имен, второй - для корня).

z33k
источник
узнал, как его использовать, но он не работает для меня, я все еще вижу пространства имен в выводе
Taiko
1
Посмотрите комментарий iMom0 к вопросу OP . Используя эту функцию, вы получаете как проанализированный объект, так и средства для его запроса с помощью find()и findall(). Вы просто кормите эти методы диктовкой пространства именparse_xml() и используете префикс пространства имен в своих запросах. Например:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
z33k