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

Я хочу использовать метод "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" и т.д.?

Ответ 1

Вместо изменения самого 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

Ответ 2

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

import re

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

Ответ 3

Ответы до сих пор явно помещают значение пространства имен в сценарий. Для более общего решения я бы предпочел извлечь пространство имен из 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

Ответ 4

Здесь добавляется расширение для ответа 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 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

Ответ 5

Улучшение ответа от 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.

Ответ 6

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

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)

Ответ 7

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

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

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

Ответ 8

Давайте объединим неагональный ответ с 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.

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

Ответ 9

Я мог бы опоздать на это, но я не думаю, что 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 выглядит очень грязно.