Как использовать пространства имен xml с find/findall в lxml?
Я пытаюсь разобрать содержимое в электронной таблице OpenSffice ODS. Формат ods - это просто zip файл с несколькими документами. Содержимое электронной таблицы хранится в 'content.xml'.
import zipfile
from lxml import etree
zf = zipfile.ZipFile('spreadsheet.ods')
root = etree.parse(zf.open('content.xml'))
Содержимое электронной таблицы находится в ячейке:
table = root.find('.//{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table')
Мы также можем перейти прямо к строкам:
rows = root.findall('.//{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-row')
Отдельные элементы знают об пространствах имен:
>>> table.nsmap['table']
'urn:oasis:names:tc:opendocument:xmlns:table:1.0'
Как использовать пространства имен непосредственно в find/findall?
Очевидное решение не работает.
Попытка получить строки из таблицы:
>>> 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: ':'
Ответы
Ответ 1
Если root.nsmap
содержит префикс пространства table
, тогда вы можете:
root.xpath('.//table:table', namespaces=root.nsmap)
findall(path)
принимает {namespace}name
синтаксис вместо namespace:name
. Поэтому path
должен быть предварительно обработан с использованием словарного словаря до формы {namespace}name
, прежде чем передать его в findall()
.
Ответ 2
Здесь можно получить все пространства имен в XML-документе (и предположить, что конфликт префикса отсутствует).
Я использую это при анализе XML-документов, где я заранее знаю, что такое URL-адреса пространства имен, и только префикс.
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)
Ответ 3
Возможно, первое, что следует заметить, это то, что пространства имен определяются на уровне элемента, а не на уровне документа.
Чаще всего, однако, все пространства имен объявляются в корневом элементе документа (office:document-content
здесь), что избавляет нас от анализа всего этого для сбора внутренних областей xmlns
.
Тогда элемент nsmap включает в себя:
- пространство имен по умолчанию с префиксом
None
(не всегда) - пространства имен всех предков, если они не переопределены.
Если, как упомянул ChrisR, пространство имен по умолчанию не поддерживается, вы можете использовать понимание dict, чтобы отфильтровать его в более компактном выражении.
У вас немного другой синтаксис для xpath и ElementPath.
Итак, вот код, который вы можете использовать для получения всех ваших первых строк таблицы (проверено с помощью: 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)
Ответ 4
Etree не найдет элементы пространства имен, если в файле XML нет определений xmlns
. Например:
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')
Иногда это данные, которые вам дают. Итак, что вы можете сделать, когда нет пространства имен?
Мое решение: добавить один.
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')