2025, Dec 30 12:03
Как сохранить переводы строк в атрибутах XML: причины и решения
Как сохранить переводы строк в атрибутах XML при разборе и записи: почему lxml и xml.etree их нормализуют, рабочий обход с regex и BeautifulSoup, советы.
Сохранение буквальных переносов строк внутри значений атрибутов XML может преподнести сюрприз: разбираете документ, записываете его обратно — и внезапно переносы строк исчезают. Если обрабатывать файл через xml.etree или lxml.etree, парсер нормализует пробельные символы в атрибутах, включая переводы строк. Задача — изменить XML так, чтобы встроенные переносы строк остались нетронутыми в итоговом выводе.
Демонстрация проблемы
Возьмём минимальный XML с многострочным значением атрибута:
<?xml version='1.0' encoding='UTF-8'?>
<xml>
<test value="This is
a test
with line breaks
"/>
</xml>
Теперь выполним простой разбор и запись с помощью lxml:
import lxml.etree as LX
with open("input.xml", "r", encoding="utf-8") as fh:
tree = LX.parse(fh)
top = tree.getroot()
out_doc = LX.ElementTree(top)
out_doc.write("output.xml", encoding="utf-8", xml_declaration=True)
В результате файл теряет переносы строк внутри атрибута и заменяет их пробелами:
<?xml version='1.0' encoding='UTF-8'?>
<xml>
<test value="This is a test with line breaks "/>
</xml>
Почему так происходит
Это поведение соответствует спецификации XML: при нормализации значений атрибутов часть пробелов схлопывается, а переводы строк преобразуются. На практике корректный XML‑парсер заменяет переносы строк в значениях атрибутов во время разбора. И xml.etree, и lxml.etree следуют этому правилу, поэтому исходные переводы строк не сохраняются после обработки документа парсером.
Рабочий способ сохранить переводы строк
Чтобы не потерять исходный текст атрибута, сначала прочитайте XML как обычный текст, извлеките значение атрибута до любого парсинга, а после разбора подставьте это сырое значение обратно в элемент. Один из вариантов — небольшое регулярное выражение, которое заберёт значение атрибута как есть, после чего можно применить BeautifulSoup для разбора и обновления XML.
import re
from bs4 import BeautifulSoup
# Читаем XML как обычный текст
with open("input.xml", encoding="utf-8") as fh:
doc_text = fh.read()
# Захватываем многострочное значение атрибута без изменений
m = re.search(r'value="(.*?)"', doc_text, re.DOTALL)
preserved_val = m.group(1) if m else None
print("Raw string: ", repr(preserved_val))
print()
# Разбираем XML и возвращаем исходное значение атрибута
dom = BeautifulSoup(doc_text, "xml")
node = dom.find("test")
if preserved_val is not None:
node["value"] = preserved_val
# Показываем результат
print(dom.prettify())
Результат:
Raw string: 'This is\n a test\n\n with line breaks\n '
<?xml version="1.0" encoding="utf-8"?>
<xml>
<test value="This is
a test
with line breaks
"/>
</xml>
Такой подход сохраняет переводы строк в значении атрибута. Согласно обсуждениям вокруг спецификации, на практике, если вы формируете XML сами, можно использовать сущность для представления перевода строки. Аналогично, после того как вы захватили исходное многострочное значение и присвоили его атрибуту, сериализатор может вывести для этих разрывов строк ; при необходимости замените их затем на нужную вам последовательность перевода строки. В ряде случаев можно обойтись одной лишь регулярной выборкой и вашей текущей XML‑библиотекой, чтобы установить значение атрибута, вовсе не прибегая к BeautifulSoup.
Почему это стоит знать
При пакетной правке XML тонкости сериализации могут незаметно изменить представление данных. Если последующие инструменты ожидают буквальные переводы строк внутри атрибутов или вам важно сохранить точное исходное форматирование, нормализация парсером помешает. Осознание того, что это предписанное поведение, помогает выбрать процесс, который сохраняет сырой текст там, где это критично.
Выводы и практические советы
Если нужно править XML, не сплющивая переводы строк в значениях атрибутов, извлекайте эти значения прямо из исходного текста до начала парсинга, а затем возвращайте их после построения или обхода дерева. Если вы генерируете XML сами и должны надёжно закодировать перевод строки внутри атрибута, используйте сущность . Протестируйте весь конвейер целиком, чтобы убедиться, что результат соответствует ожиданиям, и при необходимости выполните финальную замену, приводя переводы строк к нужному формату.