Python заменяет XML-текст Escape-последовательностью

У меня есть стороннее приложение, которое анализирует магические строки в файле XML, хотя оно должно обрабатывать их как символьные литералы. В качестве примера предположим, что мой XML содержит следующий сегмент:

<element>Sentence containing magicString</element>

Чтобы стороннее приложение не анализировало magicString как команду, я хочу преобразовать этот фрагмент xml в:

<element>Sentence containing &#109;agicString</element>

Как я могу добиться этого в Python, не выполняя глобальный поиск-замену (например, могут быть элементы с именем magicString, которые нельзя переименовать, или XML недействителен)? Следующее иллюстрирует то, что я пытался:

from xml.etree import ElementTree
xml = ElementTree.parse(xmlPath)
element = xml.find('.//grandparent/parent/element'):
element.text = '&#109;agicString'
xml.write(xmlPath)

Проблема в том, что при присвоении свойства Element.text экранируется текст, поэтому результатом является XML-файл со следующим содержимым:

<element>&amp;#109;agicString</element>

🤔 А знаете ли вы, что...
С Python можно создавать ботов для социальных сетей и мессенджеров.


1
79
2

Ответы:

Мне понравилось решение Ицхака и Дэниела. Возможно, вам тоже поможет хак с saxutilsссылка:

import xml.etree.ElementTree as ET
from xml.sax.saxutils import escape

xml_ = """<element>Sentence containing magicString</element>"""

root = ET.fromstring(xml_)
print('Origin:', root.text)

a = escape(f"{root.text}", {"magicString": "&#109;agicString"})
print('Escaped:', a)
root.text = a

with open("w_xml", 'w') as f:
    f.write(ET.tostring(root).decode().replace("&amp;","&")) 
    # <element>Sentence containing &#109;agicString</element>

Выход:

Origin: Sentence containing magicString
Escaped: Sentence containing &#109;agicString

Решено

Вот решение на основе XSLT.

Он будет искать только текстовые узлы, содержащие «magicString». Элементы XML не затрагиваются.

Амперсанд всегда должен обозначаться как &amp; в XML. В противном случае XML был бы неправильно сформирован.

Вы можете скачать движок Saxon Python XSLT 3.0 здесь: Загрузки Python

Saxon Home Edition (HE) предоставляется бесплатно.

Входной XML

<root>
    <magicString>Another sentence</magicString>
    <element>Sentence containing magicString</element>
    <city>Miami</city>
</root>

XSLT 3.0

<?xml version = "1.0"?>
<xsl:stylesheet version = "3.0"
                xmlns:xsl = "http://www.w3.org/1999/XSL/Transform">
    <xsl:output method = "xml" omit-xml-declaration = "yes"
                encoding = "UTF-8" indent = "yes"/>
    <xsl:strip-space elements = "*"/>

    <xsl:param name = "findMe" select = "'magicString'"/>

    <!--Identity transform-->
    <xsl:mode on-no-match = "shallow-copy"/>

    <xsl:template match = "text()[contains(., $findMe)]">
        <xsl:value-of select = "replace(., $findMe, concat('&amp;#109;', substring($findMe, 2, 100)))"/>
    </xsl:template>
</xsl:stylesheet>

Выходной XML

<root>
  <magicString>Another sentence</magicString>
  <element>Sentence containing &amp;#109;agicString</element>
  <city>Miami</city>
</root>

Питон

from saxonche import *

output_file = 'output.xml'

with PySaxonProcessor(license=False) as proc:
    print(proc.version)
    try:
        xsltproc = proc.new_xslt30_processor()
        document = proc.parse_xml(xml_file_name='input.xml')
        executable = xsltproc.compile_stylesheet(stylesheet_file = "process.xslt")

        output = executable.transform_to_string(xdm_node=document)

        with open(output_file, 'wb') as f:
            f.write(output.encode('utf-8'))

    except PySaxonApiError as err:
        print('Error during function call', err)

Вот модифицированная версия XSLT, в которой используется другой символ (в данном случае &#178; (надстрочный индекс 2)) и карта символов для сопоставления его с нужной ссылкой на объект. Вам просто нужно использовать символ, которого еще нет в вашем вводе, чтобы его случайно не заменили.

<xsl:stylesheet version = "3.0" xmlns:xsl = "http://www.w3.org/1999/XSL/Transform" expand-text = "yes">
    <xsl:output indent = "yes" use-character-maps = "magic"/>
    <xsl:strip-space elements = "*"/>
    
    <xsl:character-map name = "magic">
        <xsl:output-character character = "&#178;" string = "&amp;#109;"/>
    </xsl:character-map>
    
    <xsl:param name = "findMe" select = "'magicString'"/>
    
    <xsl:mode on-no-match = "shallow-copy"/>
    
    <xsl:template match = "text()[contains(., $findMe)]">
        <xsl:value-of select = "replace(., $findMe, '&#178;'||substring($findMe, 2))"/>
    </xsl:template>
    
</xsl:stylesheet>

Это производит:

<root>
   <magicString>Another sentence</magicString>
   <element>Sentence containing &#109;agicString</element>
   <city>Miami</city>
</root>

Снимок экрана Python/XSLT в PyCharm, дающий желаемый результат: