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;")
}

Ця ідея добре поєднується з нашим підходом «нормалізуйте й перевіряйте вхідні дані». І так, шаблони рядків у 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 руками й перейти до нормального парсера/генератора (але це вже наступний крок курсу).

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ