Come faccio a usare i namespace XML con find / findall in lxml?
-
25-09-2019 - |
Domanda
Sto cercando di contenuti parse in un foglio di calcolo di OpenOffice ODS. Il formato ODS è essenzialmente solo un file zip con una serie di documenti. Il contenuto del foglio di calcolo è memorizzato in 'content.xml'.
import zipfile
from lxml import etree
zf = zipfile.ZipFile('spreadsheet.ods')
root = etree.parse(zf.open('content.xml'))
Il contenuto del foglio di calcolo è in una cella:
table = root.find('.//{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table')
Possiamo anche andare dritto per le righe:
rows = root.findall('.//{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-row')
I singoli elementi conoscono gli spazi dei nomi:
>>> table.nsmap['table']
'urn:oasis:names:tc:opendocument:xmlns:table:1.0'
Come posso utilizzare gli spazi dei nomi direttamente nel find / findall?
La soluzione più ovvia non funziona.
Cercando di ottenere le righe della tabella:
>>> 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: ':'
Soluzione
Se root.nsmap
contiene il table
namespace prefix allora si potrebbe:
root.xpath('.//table:table', namespaces=root.nsmap)
findall(path)
accetta la sintassi {namespace}name
invece di namespace:name
. Pertanto path
deve essere pre-elaborato utilizzando namespace dizionario alla forma {namespace}name
prima di passarlo a findall()
.
Altri suggerimenti
Ecco un modo per ottenere tutti gli spazi dei nomi nel documento XML (e supponendo non c'è alcun conflitto prefisso).
Io uso questo durante l'analisi di documenti XML in cui io non so in anticipo che cosa gli URL namespace sono, e solo il prefisso.
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)
Forse la prima cosa da notare è che gli spazi dei nomi sono definiti a livello elemento , non a livello di documento.
Il più delle volte, però, tutti gli spazi dei nomi vengono dichiarati nella del documento
elemento radice (office:document-content
qui), che ci salva l'analisi tutto per raccogliere scopi xmlns
interne.
Poi un nsmap elemento comprende:
- uno spazio dei nomi di default, con il prefisso
None
(non sempre) - tutti gli antenati spazi dei nomi, a meno che sovrascritto.
Se, come menzionate ChrisR, lo spazio dei nomi di default non è supportato, è possibile utilizzare un dict comprensione per filtrare fuori in un'espressione più compatto.
Hai una sintassi leggermente diversa per XPath e ElementPath .
Quindi, ecco il codice è possibile utilizzare per ottenere tutte le righe la tua prima della tabella
(Testato con: 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 non troverà elementi namespace se non ci sono definizioni xmlns
nel file XML. Per esempio:
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')
A volte che sono i dati che vi vengono dati. Allora, che cosa si può fare quando non c'è spazio dei nomi?
La mia soluzione:. Aggiungere una
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')