JavaRush /Курсы /Kotlin SELF /XML: элементы, атрибуты, текст и правила well‑formed

XML: элементы, атрибуты, текст и правила well‑formed

Kotlin SELF
50 уровень , 0 лекция
Открыта

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 можно воспринимать как правила для скобок и кавычек: если нарушили — всё, парсер останавливается. Для практики удобно держать это в виде небольшой таблички.

Правило Что это значит по‑человечески Пример ошибки
Один корневой элемент Нельзя иметь два «верхних» элемента рядом
<a/> <b/>
Каждый открытый тег должен закрываться Если открыли <x>, закройте </x>
<x><y></x>
Вложенность должна быть правильной Закрываем в обратном порядке (как со скобками)
<a><b></a></b>
Имена тегов чувствительны к регистру <Name> и </name> — разные имена
<Name></name>
Атрибуты должны быть в кавычках Нельзя id=42, нужно id="42"
<user id=42 />
Спецсимволы в тексте нужно экранировать & и < нельзя писать «как есть»
<t>Tom & Jerry</t>

Давайте посмотрим на пару типичных «сломанных» примеров, чтобы мозг начал узнавать их с первого взгляда.

Неправильная вложенность:

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 Где встречается
&
&amp;
в тексте, в атрибутах
<
&lt;
в тексте, в атрибутах
>
&gt;
обычно можно в тексте, но часто тоже экранируют для симметрии
"
&quot;
особенно важно внутри attr="..."
'
&apos;
когда атрибуты пишут в одинарных кавычках

Пример «плохого» XML:

fun main() {
    val unsafe = "<title>Tom & Jerry</title>"
    println(unsafe) // <title>Tom & Jerry</title>
    // В XML это НЕ ок: & должен быть экранирован
}

Правильная версия:

fun main() {
    val safe = "<title>Tom &amp; Jerry</title>"
    println(safe) // <title>Tom &amp; Jerry</title>
}

Мини‑утилита: экранирование текста для XML

Писать XML руками — занятие, в котором легко совершить «символическое преступление» (случайно вставить &). Поэтому даже в учебном приложении полезно завести маленькую функцию, которая экранирует текст. Она не обязана быть идеальной для всех случаев мира, но должна быть понятной.

Тут важно соблюдать порядок: сначала заменяем &, а уже потом < и прочее. Иначе вы рискуете «переэкранировать» уже вставленные &amp;.

fun escapeXmlText(text: String): String {
    return text
        .replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
        .replace("\"", "&quot;")
}

Эта идея хорошо сочетается с нашим подходом «нормализуй и валидируй входные данные». И да, 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 &amp; Marketing" count="3">
    //     <note>Top &lt; 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 руками и перейти к нормальному парсеру/генератору (но это уже следующий шаг курса).

1
Задача
Kotlin SELF, 50 уровень, 0 лекция
Недоступна
Конверт письма
Конверт письма
1
Задача
Kotlin SELF, 50 уровень, 0 лекция
Недоступна
Паспорт файла
Паспорт файла
1
Задача
Kotlin SELF, 50 уровень, 0 лекция
Недоступна
Безопасный текст
Безопасный текст
1
Задача
Kotlin SELF, 50 уровень, 0 лекция
Недоступна
Карточка профиля
Карточка профиля
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ