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?

KevinLeng
fonte
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
nonagon
fonte
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)
user2212280
fonte
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
wimous
fonte
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)

barny
fonte
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.

lijat
fonte
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)
tzp
fonte
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.

ericspod
fonte
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.

Husa
fonte
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).

z33k
fonte
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