Модуль 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
Используя эту функцию, мы:
Создайте итератор, чтобы получить оба пространства имен и проанализированный объект дерева.
Переберите созданный итератор, чтобы получить пространство имен, которое мы можем позже передать каждый вызов find()
или findall()
, как предложено iMom0.
- Вернуть проанализированный объект корневого элемента дерева и пространства имен.
Я думаю, что это лучший подход, так как нет никаких манипуляций ни с исходным 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 выглядит очень грязно.