У попередній темі ми навчилися
відкривати файл через with, писати в нього рядки й обовʼязково вказувати
encoding="utf-8". Тепер файл на диску є — і одразу постає питання, від якого
не втекти: що саме в ньому написати, щоб через півроку його прочитала
не лише ця програма?
Памʼять програми зникає разом із програмою. Список товарів, який ти зібрав у словник, живе рівно доти, доки процес не завершився. Файл — це спосіб пережити завершення. Але файл із власною вигадкою всередині («тут ціна, потім два пробіли, потім назва») зрозуміє тільки його автор — і то поки памʼятає. Формат обміну — це домовленість, записана заздалегідь і однаково зрозуміла Python, Excel, JavaScript і колезі, який відкриє файл через рік.
01 / ІдеяНавіщо потрібні формати
Форматів вигадано сотні, але в повсякденній роботі ти впираєшся в два. Обидва — звичайний текст, який можна відкрити блокнотом і прочитати очима. Обидва вміє читати будь-яка мова програмування. Різниця між ними — у формі даних:
- CSV (comma-separated values, «значення, розділені комами») — це таблиця. Рядки й стовпці, як в Excel. Кожен запис має рівно ті самі поля, що й сусідній;
- JSON (JavaScript Object Notation) — це дерево. Усередині запису може лежати список, усередині того списку — ще словники, і так на будь-яку глибину.
Наскрізний приклад теми — маленька кавʼярня. Її меню чудово лягає в таблицю: у кожного напою є назва, ціна, категорія й опис. А замовлення в таблицю вже не лягає: у ньому є список позицій, і в різних замовленнях цих позицій різна кількість. Меню поїде в CSV, замовлення — у JSON. На цій парі ми й розберемо обидва модулі.
02 / CSVТаблиця як текст
Ідея CSV настільки проста, що її можна пояснити одним прикладом. Один рядок файлу — один рядок таблиці. Поля всередині рядка розділені комою. Перший рядок зазвичай не дані, а шапка: назви стовпців.
назва,ціна,категорія,опис
Еспресо,25,кава,міцна
Капучино,45,кава,"молоко, кориця"
Чай,30,чай,трав'яний
Це весь формат. Ані типів, ані вкладеності, ані коментарів — самі значення й коми. Саме через цю простоту CSV вміють геть усі: бухгалтерські програми, бази даних, банки у виписці по картці, наукові прилади. Якщо тобі колись дадуть «дані», з імовірністю відсотків шістдесят це буде CSV.
Але придивись до третього рядка. Опис капучино — молоко, кориця — сам містить
кому. Щоб її не сплутали з роздільником, поле взяли в подвійні лапки.
Ця дрібниця й породжує все, про що йдеться в наступному розділі.
csv має
поняття діалекту — набору налаштувань під конкретного відправника.03 / ПасткаЧому не можна split(",")
Перше, що спадає на думку новачкові (і, чесно кажучи, не лише новачкові): рядок є,
кома є, у нас же є split() з теми про
рядки. Написав рядок.split(",") — отримав список полів, готово.
print(рядок.split(","))
['Капучино', '45', 'кава', '"молоко', ' кориця"']
Полів мало бути чотири, а вийшло пʼять. Кома всередині опису порізала поле навпіл:
тепер у нас є огризок "молоко з приліпленою лапкою й огризок
кориця" з пробілом попереду. Ціна тепер у стовпці 1, а категорія — у стовпці 2,
і все ще нібито збігається. Але наступний рядок, де ком немає, дасть чотири поля, і твоя
програма почне брати з різних рядків різні речі. Це найгірший тип помилки: не падіння,
а тихо зіпсовані дані.
Модуль csv існує саме для цього. Він читає рядок не як «текст із комами»,
а як автомат зі станами: побачив лапку на початку поля — перейшов у режим
«всередині лапок», де кома вже не роздільник, а звичайний символ; побачив закривальну
лапку — вийшов назад. Порівняй обидва підходи на одних і тих самих рядках:
Інтерактив 1 · split(",") проти модуля csv
Один і той самий рядок, два способи розібрати. Дивись на кількість полів.
"" — це спосіб записати одну лапку всередині поля, і модуль повертає її
назад однією.Висновок простий і категоричний: рядок CSV не можна розібрати рядковими методами. Не тому, що це складно, а тому, що правила формату складніші, ніж здається, — і твоя саморобна версія працюватиме рівно доти, доки в даних не трапиться кома, лапка або перенос рядка всередині поля. У практиці ти напишеш обидві версії й порівняєш їх на однакових даних.
04 / ДіалектРоздільник, лапки, newline
Тепер про те, чому файл від колеги не читається твоїм кодом. Три налаштування, які вирішують майже все.
Роздільник
Кома — лише значення за замовчуванням. В українській (як і в німецькій чи французькій)
локалі Excel десятковим роздільником є кома: 45,50 — це сорок пʼять із половиною.
Якби такий Excel розділяв стовпці теж комою, файл був би нечитабельним, тому він бере
крапку з комою. Тому у файлі з української бухгалтерії ти майже завжди
побачиш ;. Модулю про це треба сказати:
csv.reader(файл, delimiter="\t") # TSV: розділено табуляцією
Що станеться, якщо не сказати? Не помилка. Модуль слухняно розріже рядок по комах, яких у файлі майже немає, — і поверне один-два величезні поля замість чотирьох. Помацай це руками:
Інтерактив 2 · Роздільник і лапки
Той самий запис, різні діалекти. Знизу — що буде, якщо про діалект не сказати.
молоко, кориця
доїжджає цілим навіть без лапок, бо для цього діалекту кома — звичайний символ.
Лапки потрібні не «завжди», а рівно тоді, коли всередині поля трапляється
власний роздільник файлу, лапка або перенос рядка.Лапки
Правило екранування в CSV одне й дуже коротке: поле беруть у подвійні лапки, якщо
всередині є роздільник, лапка або перенос рядка; сама лапка всередині подвоюється.
Слово сироп "Карамель" у файлі виглядає як "сироп ""Карамель""" —
виглядає дико, але читається однозначно. Модуль csv робить це при записі
й розбирає при читанні; тобі не треба знати цю механіку напамʼять — треба не робити її руками.
newline="" — той самий рядок, який усі забувають
Відкриваючи файл для csv, пиши так:
# ... тут працює csv.writer
Причина технічна, але наслідки видно одразу. Модуль csv сам вирішує, чим
завершувати рядок, — за замовчуванням це послідовність \r\n. Якщо файл
відкрито звичайно, текстовий режим на Windows додатково перетворює кожен
\n на \r\n — і в файлі зʼявляється \r\r\n, тобто зайвий
порожній рядок між кожними двома записами. Порожній newline="" означає
«не чіпай переноси, модуль розбереться сам».
При читанні ця сама вказівка рятує від іншої халепи: якщо всередині поля в лапках є
перенос рядка (а таке буває — багаторядковий коментар у клітинці), без newline=""
файловий обʼєкт поріже його передчасно, і модуль отримає обірваний запис. Правило,
яке варто запамʼятати як догму: файл для csv завжди відкривають із
newline="" — і на запис, і на читання.
utf-8-sig при записі: воно ставить на початок файлу невидиму
позначку BOM, за якою Excel упізнає UTF-8. Для Python обидва варіанти читаються однаково —
utf-8-sig при читанні цю позначку зʼїдає.05 / Читанняreader і DictReader
Читачів у модулі два, і вибір між ними — це вибір, як ти хочеш звертатися до полів: за номером чи за назвою.
with open("меню.csv", newline="", encoding="utf-8") as файл:
for рядок in csv.reader(файл):
print(рядок)
['назва', 'ціна', 'категорія', 'опис']
['Еспресо', '25', 'кава', 'міцна']
['Капучино', '45', 'кава', 'молоко, кориця']
Зверни увагу на перший надрукований рядок: reader віддав шапку як звичайні
дані. Він про неї нічого не знає — для нього це просто перший рядок файлу. Хочеш пропустити
шапку — зроби це явно: шапка = next(читач) перед циклом.
DictReader поводиться інакше: він зʼїдає перший рядок,
запамʼятовує його як назви полів і далі віддає словники.
for рядок in csv.DictReader(файл):
print(рядок["назва"], "—", рядок["ціна"])
Еспресо — 25
Капучино — 45
Різниця виглядає косметичною, а насправді вона про надійність. рядок[1]
мовчки зламається, щойно відправник додасть стовпець на початок. рядок["ціна"]
переживе будь-яку перестановку стовпців і читається без коментаря. Ціна — трохи повільніше
й трохи більше памʼяті; на файлах, які поміщаються в памʼять, це не має значення.
Інтерактив 3 · reader проти DictReader
Один файл, два читачі. Веди повзунок по ітераціях циклу.
reader віддає шапку як дані —
і якщо не пропустити її вручну, у твоїх розрахунках зʼявиться «напій» на назву
«назва» з ціною «ціна». DictReader шапку вже зʼїв, тому ітерацій
у нього на одну менше — і на пʼятій цикл просто закінчується.06 / Записwriter і DictWriter
Симетрично читанню: csv.writer приймає списки, csv.DictWriter —
словники. Різниця лише в тому, що DictWriter треба заздалегідь сказати,
які поля й у якому порядку писати.
меню = [
{"назва": "Еспресо", "ціна": 25, "опис": "міцна"},
{"назва": "Капучино", "ціна": 45, "опис": "молоко, кориця"},
]
with open("меню.csv", "w", newline="", encoding="utf-8") as файл:
писар = csv.DictWriter(файл, fieldnames=["назва", "ціна", "опис"])
писар.writeheader() # рядок із назвами стовпців
писар.writerows(меню) # усі записи одним викликом
Лапки навколо молоко, кориця зʼявляться у файлі самі — писар бачить кому
й екранує поле. Це і є головна причина користуватися модулем на запис, а не склеювати
рядок через ",".join(...): склеювання поставить кому як роздільник і зіпсує файл,
який ти сам же потім прочитаєш.
| що робить | читання | запис |
|---|---|---|
| списками | csv.reader(файл) | csv.writer(файл) |
| словниками | csv.DictReader(файл) | csv.DictWriter(файл, fieldnames=…) |
| один запис | next(читач) | писар.writerow(запис) |
| усі записи | list(читач) | писар.writerows(записи) |
| шапка | DictReader бере сам | писар.writeheader() |
07 / ТипиCSV не знає, що таке число
Ось риса, на якій спотикаються всі. У файлі немає типів — у ньому є символи. Тому
усе, що прочитано з CSV, — рядки. Завжди. Ціна 45 повернеться
як '45', а не 45.
print(рядок["ціна"] + 5)
TypeError: can only concatenate str (not "int") to str
print(int(рядок["ціна"]) + 5)
50
Перетворення — твоя робота, і робити її треба свідомо, в одному місці, одразу після читання. Три речі, про які варто памʼятати:
- Порожня клітинка — це
'', а неNone.int('')падає зValueError, тому порожнечу перевіряй окремо; - Дробові з комою не перетворюються.
float("45,5")—ValueError: Python чекає крапку. У файлах з української локалі це штатна ситуація, лікуєтьсязаміна = текст.replace(",", "."); - Числа записуються без проблем. При записі
csvсам покличеstr(), тому класти вwriterowможна й числа. Асиметрія: пишеш числом — читаєш рядком.
quoting=csv.QUOTE_NONNUMERIC,
який при читанні перетворює всі поля без лапок на float. Виглядає як
розвʼязання проблеми типів, але насправді це пастка: назва без лапок теж піде на
float() і впаде, а цілі числа стануть дробовими. Явне
int(рядок["ціна"]) у своєму коді надійніше.08 / JSONКоли таблиці замало
Повернімось до замовлення кавʼярні. У ньому є номер, імʼя клієнта, ознака оплати — і список позицій, у кожної з яких своя назва й кількість. У таблицю це не лягає: клітинка не вміщає список. JSON вміщає.
"номер": 17,
"клієнт": "Аня",
"оплачено": true,
"позиції": [
{"назва": "Капучино", "кількість": 2},
{"назва": "Чай", "кількість": 1}
]
}
Упізнаєш? Це майже словник Python, у якому всередині лежить список словників. Схожість не випадкова: JSON народився як спосіб записати обʼєкт JavaScript текстом, а структури там ті самі. Тому переклад із Python у JSON і назад майже дослівний — майже, і саме про це «майже» піде розділ 09.
Чотири функції, і всі про одне
Модуль json має рівно чотири функції, які легко переплутати. Правило:
буква s означає «string», тобто робота з рядком у памʼяті; без s —
робота з файлом.
| функція | напрямок | з чим працює |
|---|---|---|
| json.dumps(об) | обʼєкт → текст | повертає рядок |
| json.loads(текст) | текст → обʼєкт | приймає рядок |
| json.dump(об, файл) | обʼєкт → файл | пише у відкритий файл |
| json.load(файл) | файл → обʼєкт | читає відкритий файл |
with open("замовлення.json", "w", encoding="utf-8") as файл:
json.dump(замовлення, файл, ensure_ascii=False, indent=2)
with open("замовлення.json", encoding="utf-8") as файл:
повернулось = json.load(файл)
print(повернулось["позиції"][0]["назва"])
Капучино
Ніякого newline="" тут не треба — це вимога модуля csv, а не
загальне правило. А от encoding="utf-8" потрібне так само, і про сусіда
ensure_ascii=False — окремий розділ нижче, бо без нього твої дані стануть
нечитабельними.
Два корисні аргументи dump/dumps: indent=2 робить
відступи й переноси, щоб файл читала людина; sort_keys=True впорядковує ключі
за абеткою. Без indent усе пишеться одним рядком — компактніше на 10-20%
і швидше читається машиною. Правило: файл, у який заглядатиме людина (налаштування,
приклад, дані в репозиторії) — з indent; файл, який летить по мережі
чи лежить мільйоном записів на диску — без.
'Аня' — помилка). Ніяких коментарів. Ніякої коми після останнього елемента.
true, false, null — маленькими літерами, на відміну
від пітонівських True, False, None. Файл, який
Python друкує сам, завжди правильний; проблеми починаються, коли JSON редагують руками.09 / ВідповідністьЩо з чим збігається
Перетворення Python → JSON не взаємно однозначне. Кілька різних типів Python відображаються в один тип JSON — і назад повертається вже щось одне.
| Python | JSON | що повернеться з loads |
|---|---|---|
| dict | object | dict — те саме |
| list | array | list — те саме |
| tuple | array | list — тип змінився |
| str | string | str |
| int / float | number | int / float |
| True / False | true / false | bool |
| None | null | None |
| set | — | TypeError при записі |
| дата, час | — | TypeError при записі |
Три рядки з кінця варті окремої уваги. Кортеж перетворюється на список і назад уже не повертається — у JSON просто немає такого поняття, як незмінна послідовність. Якщо код десь розраховував, що там кортеж (наприклад, використовував його ключем словника), після циклу «записали — прочитали» він зламається.
Множина й дата не серіалізуються взагалі. Python не вгадуватиме, у що
їх перетворити, — він чесно кидає TypeError: Object of type set is not JSON
serializable. Перетворюй сам: множину — у sorted(множина),
дату — у рядок. Це не обмеження Python, а обмеження формату: у JSON типів усього шість.
І третє, найпідступніше: ключі словника завжди стають рядками.
У JSON ключ обʼєкта може бути тільки рядком, тому {1: "перший"} запишеться
як {"1": "перший"} — а прочитається як словник із ключем-рядком
"1". Числовий ключ мовчки пережив перетворення й став іншим.
Інтерактив 4 · Обʼєкт → JSON → обʼєкт
Обери, що записуємо, — і подивись, що повернулось із зворотного шляху.
json.loads. Бірюзова рамка внизу означає,
що обʼєкт повернувся тим самим; рожева — що змінився або взагалі не записався.
Режим «ключі-числа» показує найтихішу з усіх втрат.10 / Кирилицяensure_ascii і українські літери
Найпоширеніша практична пастка теми. Запиши словник з українським текстом без додаткових аргументів — і подивись на файл:
{"\u043d\u0430\u043f\u0456\u0439": "\u041a\u0430\u0432\u0430"}
print(json.dumps({"напій": "Кава"}, ensure_ascii=False))
{"напій": "Кава"}
Обидва рядки — коректний JSON, і json.loads поверне з них однаковий словник.
Але перший неможливо прочитати очима, неможливо знайти пошуком по файлу й соромно показати
замовникові. Кожна українська літера перетворилась на шість символів
\uXXXX — це «екранована» форма запису символу за його номером у Юнікоді.
Чому це поведінка за замовчуванням? Історія: аргумент означає «гарантувати, що на виході буде чистий ASCII». Такий файл переживе будь-який древній канал звʼязку, будь-яку систему, яка не знає про UTF-8, і будь-який редактор із неправильним кодуванням. У 1999 році це було розумно. Сьогодні UTF-8 усюди, тому для української мови розумно інше:
Це має стати автоматизмом — так само, як encoding="utf-8" у
open(). Подивись, у що обходиться забудькуватість:
Інтерактив 5 · ensure_ascii: та сама інформація, різний файл
Один словник, один прапорець. Порівняй читабельність і розмір.
11 / Криві даніКоли файл зіпсований
Файли, які ти отримуєш ззовні, бувають биті: обірвалась передача, хтось редагував JSON руками й забув лапку, вивантаження зробили з помилкою. Код, який це не переживає, не можна пускати в роботу. Знання з теми про винятки тут стають у пригоді буквально.
JSON падає з json.JSONDecodeError — це підвид ValueError,
тому ловити можна будь-який із двох. Виняток корисний тим, що точно каже, де
зламалось:
try:
дані = json.loads(текст)
except json.JSONDecodeError as помилка:
print("рядок", помилка.lineno, "стовпець", помилка.colno)
print("причина:", помилка.msg)
дані = {} # працюємо далі з порожнечею, а не падаємо
CSV поводиться інакше — і тим підступніший. Файл, у якому в рядку бракує стовпця,
для модуля не помилка: DictReader просто поставить у відсутнє поле
None. Зайві стовпці він теж не викине — складе їх у список під ключем
None. Помилки немає, дані криві, програма працює далі. Перевіряти доводиться
самому.
Інтерактив 6 · Що робить Python із кривими даними
Чотири типові поломки: дві в JSON, дві в CSV. Дивись, як саме воно ламається.
None замість
значення, і помітиш ти це вже в підсумковому звіті. Тому після читання CSV
перевіряй кількість полів самостійно — у практиці ти напишеш саме таку перевірку.12 / ВибірТаблиця чи вкладеність
Запитання, яке варто ставити собі перед тим, як обрати формат: чи всі мої записи однакові за формою? Якщо так — CSV. Якщо всередині запису є список змінної довжини або ще один запис — JSON.
Спокуса «запхати все в CSV» велика, бо таблиця звична. Але вкладеність у клітинку
не влазить, і починаються милиці: теги через вертикальну риску кава|знижка,
JSON-рядок усередині CSV-поля, дублювання шапки замовлення в кожному рядку позиції.
Усі три працюють — і всі три означають, що розбирати дані назад доведеться руками.
Інтерактив 7 · Три задачі, два формати
Той самий набір даних у CSV і в JSON. Дивись і на форму, і на розмір.
13 / ПрофіМежі JSON
Три речі, які знадобляться, коли дані виростуть.
JSON читається цілком
json.load() не вміє читати наполовину: формат такий, що перевірити коректність
можна лише дочитавши до останньої дужки. Тому файл на 2 ГБ спершу цілком приїде в памʼять
текстом, а потім перетвориться на дерево обʼєктів Python — яке важить ще більше, ніж текст,
бо кожен рядок і словник несуть службові дані. На великих обсягах це закінчується
MemoryError.
Розвʼязання називається JSON Lines (розширення .jsonl):
один самостійний JSON-обʼєкт на рядок, без обгортки-масиву.
{"номер": 18, "клієнт": "Богдан"}
# читається по одному запису, памʼять не росте:
with open("замовлення.jsonl", encoding="utf-8") as файл:
for рядок in файл:
запис = json.loads(рядок)
Це той самий трюк, який робить CSV масштабованим: файл читається порядково, у памʼяті одночасно живе один запис. Плюс дописувати в кінець можна без переписування всього файлу.
Числа з рухомою крапкою
Python записує float у JSON через repr(), а той із версії 3.1
дає найкоротший запис, який гарантовано читається назад у те саме число. Тому
0.1 + 0.2 поїде у файл як 0.30000000000000004 і повернеться
бітово тим самим — усередині Python втрати немає.
Втрата зʼявляється на кордоні з іншими системами. Класика: JavaScript має один числовий
тип і не тримає цілі, більші за 253. Ідентифікатор
9007199254740993, який Python запише чесно, у браузері прочитається як
9007199254740992 — на одиницю менше, і це вже інший запис у базі. Тому великі
ідентифікатори кладуть у JSON рядками. Ще одна межа: Decimal, яким рахують
гроші, серіалізується лише через float — тобто з втратою точності саме там,
де вона неприпустима.
NaN, якого в JSON немає
У стандарті JSON немає ані NaN, ані нескінченності. Python усе одно їх
записує — словами NaN і Infinity, — і Python же їх прочитає.
А сторонній парсер (у тому числі JSON.parse у браузері) на такому файлі
впаде. Якщо файл їде назовні, страхуйся: json.dumps(дані, allow_nan=False)
кине ValueError у тебе, а не в замовника.
14 / ПідсумокЩо з цього забрати
- Формат — це домовленість, а не файл. CSV і JSON існують не тому, що зручні, а тому, що їх однаково розуміють усі; власний формат означає, що дані прочитаєш тільки ти;
- CSV розбирають модулем, а не
split(","). Кома всередині лапок ламає наївний поділ тихо — не помилкою, а зсувом стовпців; - Діалект треба вказувати явно.
delimiter=";"для українського Excel,newline=""завжди,encodingзавжди; - З CSV усе читається рядками. Типів у форматі немає — перетворення роби сам і одразу після читання;
- JSON — для вкладеності, і в нього своя система типів. Кортеж стає списком, множина й дата не записуються взагалі, ключі завжди стають рядками;
ensure_ascii=False— рефлекс. Без нього український текст перетворюється на\uXXXX, і файл стає нечитабельним для людини.
І ширша думка, заради якої була ця тема. Обидва формати — компроміс між «зрозуміло людині» й «зрозуміло машині», і кожен зробив свій вибір. CSV віддав типи й вкладеність в обмін на компактність і порядкове читання. JSON віддав компактність в обмін на структуру. Немає кращого — є придатний до твоїх даних. Уміння відповісти на питання «а якої форми мої записи?» коштує більше, ніж памʼять на аргументи функцій.
"2026-03-01". Перетворювати її туди й назад —
робота наступної теми, де ми розберемо
datetime, часові пояси й формат ISO 8601, який недарма став стандартом
де-факто саме для обміну. А в practice.ipynb ти зараз запишеш і прочитаєш
меню кавʼярні у двох форматах, перетвориш типи, зловиш дві поломки й переконаєшся
на assert, що кортеж після подорожі в JSON справді став списком.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.