Напишите ElementTree напрямую в zip с кодировкой utf-8 - PullRequest
2 голосов
/ 19 марта 2020

Я хочу изменить большое количество XML. Они хранятся в ZIP-файлах. Исходные XML-файлы кодируются в формате utf-8 (по крайней мере, по предположениям инструмента file в Linux) и имеют правильное объявление XML: <?xml version='1.0' encoding='UTF-8'?>.

Целевые ZIP и Содержащиеся в нем XML-файлы также должны иметь правильное объявление XML. Тем не менее, (по крайней мере, для меня) наиболее очевидный метод (с использованием ElementTree.tostring) не работает.

Вот отдельный пример, который должен работать из коробки. Краткое пошаговое руководство:

  • импорт
  • подготовки (при создании sr c .zip эти ZIP-файлы приведены в моем реальном приложении)
  • фактическая работа программы ( изменение XML), начиная с # read XMLs from zip

Пожалуйста, обратите внимание на нижнюю часть, особенно # APPROACH 1, APPROACH 2, APPROACH 3:

import os
import tempfile
import zipfile
from xml.etree.ElementTree import Element, parse

src_1 = os.path.join(tempfile.gettempdir(), "one.xml")
src_2 = os.path.join(tempfile.gettempdir(), "two.xml")
src_zip = os.path.join(tempfile.gettempdir(), "src.zip")
trgt_appr1_zip = os.path.join(tempfile.gettempdir(), "trgt_appr1.zip")
trgt_appr2_zip = os.path.join(tempfile.gettempdir(), "trgt_appr2.zip")
trgt_appr3_zip = os.path.join(tempfile.gettempdir(), "trgt_appr3.zip")

# file on hard disk that must be used due to ElementTree insufficiencies
tmp_xml_name = os.path.join(tempfile.gettempdir(), "curr_xml.tmp")

# prepare src.zip
tree1 = ElementTree(Element('hello', {'beer': 'good'}))
tree1.write(os.path.join(tempfile.gettempdir(), "one.xml"), encoding="UTF-8", xml_declaration=True)
tree2 = ElementTree(Element('scnd', {'äkey': 'a value'}))
tree2.write(os.path.join(tempfile.gettempdir(), "two.xml"), encoding="UTF-8", xml_declaration=True)

with zipfile.ZipFile(src_zip, 'a') as src:
    with open(src_1, 'r', encoding="utf-8") as one:
        string_representation = one.read()
    # write to zip
    src.writestr(zinfo_or_arcname="one.xml", data=string_representation.encode("utf-8"))
    with open(src_2, 'r', encoding="utf-8") as two:
        string_representation = two.read()
    # write to zip
    src.writestr(zinfo_or_arcname="two.xml", data=string_representation.encode("utf-8"))
os.remove(src_1)
os.remove(src_2)

# read XMLs from zip
with zipfile.ZipFile(src_zip, 'r') as zfile:

    updated_trees = []

    for xml_name in zfile.namelist():

        curr_file = zfile.open(xml_name, 'r')
        tree = parse(curr_file)
        # modify tree
        updated_tree = tree
        updated_tree.getroot().append(Element('new', {'newkey': 'new value'}))
        updated_trees.append((xml_name, updated_tree))

    for xml_name, updated_tree in updated_trees:

        # write to target file
        with zipfile.ZipFile(trgt_appr1_zip, 'a') as trgt1_zip, zipfile.ZipFile(trgt_appr2_zip, 'a') as trgt2_zip, zipfile.ZipFile(trgt_appr3_zip, 'a') as trgt3_zip:

            #
            # APPROACH 1 [DESIRED, BUT DOES NOT WORK]: write tree to zip-file
            # encoding in XML declaration missing
            #
            # create byte representation of elementtree
            byte_representation = tostring(element=updated_tree.getroot(), encoding='UTF-8', method='xml')
            # write XML directly to zip
            trgt1_zip.writestr(zinfo_or_arcname=xml_name, data=byte_representation)

            #
            # APPROACH 2 [WORKS IN THEORY, BUT DOES NOT WORK]: write tree to zip-file
            # encoding in XML declaration is faulty (is 'utf8', should be 'utf-8' or 'UTF-8')
            #
            # create byte representation of elementtree
            byte_representation = tostring(element=updated_tree.getroot(), encoding='utf8', method='xml')
            # write XML directly to zip
            trgt2_zip.writestr(zinfo_or_arcname=xml_name, data=byte_representation)

            #
            # APPROACH 3 [WORKS, BUT LACKS PERFORMANCE]: write to file, then read from file, then write to zip
            #
            # write to file
            updated_tree.write(tmp_xml_name, encoding="UTF-8", method="xml", xml_declaration=True)
            # read from file
            with open(tmp_xml_name, 'r', encoding="utf-8") as tmp:
                string_representation = tmp.read()
            # write to zip
            trgt3_zip.writestr(zinfo_or_arcname=xml_name, data=string_representation.encode("utf-8"))

    os.remove(tmp_xml_name)

APPROACH 3 работает, но он намного более ресурсоемкий, чем два других.

APPROACH 2 - единственный способ получить объект ElementTree, который будет записан с фактическим объявлением XML, которое затем оказывается быть недопустимым (utf8 вместо UTF-8 / utf-8).

APPROACH 1 будет наиболее желательным - но завершится неудачно при чтении позже в конвейере, так как отсутствует объявление XML .

Вопрос: Как мне избавиться от записи всего XML на диск вначале, только чтобы потом прочитать его, записать в zip и удалить после того, как закончите с застежка-молния? Чего мне не хватает?

Ответы [ 2 ]

2 голосов
/ 19 марта 2020

Вы можете использовать io.BytesIO объект. Это позволяет использовать ElementTree.write, избегая при этом экспорта дерева на диск:

import zipfile
from io import BytesIO
from xml.etree.ElementTree import ElementTree, Element

tree = ElementTree(Element('hello', {'beer': 'good'}))
bio = BytesIO()
tree.write(bio, encoding='UTF-8', xml_declaration=True)
with zipfile.ZipFile('/tmp/test.zip', 'w') as z:
    z.writestr('test.xml', bio.getvalue())

Если вы используете Python 3.6 или выше, есть еще более короткое решение: вы можете получить доступный для записи файловый объект из ZipFile объект, который вы можете передать ElementTree.write:

import zipfile
from xml.etree.ElementTree import ElementTree, Element

tree = ElementTree(Element('hello', {'beer': 'good'}))
with zipfile.ZipFile('/tmp/test.zip', 'w') as z:
    with z.open('test.xml', 'w') as f:
        tree.write(f, encoding='UTF-8', xml_declaration=True)

Это также имеет то преимущество, что вы не сохраняете несколько копий дерева в памяти, что может быть актуальной проблемой для больших деревья.

1 голос
/ 19 марта 2020

Единственная вещь, которая действительно отсутствует в первом подходе - это заголовок объявления XML. Для ElementTree.write(...) вы можете использовать xml_declaration, к сожалению, для вашей версии это пока недоступно в ElementTree.tostring.

Начиная с Python 3.8, метод ElementTree.tostring имеет аргумент xml_declaration, см. : https://docs.python.org/3.8/library/xml.etree.elementtree.html

Несмотря на то, что эта реализация недоступна для вас при использовании Python 3.6, вы можете легко скопировать реализацию 3.8 в свой собственный файл Python:

import io

def tostring(element, encoding=None, method=None, *,
             xml_declaration=None, default_namespace=None,
             short_empty_elements=True):
    """Generate string representation of XML element.
    All subelements are included.  If encoding is "unicode", a string
    is returned. Otherwise a bytestring is returned.
    *element* is an Element instance, *encoding* is an optional output
    encoding defaulting to US-ASCII, *method* is an optional output which can
    be one of "xml" (default), "html", "text" or "c14n", *default_namespace*
    sets the default XML namespace (for "xmlns").
    Returns an (optionally) encoded string containing the XML data.
    """
    stream = io.StringIO() if encoding == 'unicode' else io.BytesIO()
    ElementTree(element).write(stream, encoding,
                               xml_declaration=xml_declaration,
                               default_namespace=default_namespace,
                               method=method,
                               short_empty_elements=short_empty_elements)
    return stream.getvalue()

(См. https://github.com/python/cpython/blob/v3.8.0/Lib/xml/etree/ElementTree.py#L1116)

В этом случае вы можете просто использовать подход один:

# create byte representation of elementtree
byte_representation = tostring(element=updated_tree.getroot(), encoding='UTF-8', method='xml', xml_declaration=True)
# write XML directly to zip
trgt1_zip.writestr(zinfo_or_arcname=xml_name, data=byte_representation)
...