Módulo Python ElementTree: como ignorar o espaço para nome dos arquivos XML para localizar o elemento correspondente ao usar o método “find”, “findall”


136

Eu quero usar o método "findall" para localizar alguns elementos do arquivo xml de origem no módulo ElementTree.

No entanto, o arquivo xml de origem (test.xml) possui um espaço para nome. Eu truncar parte do arquivo xml como exemplo:

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

O exemplo de código python está abaixo:

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>

Embora possa funcionar, porque existe um espaço para nome "{http://www.test.com}", é muito inconveniente adicionar um espaço para nome na frente de cada tag.

Como posso ignorar o espaço para nome ao usar o método "find", "findall" e assim por diante?


18
É tree.findall("xmlns:DEAL_LEVEL/xmlns:PAID_OFF", namespaces={'xmlns': 'http://www.test.com'})conveniente o suficiente?
— IMom0

Muito obrigado. Eu tento o seu método e ele pode funcionar. É mais conveniente que o meu, mas ainda é um pouco estranho. Você sabe se não existe outro método adequado no módulo ElementTree para resolver esse problema ou se não existe esse método?
— KevinLeng

Ou tentetree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
— Warf

No Python 3.8, um curinga pode ser usado para o espaço para nome. stackoverflow.com/a/62117710/407651
— mzjn

Respostas:


62

Em vez de modificar o próprio documento XML, é melhor analisá-lo e modificar as tags no resultado. Dessa forma, você pode lidar com vários espaços para nome e aliases de espaço para nome:

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

Isso se baseia na discussão aqui: http://bugs.python.org/issue18304

Atualização: em rpartition vez de partitiongarantir que você obtenha o nome da tag, postfixmesmo que não haja espaço para nome. Assim, você pode condensar:

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

2
Este. Isso Isso isso. Vários espaços de nome seriam a minha morte.
— Jess

8
OK, isso é legal e mais avançado, mas ainda não é et.findall('{*}sometag'). E também está manipulando a própria árvore de elementos, não apenas "realize a pesquisa ignorando os espaços para nome neste momento, sem analisar novamente o documento etc., mantendo as informações do espaço para nome". Bem, nesse caso, você notavelmente precisa percorrer a árvore e ver por si mesmo se o nó corresponde aos seus desejos após remover o espaço para nome.
— Tomasz Gandor

1
Isso funciona removendo a string, mas quando eu salvo o arquivo XML usando write (...), o espaço para nome desaparece do início do XML xmlns = " bla " desaparece. Por favor conselho
— TraceKira

@ TomaszGandor: você pode adicionar o espaço para nome a um atributo separado, talvez. Para testes simples de contenção de tags ( este documento contém esse nome de tag? ), Esta solução é ótima e pode sofrer um curto-circuito.
— Martijn Pieters

@TraceKira: essa técnica remove os espaços para nome do documento analisado, e você não pode usá-lo para criar uma nova string XML com espaços para nome. Armazene os valores do espaço para nome em um atributo extra (e volte a colocar o espaço para nome antes de transformar a árvore XML novamente em uma sequência de caracteres) ou analise novamente a partir da fonte original para aplicar alterações àquelas baseadas na árvore removida.
— Martijn Pieters

48

Se você remover o atributo xmlns do xml antes de analisá-lo, não haverá um espaço para nome anexado a cada marca na árvore.

import re

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

5
Isso funcionou em muitos casos para mim, mas depois encontrei vários espaços para nome e aliases de espaço para nome. Veja minha resposta para outra abordagem que lida com esses casos.
— Nonagon 18/09/14

47
-1 manipular o xml por meio de uma expressão regular antes de analisar está errado. embora possa funcionar em alguns casos, essa não deve ser a resposta mais votada e não deve ser usada em um aplicativo profissional.
— 1515 Mike

1
Além do fato de que o uso de uma regex para um trabalho de análise de XML é inerentemente incorreto, isso não funcionará para muitos documentos XML , porque ignora os prefixos de namespace e o fato de a sintaxe XML permitir um espaço em branco arbitrário antes dos nomes dos atributos (não apenas espaços) e ao redor do =sinal de igual.
— Martijn Pieters

Sim, é rápido e sujo, mas é definitivamente a solução mais elegante para casos de uso simples, obrigado!
— rimkashox 13/06

18

As respostas até agora colocam explicitamente o valor do espaço para nome no script. Para uma solução mais genérica, prefiro extrair o namespace do xml:

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

E use-o no método find:

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

15
Demais para supor que existe apenas umnamespace
— Kashyap

Isso não leva em consideração que as tags aninhadas podem usar diferentes espaços para nome.
— Martijn Pieters

15

Aqui está uma extensão da resposta de nonagon, que também remove os namespaces dos atributos:

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

UPDATE: adicionado list()para que o iterador funcione (necessário para o Python 3)


14

Melhorando a resposta de ericspod:

Em vez de alterar o modo de análise globalmente, podemos agrupar isso em um objeto que suporta a construção 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

Isso pode ser usado da seguinte maneira

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

A vantagem dessa maneira é que ela não altera nenhum comportamento para código não relacionado fora do bloco with. Acabei criando isso depois de obter erros em bibliotecas não relacionadas depois de usar a versão ericspod, que também utilizava expat.


Isso é doce e saudável! Salvou o meu dia! 1
— AndreasT

No Python 3.8 (não testei com outras versões), isso não parece funcionar para mim. Olhando a fonte para a qual ela deve funcionar, parece que o código fonte xml.etree.ElementTree.XMLParserestá de alguma forma otimizado e a correção de macacos expatnão tem absolutamente nenhum efeito.
— Reinderien

Ah sim. Veja o comentário de @ barny: stackoverflow.com/questions/13412496/…
— Reinderien

5

Você também pode usar a construção elegante de formatação de string:

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

ou, se tiver certeza de que PAID_OFF aparece apenas em um nível na árvore:

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

2

Se você estiver usando ElementTreee não, cElementTreepoderá forçar o Expat a ignorar o processamento do espaço para nome substituindo ParserCreate():

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

ElementTreetenta usar o Expat chamando, ParserCreate()mas não fornece opção para não fornecer uma cadeia separadora de namespace; o código acima fará com que seja ignorado, mas avise que isso pode quebrar outras coisas.


Esta é uma maneira melhor do que outras respostas atuais, uma vez que não depende de processamento de strings
— lijat

3
Em pitão 3.7.2 (e possivelmente eariler) AFAICT ele não é mais possível para evitar a utilização de cElementTree, pelo que esta solução pode não ser possível :-(
— barny

1
cElemTree está obsoleta, mas não é o sombreamento de tipos que está sendo feito com aceleradores C . O código C não está chamando expat, então sim, esta solução está quebrada.
— 19419 ericspod

@ barny ainda é possível, ElementTree.fromstring(s, parser=None)estou tentando passar um analisador para ele.
— est

2

Eu posso estar atrasado para isso, mas eu não acho re.sub seja uma boa solução.

No entanto, a reescrita xml.parsers.expat não funciona nas versões do Python 3.x,

O principal culpado é a xml/etree/ElementTree.pyparte inferior do código fonte

# 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

O que é meio triste.

A solução é se livrar dele primeiro.

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)

Testado em Python 3.6.

Experimentar try instrução é útil caso, em algum lugar do seu código, você recarregue ou importe um módulo duas vezes e receba erros estranhos, como

  • profundidade de recursão máxima excedida
  • AttributeError: XMLParser

btw caramba o código fonte etree parece realmente confuso.


1

Vamos combinar a resposta de nonagon com a resposta de mzjn para uma pergunta relacionada :

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

Usando esta função, nós:

  1. Crie um iterador para obter os espaços para nome e um objeto de árvore analisada .

  2. Faça uma iteração sobre o iterador criado para obter os ditames dos namespaces que podemos passar posteriormente em cada um find()ou findall()chamar conforme sugerido pelo iMom0 .

  3. Retorne o objeto do elemento raiz e os espaços para nome da árvore analisada.

Eu acho que essa é a melhor abordagem geral, já que não há manipulação de um XML de origem ou resultado analisado xml.etree.ElementTree saída .

Eu também gostaria de creditar a resposta de Barny por fornecer uma peça essencial desse quebra-cabeça (que você pode obter a raiz analisada do iterador). Até que eu realmente percorri a árvore XML duas vezes no meu aplicativo (uma vez para obter namespaces, o segundo para uma raiz).


descobri como usá-lo, mas não funciona para mim, ainda vejo os namespaces na saída
— taiko

1
Veja o comentário do iMom0 à pergunta do OP . Usando esta função, você obtém o objeto analisado e os meios para consultá-lo com find()e findall(). Você apenas alimenta esses métodos com o ditado do namespaceparse_xml() e usa o prefixo do namespace em suas consultas. Por exemplo:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
— z33k 17/02
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.