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("\"", """)
}
Ця ідея добре поєднується з нашим підходом «нормалізуйте й перевіряйте вхідні дані». І так, шаблони рядків у 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 руками й перейти до нормального парсера/генератора (але це вже наступний крок курсу).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ