Python: обновляйте XML файл с помощью ElementTree, сохраняя при этом максимально возможный макет

У меня есть документ, который использует пространство имен XML, для которого я хочу увеличить /group/house/dogs на один: (файл называется houses.xml)

<?xml version="1.0"?>
<group xmlns="http://dogs.house.local">
    <house>
            <id>2821</id>
            <dogs>2</dogs>
    </house>
</group>

Мой текущий результат, используя следующий код: (созданный файл называется houses2.xml)

<ns0:group xmlns:ns0="http://dogs.house.local">
    <ns0:house>
        <ns0:id>2821</ns0:id>
        <ns0:dogs>3</ns0:dogs>
    </ns0:house>
</ns0:group>

Я хотел бы исправить две вещи (если это возможно, используя ElementTree.Если это не так, я буду благодарен за предложение относительно того, что я должен использовать вместо этого):

  • Я хочу сохранить строку <?xml version="1.0"?>.
  • Я не хочу префикс всех тегов, я хотел бы сохранить его как есть.

В заключение, Я не хочу связываться с документом больше, чем я должен.

Мой текущий код (который работает, кроме вышеупомянутых недостатков), генерирует приведенный выше результат.

Я сделал функцию утилиты, которая загружает XML файл с помощью ElementTree и возвращает elementTree и пространство имен (так как я не хочу жестко кодировать пространство имен и готов принять на себя риск):

def elementTreeRootAndNamespace(xml_file):
    from xml.etree import ElementTree
    import re
    element_tree = ElementTree.parse(xml_file)

    # Search for a namespace on the root tag
    namespace_search = re.search('^({\S+})', element_tree.getroot().tag)
    # Keep the namespace empty if none exists, if a namespace exists set
    # namespace to {namespacename}
    namespace = ''
    if namespace_search:
        namespace = namespace_search.group(1)

    return element_tree, namespace

Это мой код для обновления количества собак и сохранения его в новом файле houses2.xml:

elementTree, namespace = elementTreeRootAndNamespace('houses.xml')

# Insert the namespace before each tag when when finding current number of dogs,
# as ElementTree requires the namespace to be prefixed within {...} when a
# namespace is used in the document.
dogs = elementTree.find('{ns}house/{ns}dogs'.format(ns = namespace))

# Increase the number of dogs by one
dogs.text = str(int(dogs.text) + 1)

# Write the result to the new file houses2.xml.
elementTree.write('houses2.xml')

Ответ 1

Круговое отключение, к сожалению, не является тривиальной проблемой. В XML обычно невозможно сохранить исходный документ, если вы не используете специальный парсер (например, DecentXML, но для Java).

В зависимости от ваших потребностей у вас есть следующие возможности:

  • Если вы контролируете источник, и вы можете защитить свой код с помощью модульных тестов, вы можете написать свой собственный простой синтаксический анализатор. Этот анализатор не принимает XML, а только ограниченное подмножество. Вы можете, например, прочитать весь документ в виде строки, а затем использовать операции строки Python для поиска <dogs> и заменить что-либо до следующего <. Hack? Да.

  • Вы можете отфильтровать вывод. XML допускает строку <ns0: только в одном месте, поэтому вы можете искать и заменять ее на <, а затем то же самое с <group xmlns:ns0="<group xmlns=". Это довольно безопасно, если вы не можете CDATA в своем XML.

  • Вы можете написать собственный простой XML-синтаксический анализатор. Прочитайте ввод как строку, а затем создайте Элементы для каждой пары <> плюс их позиции на входе. Это позволяет быстро принимать входные данные, но работает только для небольших входов.

Ответ 2

Решение на основе XML для этой проблемы заключается в написании вспомогательного класса для ElementTree, который:

  • Захватывает строку объявления XML перед синтаксическим анализом, поскольку ElementTree на момент написания не может написать строку декларации XML, не записывая также атрибут кодирования (я проверил источник).
  • Разбирает входной файл один раз, захватывает пространство имен корневого элемента. Регистрирует это пространство имен с ElementTree как имеющее пустую строку в качестве префикса. Когда это будет сделано, исходный файл снова будет проанализирован с помощью ElementTree с этой новой настройкой.

У него есть один главный недостаток:

  • XML-комментарии потеряны. Я узнал, что это неприемлемо для этой ситуации (изначально я не думал, что входные данные имеют какие-либо комментарии, но, оказывается, это так).

Мой класс-помощник с примером:

from xml.etree import ElementTree as ET
import re


class ElementTreeHelper():
    def __init__(self, xml_file_name):
        xml_file = open(xml_file_name, "rb")

        self.__parse_xml_declaration(xml_file)

        self.element_tree = ET.parse(xml_file)
        xml_file.seek(0)

        root_tag_namespace = self.__root_tag_namespace(self.element_tree)
        self.namespace = None
        if root_tag_namespace is not None:
            self.namespace = '{' + root_tag_namespace + '}'
            # Register the root tag namespace as having an empty prefix, as
            # this has to be done before parsing xml_file we re-parse.
            ET.register_namespace('', root_tag_namespace)
            self.element_tree = ET.parse(xml_file)

    def find(self, xpath_query):
        return self.element_tree.find(xpath_query)

    def write(self, xml_file_name):
        xml_file = open(xml_file_name, "wb")
        if self.xml_declaration_line is not None:
            xml_file.write(self.xml_declaration_line + '\n')

        return self.element_tree.write(xml_file)

    def __parse_xml_declaration(self, xml_file):
        first_line = xml_file.readline().strip()
        if first_line.startswith('<?xml') and first_line.endswith('?>'):
            self.xml_declaration_line = first_line
        else:
            self.xml_declaration_line = None
        xml_file.seek(0)

    def __root_tag_namespace(self, element_tree):
        namespace_search = re.search('^{(\S+)}', element_tree.getroot().tag)
        if namespace_search is not None:
            return namespace_search.group(1)
        else:
            return None


def __main():
    el_tree_hlp = ElementTreeHelper('houses.xml')

    dogs_tag = el_tree_hlp.element_tree.getroot().find(
                   '{ns}house/{ns}dogs'.format(
                         ns=el_tree_hlp.namespace))
    one_dog_added = int(dogs_tag.text.strip()) + 1
    dogs_tag.text = str(one_dog_added)

    el_tree_hlp.write('hejsan.xml')

if __name__ == '__main__':
    __main()

Выход:

<?xml version="1.0"?>
<group xmlns="http://dogs.house.local">
    <house>
            <id>2821</id>
            <dogs>3</dogs>
    </house>
</group>

Если у кого-то есть улучшение этого решения, не стесняйтесь хватать код и улучшать его.

Ответ 3

когда Save xml добавляет аргумент default_namespace, легко избежать ns0, в моем коде

key code: xmltree.write(xmlfiile, "utf-8", default_namespace = xmlnamespace)

if os.path.isfile(xmlfiile):
            xmltree = ET.parse(xmlfiile)
            root = xmltree.getroot()
            xmlnamespace = root.tag.split('{')[1].split('}')[0]  //get namespace

            initwin=xmltree.find("./{"+ xmlnamespace +"}test")
            initwin.find("./{"+ xmlnamespace +"}content").text = "aaa"
            xmltree.write(xmlfiile,"utf-8",default_namespace=xmlnamespace)

Ответ 4

etree из lxml предоставляет эту функцию.

  • elementTree.write('houses2.xml',encoding = "UTF-8",xml_declaration = True) помогает вам не пропускать декларацию

  • При записи в файл он не меняет пространства имен.

http://lxml.de/parsing.html является ссылкой для его учебника.

P.S: lxml следует устанавливать отдельно.