Como uso namespaces xml com find/findall no lxml?
-
25-09-2019 - |
Pergunta
Estou tentando analisar o conteúdo em uma planilha ODS OpenOffice. O formato ODS é essencialmente apenas um zipfile com vários documentos. O conteúdo da planilha é armazenado em 'Content.xml'.
import zipfile
from lxml import etree
zf = zipfile.ZipFile('spreadsheet.ods')
root = etree.parse(zf.open('content.xml'))
O conteúdo da planilha está em uma célula:
table = root.find('.//{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table')
Também podemos ir direto para as linhas:
rows = root.findall('.//{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-row')
Os elementos individuais sabem sobre os espaços para nome:
>>> table.nsmap['table']
'urn:oasis:names:tc:opendocument:xmlns:table:1.0'
Como uso os namespaces diretamente no find/findall?
A solução óbvia não funciona.
Tentando tirar as fileiras da tabela:
>>> root.findall('.//table:table')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "lxml.etree.pyx", line 1792, in lxml.etree._ElementTree.findall (src/lxml/lxml.etree.c:41770)
File "lxml.etree.pyx", line 1297, in lxml.etree._Element.findall (src/lxml/lxml.etree.c:37027)
File "/usr/lib/python2.6/dist-packages/lxml/_elementpath.py", line 225, in findall
return list(iterfind(elem, path))
File "/usr/lib/python2.6/dist-packages/lxml/_elementpath.py", line 200, in iterfind
selector = _build_path_iterator(path)
File "/usr/lib/python2.6/dist-packages/lxml/_elementpath.py", line 184, in _build_path_iterator
selector.append(ops[token[0]](_next, token))
KeyError: ':'
Solução
Se root.nsmap
contém o table
Prefixo de espaço para nome da names então você pode:
root.xpath('.//table:table', namespaces=root.nsmap)
findall(path)
aceita {namespace}name
sintaxe em vez de namespace:name
. Portanto path
deve ser pré -processado usando o dicionário de namespace para o {namespace}name
forma antes de passar para findall()
.
Outras dicas
Aqui está uma maneira de obter todos os espaços para nome no documento XML (e supondo que não haja conflito de prefixo).
Eu uso isso ao analisar documentos XML, onde conheço antecipadamente quais são os URLs de namespace e apenas o prefixo.
doc = etree.XML(XML_string)
# Getting all the name spaces.
nsmap = {}
for ns in doc.xpath('//namespace::*'):
if ns[0]: # Removes the None namespace, neither needed nor supported.
nsmap[ns[0]] = ns[1]
doc.xpath('//prefix:element', namespaces=nsmap)
Talvez a primeira coisa a perceber seja que os namespaces sejam definidos em Nível de elemento, não Nível de documento.
Na maioria das vezes, porém, todos os namespaces são declarados no elemento raiz do documento (office:document-content
aqui), o que nos salva analisando tudo para coletar interno xmlns
escopos.
Em seguida, um elemento NSMAP inclui:
- um espaço para nome padrão, com
None
prefixo (nem sempre) - Todos os namespaces de ancestrais, a menos que substituídos.
Se, como Chris mencionou, o espaço para nome padrão não for suportado, você pode usar um Compreensão do dicto para filtrá -lo em uma expressão mais compacta.
Você tem uma sintaxe ligeiramente diferente para xpath eElementPath.
Então, aqui está o código que você pode usar para obter todas as linhas da sua primeira tabela (testadas com: lxml=3.4.2
) :
import zipfile
from lxml import etree
# Open and parse the document
zf = zipfile.ZipFile('spreadsheet.ods')
tree = etree.parse(zf.open('content.xml'))
# Get the root element
root = tree.getroot()
# get its namespace map, excluding default namespace
nsmap = {k:v for k,v in root.nsmap.iteritems() if k}
# use defined prefixes to access elements
table = tree.find('.//table:table', nsmap)
rows = table.findall('table:table-row', nsmap)
# or, if xpath is needed:
table = tree.xpath('//table:table', namespaces=nsmap)[0]
rows = table.xpath('table:table-row', namespaces=nsmap)
Etree não encontrará elementos nomes se houver xmlns
Definições no arquivo XML. Por exemplo:
import lxml.etree as etree
xml_doc = '<ns:root><ns:child></ns:child></ns:root>'
tree = etree.fromstring(xml_doc)
# finds nothing:
tree.find('.//ns:root', {'ns': 'foo'})
tree.find('.//{foo}root', {'ns': 'foo'})
tree.find('.//ns:root')
tree.find('.//ns:root')
Às vezes, esses são os dados que você recebe. Então, o que você pode fazer quando não há espaço para nome?
Minha solução: adicione um.
import lxml.etree as etree
xml_doc = '<ns:root><ns:child></ns:child></ns:root>'
xml_doc_with_ns = '<ROOT xmlns:ns="foo">%s</ROOT>' % xml_doc
tree = etree.fromstring(xml_doc_with_ns)
# finds what you're looking for:
tree.find('.//{foo}root')