Модуль 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>

Приклад пітонного коду нижче:

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: ви можете додати простір імен до окремого атрибуту, можливо. Для простих тестів на вміст тегів ( чи містить цей документ ім'я тега? ) Це рішення є чудовим і може бути короткозамкненим.
Martijn Pieters

@TraceKira: ця техніка видаляє простори імен з проаналізованого документа, і ви не можете використовувати це для створення нової рядок XML з просторами імен. Або зберігайте значення простору імен у додатковому атрибуті (і поверніть простір імен назад, перш ніж повернути дерево XML назад у рядок) або повторно проаналізуйте з вихідного джерела, щоб застосувати зміни до цього на основі позбавленого дерева.
Мартійн Пітерс

48

Якщо ви видалите атрибут xmlns з xml перед його розбором, то для кожного тегу в дереві не буде попереднього простору імен.

import re

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

5
Для мене це спрацьовувало у багатьох випадках, але тоді я зіткнувся з кількома просторами імен та псевдонімами простору імен. Дивіться мою відповідь щодо іншого підходу, який розглядає ці випадки.
nonagon

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

1
Окрім того, що використання регулярного вираження для завдання розбору XML за своєю суттю не захищено, це не працює для багатьох документів XML , оскільки він ігнорує префікси простору імен, а також той факт, що синтаксис XML передбачає довільну пробіл перед іменами атрибутів (не тільки пробіли) і навколо =знака рівності.
Martijn Pieters

Так, це швидко і брудно, але, безумовно, найелегантніше рішення для простих випадків використання, дякую!
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
Каш’яп

Це не враховує, що вкладені теги можуть використовувати різні простори імен.
Martijn Pieters

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:

Замість зміни режиму синтаксичного розбору ми можемо обернути це об'єктом, що підтримує конструкт.

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")

Краса цього способу полягає в тому, що він не змінює жодної поведінки для непов'язаного коду поза блоком. Я в кінцевому підсумку створив це після отримання помилок у непов'язаних бібліотеках після використання версії 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 . Код С не дзвонить в емігрант, так що так це рішення порушено.
ericspod

@barny це все ще можливо, ElementTree.fromstring(s, parser=None)я намагаюся пройти парсер до нього.
est

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заяву корисно, якщо десь у вашому коді ви перезавантажуєте або імпортуєте модуль двічі, ви отримуєте якісь дивні помилки

  • максимальна глибина рекурсії перевищена
  • AttributeError: XMLParser

btw проклятий вихідний код 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 до питання ОП . За допомогою цієї функції ви отримуєте як проаналізований об'єкт, так і засоби для запиту до нього за допомогою 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.