1. Зачем нам XML, если есть JSON?
XML — штука, которую многие впервые видят в виде «страшных скобочек», а потом внезапно встречают в реальных интеграциях: банковские выгрузки, корпоративные шины, конфиги, старые (и не очень) протоколы. Наша цель сегодня — научиться читать XML глазами разработчика: понимать, что в нём является данными, что — структурой, и почему иногда «ну вроде же красиво написано» превращается в ошибку парсера.
Если вы сейчас думаете «JSON же проще, зачем эти угловые скобки из 2007-го», то вы мыслите совершенно нормально. Но реальность любит приносить нам не только свежие технологии, но и те, которые «уже всё настроено, работает 15 лет, не трогай». XML — как тот сервер в кладовке: может выглядеть пугающе, но он выполняет важную работу и иногда даже не шумит.
XML часто выбирают там, где нужна строгая структурированность, совместимость со старыми системами или где исторически много инструментов вокруг: схемы, трансформации, сложные запросы. При этом XML остаётся обычным текстом — его можно хранить в файле, передавать по сети, логировать, сравнивать (осторожно), и да, случайно сломать одним лишним символом.
Важно сразу отделить два вопроса. Первый: «XML можно распарсить?» — это про корректность формата (well‑formed). Второй: «XML содержит правильные данные для нашей задачи?» — это про бизнес‑валидацию. Сегодня мы занимаемся первым: учимся читать XML как формат и понимать, почему он может не распарситься вообще.
2. Базовая структура XML
Элемент и дерево
Когда вы смотрите на XML, полезно переключить мозг в режим «я вижу дерево». Не строку, не текст, не набор символов, а именно иерархию: у нас есть корень, у корня — дети, у детей — свои дети, и так далее. В этом смысле XML очень похож на JSON: там тоже дерево. Просто в XML дерево записано через теги.
Минимальный «кирпичик» XML — элемент. У элемента есть имя (название тега), могут быть атрибуты и содержимое (текст и/или вложенные элементы). Сам элемент записывается как пара тегов: открывающий и закрывающий.
Вот самый минимальный пример XML (один корневой элемент, внутри текст):
fun main() {
val xml = "<message>Hello</message>"
println(xml) // <message>Hello</message>
}
Визуально это похоже на «обёртку вокруг текста». Но если думать деревом, то это:
- корень: message
- текст внутри: Hello
Для многострочных XML удобнее использовать """...""" и потом trimIndent(), чтобы отступы были красивыми и в коде, и в результате:
fun main() {
val xml = """
<message>
Hello
</message>
""".trimIndent()
println(xml)
// <message>
// Hello
// </message>
}
Обратите внимание: переносы строк и пробелы становятся частью текста. Мы позже подробно обсудим, почему это важно (и почему иногда внезапно приходится писать trim()).
Вложенность и один корень
Когда XML становится чуть сложнее, в нём появляется вложенность. Это буквально «один элемент внутри другого». И здесь начинаются правила, похожие на правильно расставленные скобки в математике: открыли — закройте, и закройте в правильном порядке. Компьютер очень педантичен: он не примет «почти правильно». У него нет режима «ну я понял, что ты хотел сказать».
Пример: пользователь с id и именем внутри:
fun main() {
val xml = """
<user id="42">
<name>Ann</name>
</user>
""".trimIndent()
println(xml)
}
Если представить это деревом, получится так:
flowchart TD
U["user (id=42)"]
N["name"]
T["текст: Ann"]
U --> N --> T
Здесь user — корневой элемент (root). А name — вложенный элемент (child).
У XML есть жёсткое правило: в документе должен быть один корневой элемент. Не два «соседа», не «ну у меня же два блока данных». Если хочется два блока — вы обязаны завернуть их в один общий корень.
Вот пример, который выглядит «логично», но считается некорректным XML-документом:
fun main() {
val broken = """
<a>1</a>
<b>2</b>
""".trimIndent()
println(broken)
// Это НЕ единый XML-документ: два корневых элемента
}
А вот так — уже нормально (есть один корень root):
fun main() {
val ok = """
<root>
<a>1</a>
<b>2</b>
</root>
""".trimIndent()
println(ok)
}
3. Атрибуты и пустые элементы
Когда начинаешь работать с XML, атрибуты — это первая вещь, которая кажется «очевидной», а потом слегка троллит новичков. Атрибут — это пара ключ="значение" внутри открывающего тега. В примере выше id="42" — атрибут элемента user.
Смысл атрибутов обычно такой: «маленькие свойства элемента». Например, идентификатор, код, флаг, тип. Но важно помнить: значение атрибута в XML — это строка. Даже если там написано 42, парсер видит это как "42". В Kotlin вы потом будете сами решать: оставить строкой или преобразовать в число (toIntOrNull()), как мы делали раньше.
Пример с двумя атрибутами:
fun main() {
val xml = """<file name="report.xml" size="128" />"""
println(xml) // <file name="report.xml" size="128" />
}
Здесь мы встречаем ещё одну форму элемента: пустой элемент (self‑closing tag) — <file ... />. Это просто сокращённая запись для случая, когда внутри элемента ничего нет. То есть вот эти две записи эквивалентны по смыслу:
fun main() {
val a = "<file name=\\"report.xml\\" />"
val b = "<file name=\\"report.xml\\"></file>"
println(a) // <file name="report.xml" />
println(b) // <file name="report.xml"></file>
}
Обратите внимание на экранирование кавычек \" в Kotlin‑строке. В реальном коде вы чаще будете использовать тройные кавычки, чтобы не экранировать:
fun main() {
val xml = """<file name="report.xml" />"""
println(xml) // <file name="report.xml" />
}
4. Текст и пробелы как данные
Когда мы форматируем XML красивыми отступами, нам кажется, что пробелы — это «просто для людей». Но парсер видит иначе: пробелы и переносы строк — это текстовые узлы (про узлы подробно будем говорить на следующей лекции, но идею важно почувствовать уже сейчас). Поэтому если вы написали:
<name>
Ann
</name>
то внутри name есть текст, который выглядит примерно как "\n Ann\n" (перенос строки, пробелы, Ann, перенос строки). В реальности всё чуть зависит от конкретного парсера, но общий смысл такой: форматирование становится частью текста.
Практически это означает простую вещь: когда вы извлекаете текст из XML, вы почти всегда делаете .trim(). Поэтому привычка «нормализуй строку после чтения» (которую мы отрабатывали на вводе и JSON) в XML встречается постоянно.
Мини‑пример в Kotlin, чтобы увидеть «невидимые» символы: мы просто распечатаем строку и её длину.
fun main() {
val text = """
Ann
""".trimIndent()
println(text) // Ann
println(text.length) // 3
}
А теперь похожая идея, но с «внутренними» переводами строки:
fun main() {
val xml = """
<name>
Ann
</name>
""".trimIndent()
println(xml)
println("Длина xml = ${'$'}{xml.length}") // Длина xml = ...
}
Мы пока не парсим XML, но уже понимаем: форматирование влияет на байты/символы. Это не «философия», это причина половины странных багов вида «почему имя с пробелом в начале?».
5. Правила well‑formed XML
Термин well‑formed (дословно «хорошо сформированный») означает: XML соответствует базовой грамматике и может быть распарсен как XML‑документ. Это не про «данные правильные», а про «это вообще XML, а не набор символов, который только притворяется XML».
Правила well‑formed можно воспринимать как правила для скобок и кавычек: если нарушили — всё, парсер останавливается. Для практики удобно держать это в виде небольшой таблички.
| Правило | Что это значит по‑человечески | Пример ошибки |
|---|---|---|
| Один корневой элемент | Нельзя иметь два «верхних» элемента рядом | |
| Каждый открытый тег должен закрываться | Если открыли <x>, закройте </x> | |
| Вложенность должна быть правильной | Закрываем в обратном порядке (как со скобками) | |
| Имена тегов чувствительны к регистру | <Name> и </name> — разные имена | |
| Атрибуты должны быть в кавычках | Нельзя id=42, нужно id="42" | |
| Спецсимволы в тексте нужно экранировать | & и < нельзя писать «как есть» | |
Давайте посмотрим на пару типичных «сломанных» примеров, чтобы мозг начал узнавать их с первого взгляда.
Неправильная вложенность:
fun main() {
val broken = "<a><b></a></b>"
println(broken) // <a><b></a></b>
// Парсер скажет: "я не понимаю, что ты закрываешь"
}
Незакрытый тег:
fun main() {
val broken = "<user><name>Ann</user>"
println(broken) // <user><name>Ann</user>
// <name> не закрыт, документ некорректен
}
Атрибут без кавычек:
fun main() {
val broken = "<user id=42></user>"
println(broken) // <user id=42></user>
// Для XML это ошибка: id должен быть в кавычках
}
6. Экранирование и сущности
Сейчас будет момент, который ломает новичков чаще всего. В XML есть несколько символов, которые имеют специальный смысл. Самые важные: & и <. Если вы поставите их в тексте «как есть», XML может стать нераспарсиваемым, потому что парсер будет думать, что вы начинаете сущность (&...;) или новый тег (<tag>).
Поэтому в XML используются сущности (entities) — специальные последовательности, которые означают «вставь символ, но безопасно». Базовые сущности такие:
| Символ | Как писать в XML | Где встречается |
|---|---|---|
|
|
в тексте, в атрибутах |
|
|
в тексте, в атрибутах |
|
|
обычно можно в тексте, но часто тоже экранируют для симметрии |
|
|
особенно важно внутри attr="..." |
|
|
когда атрибуты пишут в одинарных кавычках |
Пример «плохого» XML:
fun main() {
val unsafe = "<title>Tom & Jerry</title>"
println(unsafe) // <title>Tom & Jerry</title>
// В XML это НЕ ок: & должен быть экранирован
}
Правильная версия:
fun main() {
val safe = "<title>Tom & Jerry</title>"
println(safe) // <title>Tom & Jerry</title>
}
Мини‑утилита: экранирование текста для XML
Писать XML руками — занятие, в котором легко совершить «символическое преступление» (случайно вставить &). Поэтому даже в учебном приложении полезно завести маленькую функцию, которая экранирует текст. Она не обязана быть идеальной для всех случаев мира, но должна быть понятной.
Тут важно соблюдать порядок: сначала заменяем &, а уже потом < и прочее. Иначе вы рискуете «переэкранировать» уже вставленные &.
fun escapeXmlText(text: String): String {
return text
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
}
Эта идея хорошо сочетается с нашим подходом «нормализуй и валидируй входные данные». И да, string templates в Kotlin делают сборку строк гораздо приятнее, поэтому дальше мы будем собирать XML через шаблоны, а текст — прогонять через escapeXmlText.
7. Практический пример: экспорт отчёта в XML‑строку
Мы продолжаем развивать наше консольное приложение‑анализатор данных (в прошлой лекции оно принимало JSON и строило отчёты). Сегодня мы не парсим XML, но добавим возможность сформировать XML‑выгрузку, потому что это отличный способ закрепить: элементы, атрибуты, текст, экранирование и well‑formed.
Представим, что внутри приложения у нас есть результат отчёта:
- имя отчёта
- количество записей
- короткий комментарий (который может содержать спецсимволы)
Сделаем функцию, которая создаёт XML:
fun buildReportXml(title: String, count: Int, note: String): String {
val safeTitle = escapeXmlText(title)
val safeNote = escapeXmlText(note)
return """
<report title="$safeTitle" count="$count">
<note>$safeNote</note>
</report>
""".trimIndent()
}
Здесь есть сразу несколько «правильных» привычек. Мы используем многострочную строку и trimIndent(), чтобы XML был читаемым в коде и в выводе. Мы явно экранируем текст, чтобы не сломать документ символом &. И мы помним, что count в атрибуте всё равно станет строкой.
Теперь пример использования в main:
fun main() {
val xml = buildReportXml(
title = "Sales & Marketing",
count = 3,
note = "Top < 10 entries, ok"
)
println(xml)
// <report title="Sales & Marketing" count="3">
// <note>Top < 10 entries, ok</note>
// </report>
}
Мы получили well‑formed XML, который не развалится из‑за & и <. И даже если вы пока не умеете парсить XML, вы уже умеете формировать корректный документ и можете глазами проверить, что там происходит.
8. Типичные ошибки при работе с XML как форматом
Ошибка №1: путать «красиво выглядит» и «well‑formed».
Новичок смотрит на XML, видит отступы, переносы строк, аккуратные теги — и думает, что раз красиво, значит корректно. Но XML не про красоту, а про строгую грамматику: один неверно закрытый тег или два корня подряд — и парсер откажется работать. «Почти правильно» для XML означает «неправильно».
Ошибка №2: вставлять текст в XML без экранирования.
Это классика жанра: вы подставили в <title>...</title> строку с & или <, и документ внезапно перестал быть XML. Самое обидное, что визуально он выглядит «обычно». Поэтому даже в учебных примерах полезно дисциплинировать себя: любой пользовательский текст прогоняем через escapeXmlText, иначе будете ловить ошибки в самых неожиданных местах.
Ошибка №3: ожидать, что пробелы и переносы строк «не считаются».
В XML форматирование — это тоже текст. Если вы пишете <name>\n Ann\n</name>, внутри будет текст с пробелами и переводами строк. Потом вы удивляетесь, почему сравнение строк не сходится или почему в выводе появляются лишние пробелы. В реальной обработке XML почти всегда есть этап нормализации текста: хотя бы trim().
Ошибка №4: думать, что атрибуты — это «почти числа/булевы значения».
Атрибуты в XML всегда строки. Даже если вы написали count="3", это строка "3". Поэтому любая «типизация» (в Int, Boolean, enum) происходит уже на вашей стороне. В этом смысле XML похож на ввод через readln(): сначала строка, потом попытка распарсить, потом обработка ошибки.
Ошибка №5: ломать документ неправильной вложенностью.
<a><b></a></b> — пример, который часто появляется при ручной сборке строк или при неаккуратном копипасте. Думайте о тегах как о скобках: закрываем строго в обратном порядке. Если поймали себя на том, что «сложно уследить глазами», значит пора либо упростить структуру, либо перестать собирать XML руками и перейти к нормальному парсеру/генератору (но это уже следующий шаг курса).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ