Analisando XML com espaço para nome em Python via 'ElementTree'

163

Eu tenho o seguinte XML que eu quero analisar usando Python ElementTree:

<rdf:RDF xml:base="http://dbpedia.org/ontology/"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:owl="http://www.w3.org/2002/07/owl#"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
    xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
    xmlns="http://dbpedia.org/ontology/">

    <owl:Class rdf:about="http://dbpedia.org/ontology/BasketballLeague">
        <rdfs:label xml:lang="en">basketball league</rdfs:label>
        <rdfs:comment xml:lang="en">
          a group of sports teams that compete against each other
          in Basketball
        </rdfs:comment>
    </owl:Class>

</rdf:RDF>

Quero encontrar todas as owl:Classtags e extrair o valor de todas as rdfs:labelinstâncias dentro delas. Estou usando o seguinte código:

tree = ET.parse("filename")
root = tree.getroot()
root.findall('owl:Class')

Por causa do espaço para nome, estou recebendo o seguinte erro.

SyntaxError: prefix 'owl' not found in prefix map

Tentei ler o documento em http://effbot.org/zone/element-namespaces.htm, mas ainda não consigo fazer isso funcionar, pois o XML acima tem vários namespaces aninhados.

Por favor, deixe-me saber como alterar o código para encontrar todas as owl:Classtags.

Sudar
fonte

Respostas:

226

O ElementTree não é muito inteligente sobre namespaces. Você precisa dar o .find(), findall()e iterfind()métodos de um dicionário namespace explícito. Isso não está muito bem documentado:

namespaces = {'owl': 'http://www.w3.org/2002/07/owl#'} # add more as needed

root.findall('owl:Class', namespaces)

Os prefixos são procurados apenas no namespacesparâmetro que você passa. Isso significa que você pode usar qualquer prefixo de espaço para nome que desejar; a API separa a owl:parte, pesquisa o URL do espaço para nome correspondente no namespacesdicionário e altera a pesquisa para procurar a expressão XPath {http://www.w3.org/2002/07/owl}Class. Você também pode usar a mesma sintaxe, é claro:

root.findall('{http://www.w3.org/2002/07/owl#}Class')

Se você pode mudar para a lxmlbiblioteca, as coisas são melhores; essa biblioteca suporta a mesma API ElementTree, mas coleta espaços de nome para você em um .nsmapatributo nos elementos.

Martijn Pieters
fonte
7
Obrigado. Alguma idéia de como posso obter o namespace diretamente do XML, sem codificá-lo? Ou como posso ignorá-lo? Eu tentei findall ('{*} Class'), mas não funcionará no meu caso.
Kostanos
7
Você teria que varrer a árvore em busca de xmlnsatributos; como indicado na resposta, lxmlfaz isso por você, o xml.etree.ElementTreemódulo não. Mas se você está tentando corresponder a um elemento específico (já codificado), também está tentando corresponder a um elemento específico em um espaço para nome específico. Esse espaço para nome não será alterado entre os documentos mais do que o nome do elemento. Você também pode codificar isso com o nome do elemento.
Martijn Pieters
14
@ Jon: register_namespaceapenas influencia a serialização, não a pesquisa.
Martijn Pieters
5
Uma pequena adição que pode ser útil: ao usar em cElementTreevez de ElementTree, findallnão usará espaços para nome como argumento de palavra-chave, mas simplesmente como argumento normal, ou seja, use ctree.findall('owl:Class', namespaces).
egpbos
2
@ Bludwarf: Os documentos mencionam isso (agora, se não quando você escreveu isso), mas você deve lê-los com atenção. Consulte a seção Analisando XML com Namespaces : há um exemplo contrastando o uso de findallsem e depois com o namespaceargumento, mas o argumento não é mencionado como um dos argumentos para o método method na seção Objeto Elemento .
Wilson F
57

Veja como fazer isso com o lxml sem precisar codificar os namespaces ou digitalizar o texto para eles (como Martijn Pieters menciona):

from lxml import etree
tree = etree.parse("filename")
root = tree.getroot()
root.findall('owl:Class', root.nsmap)

ATUALIZAÇÃO :

Cinco anos depois, ainda estou enfrentando variações desse problema. O lxml ajuda como mostrei acima, mas não em todos os casos. Os comentaristas podem ter um ponto válido em relação a essa técnica quando se trata de mesclar documentos, mas acho que a maioria das pessoas está tendo dificuldade em simplesmente pesquisar documentos.

Aqui está outro caso e como eu lidei com isso:

<?xml version="1.0" ?><Tag1 xmlns="http://www.mynamespace.com/prefix">
<Tag2>content</Tag2></Tag1>

xmlns sem prefixo significa que tags não prefixadas obtêm esse espaço de nome padrão. Isso significa que, ao procurar o Tag2, é necessário incluir o espaço para nome para encontrá-lo. No entanto, o lxml cria uma entrada nsmap com None como chave e não consegui encontrar uma maneira de procurá-la. Então, eu criei um novo dicionário de namespace como este

namespaces = {}
# response uses a default namespace, and tags don't mention it
# create a new ns map using an identifier of our choice
for k,v in root.nsmap.iteritems():
    if not k:
        namespaces['myprefix'] = v
e = root.find('myprefix:Tag2', namespaces)
Brad Dre
fonte
3
O URL completo do namespace é o identificador do namespace que você deve codificar. O prefixo local ( owl) pode mudar de arquivo para arquivo. Portanto, fazer o que essa resposta sugere é uma péssima idéia.
Matti Virkkunen
1
@MattiVirkkunen exatamente se a definição da coruja pode mudar de arquivo para arquivo, não devemos usar a definição definida em cada arquivo em vez de codificá-la?
Loïc Faure-Lacroix
@ LoïcFaure-Lacroix: Normalmente, as bibliotecas XML permitem abstrair essa parte. Você não precisa nem saber ou se importar com o prefixo usado no arquivo, apenas define seu próprio prefixo para fins de análise ou apenas usa o nome completo do espaço para nome.
Matti Virkkunen
esta resposta ajudou-me a pelo menos conseguir usar a função find. Não há necessidade de criar seu próprio prefixo. Acabei de fazer key = list (root.nsmap.keys ()) [0] e adicionei a chave como prefixo: root.find (f '{key}: Tag2', root.nsmap)
Eelco van Vliet
30

Nota : Esta é uma resposta útil para a biblioteca padrão ElementTree do Python sem usar espaços para nome codificados.

Para extrair prefixos e URI do espaço para nome dos dados XML, você pode usar a ElementTree.iterparsefunção, analisando apenas os eventos start do espaço para nome ( start-ns ):

>>> from io import StringIO
>>> from xml.etree import ElementTree
>>> my_schema = u'''<rdf:RDF xml:base="http://dbpedia.org/ontology/"
...     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
...     xmlns:owl="http://www.w3.org/2002/07/owl#"
...     xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
...     xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
...     xmlns="http://dbpedia.org/ontology/">
... 
...     <owl:Class rdf:about="http://dbpedia.org/ontology/BasketballLeague">
...         <rdfs:label xml:lang="en">basketball league</rdfs:label>
...         <rdfs:comment xml:lang="en">
...           a group of sports teams that compete against each other
...           in Basketball
...         </rdfs:comment>
...     </owl:Class>
... 
... </rdf:RDF>'''
>>> my_namespaces = dict([
...     node for _, node in ElementTree.iterparse(
...         StringIO(my_schema), events=['start-ns']
...     )
... ])
>>> from pprint import pprint
>>> pprint(my_namespaces)
{'': 'http://dbpedia.org/ontology/',
 'owl': 'http://www.w3.org/2002/07/owl#',
 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
 'xsd': 'http://www.w3.org/2001/XMLSchema#'}

Em seguida, o dicionário pode ser passado como argumento para as funções de pesquisa:

root.findall('owl:Class', my_namespaces)
Davide Brunato
fonte
1
Isso é útil para aqueles que não têm acesso ao lxml e não desejam codificar o namespace.
delrocco
1
Eu recebi o erro: ValueError: write to closedpara esta linha filemy_namespaces = dict([node for _, node in ET.iterparse(StringIO(my_schema), events=['start-ns'])]). Alguma idéia quer errado?
Yuli
Provavelmente, o erro está relacionado à classe io.StringIO, que recusa seqüências de caracteres ASCII. Eu testei minha receita com Python3. Adicionando o prefixo da string unicode 'u' à string de amostra, ele também funciona com o Python 2 (2.7).
Davide Brunato
Em vez de dict([...])você também pode usar a compreensão do ditado.
Arminius
Em vez de StringIO(my_schema)você também pode colocar o nome do arquivo XML.
JustAC0der
6

Eu tenho usado um código semelhante a este e descobri que sempre vale a pena ler a documentação ... como sempre!

findall () encontrará apenas elementos que são filhos diretos da tag atual . Então, não é realmente tudo.

Pode valer a pena tentar fazer com que seu código funcione com o seguinte, especialmente se você estiver lidando com arquivos xml grandes e complexos, para que os subelementos (etc.) também sejam incluídos. Se você se conhece onde estão os elementos no seu xml, acho que tudo ficará bem! Apenas pensei que valia a pena lembrar.

root.iter()

ref: https://docs.python.org/3/library/xml.etree.elementtree.html#finding-interesting-elements "Element.findall () encontra apenas elementos com uma tag que são filhos diretos do elemento atual. Element.find () localiza o primeiro filho com uma tag específica e Element.text acessa o conteúdo de texto do elemento. Element.get () acessa os atributos do elemento: "

MJM
fonte
6

Para obter o espaço para nome em seu formato, por exemplo {myNameSpace}, você pode fazer o seguinte:

root = tree.getroot()
ns = re.match(r'{.*}', root.tag).group(0)

Dessa forma, você pode usá-lo posteriormente em seu código para encontrar nós, por exemplo, usando interpolação de strings (Python 3).

link = root.find(f"{ns}link")
Bram Vanroy
fonte
0

Minha solução é baseada no comentário de @Martijn Pieters:

register_namespace influencia apenas a serialização, não a pesquisa.

Portanto, o truque aqui é usar dicionários diferentes para serialização e pesquisa.

namespaces = {
    '': 'http://www.example.com/default-schema',
    'spec': 'http://www.example.com/specialized-schema',
}

Agora, registre todos os namespaces para análise e gravação:

for name, value in namespaces.iteritems():
    ET.register_namespace(name, value)

Para pesquisar ( find(), findall(), iterfind()) precisamos de um prefixo não vazio. Passe essas funções para um dicionário modificado (aqui modifico o dicionário original, mas isso só deve ser feito após o registro dos namespaces).

self.namespaces['default'] = self.namespaces['']

Agora, as funções da find()família podem ser usadas com o defaultprefixo:

print root.find('default:myelem', namespaces)

mas

tree.write(destination)

não usa nenhum prefixo para elementos no espaço para nome padrão.

peter.slizik
fonte