JSON Toolkit

Практика роботи з JSONL-файлами: відкриття, конвертація та пошук пошкоджених рядків

Головне: Якщо весь .jsonl-файл передати JSON.parse або json.load і отримати помилку — майже завжди винен не файл, а спосіб читання. У статті: стандартні патерни перевірки та построкового читання, команди взаємного перетворення з JSON-масивом, таблиця шести типів битих рядків і коректне споживання NDJSON з потокової HTTP-відповіді.

Середній~9 хв читанняОпубліковано 2026-11-06Оновлено 2026-11-06
Практичний посібник із построкового читання JSONL-файлів та ізоляції пошкоджених рядків

Головне: кожен рядок JSONL-файлу — незалежне коректне JSON-значення, але весь файл єдиною JSON-структурою не є. Якщо передача файлу цілком у JSON.parse або json.load падає, у дев'яти випадках із десяти файл у порядку — ви просто читаєте його неправильно.

Один запис у рядку, без ком між рядками, без зовнішнього масиву — JSONL відмовляється від «цілісної структури документа» в обмін на дозаписування, потокове читання та ізоляцію збою одним рядком. Усі три переваги реалізуються лише при построковій обробці й усі три перетворюються на помилки при спробі розпарсити файл цілком.

1. Три назви, один формат: JSONL, NDJSON, JSON Lines

Новачки часто приймають різницю назв за різницю в синтаксисі. Її немає:

Назва Типовий контекст Розширення / media type
JSON Lines Інженерія даних, ML-датасети (pandas, Spark, Hugging Face) .jsonl
NDJSON Потокові HTTP API, дані в реальному часі .ndjson, application/x-ndjson
JSONL Побутове скорочення для будь-якого з двох —

Інструмент, що читає один варіант, прочитає й інший. Єдиний критерій вибору назви — конвенції вашої екосистеми: пишіть .jsonl, коли готуєте датасети для Hugging Face, ставте application/x-ndjson, коли публікуєте потоковий API. Портативний JSONL-файл задовольняє трьом правилам: кодування UTF-8 (без BOM), одне повне JSON-значення в кожному рядку, рядки розділені \n. Завершальний перенос рядка в кінці файлу наполегливо рекомендується, а порожні рядки не є коректними записами. Деталі протоколу та мотивацію дизайну розібрано в нашому огляді JSON Lines і NDJSON; ця стаття строго про те, що робити, коли файл уже у вас.

2. Огляд: спершу head, потім grep, редактор — наприкінці

Подвійний клік по 500-мегабайтному .jsonl, щоб відкрити його в редакторі, — найчастіша помилка: три мільйони рядків плюс підсвічування синтаксису плюс буфер скасування повісять практично будь-який редактор, і JSON тут ні до чого. Парсинг документа на 45 МБ займає в Node 22 близько 208 мс; підвисає саме рендеринг. Построкові формати створено для рядкових утиліт, бо записи збігаються з фізичними рядками:

# Спершу оцініть масштаб: кількість записів і розмір файлу, не читаючи вміст
wc -l events.jsonl && du -h events.jsonl

# Подивіться, як виглядає запис (поля та структура)
head -n 1 events.jsonl | jq .

# Знайдіть потрібні записи напряму: рядкові утиліти просто працюють — привілей JSONL
grep '"level":"error"' events.jsonl | head -n 5
tail -n 100 events.jsonl | jq 'select(.latency_ms > 500)'

Зверніть увагу на останню команду: кожен рядок на виході tail — як і раніше незалежне JSON-значення, тож його можна подати просто в jq, не збираючи масив заново. На багатогігабайтних файлах цей підхід працює так само майже без витрат пам'яті — той самий принцип, що й у матеріалі про те, чому великі JSON уповільнюють сторінки.

3. Три стандартні патерни построкового читання

У построкового читання три обов'язкові вимоги: пропускати порожні рядки (особливо завершальний перенос), повідомляти помилки з номером рядка й не давати одному битому рядку втратити всю роботу. Стандартні патерни в усіх трьох мовах побудовані навколо цих пунктів.

Python, лише стандартна бібліотека:

import json

with open("events.jsonl", encoding="utf-8") as f:
    for line_number, line in enumerate(f, start=1):
        if not line.strip():          # пропускати порожні рядки
            continue
        try:
            record = json.loads(line) # парсити кожен рядок незалежно
        except json.JSONDecodeError as e:
            print(f"Рядок {line_number}: {e}")  # повідомити, не перериваючи
            continue                  # ізолювати биту строку
        handle(record)

Node зі встроєним модулем readline — постійна пам'ять незалежно від розміру файлу:

import fs from "node:fs";
import readline from "node:readline";

const input = fs.createReadStream("events.jsonl", "utf8");
const lines = readline.createInterface({ input, crlfDelay: Infinity });
let lineNumber = 0;
for await (const line of lines) {
  lineNumber += 1;
  if (!line.trim()) continue;
  try {
    handle(JSON.parse(line));
  } catch (e) {
    console.error(`Рядок ${lineNumber}: ${e.message}`);
  }
}

У Go 1.27 encoding/json/v2 став стабільним за замовчуванням, і його потокове декодування за значеннями ідеально лягає на JSONL; для вбудовування jq-виразів є gojq. З бібліотеками не поспішайте ставити пакети: Python-бібліотека jsonlines повідомляє номер і вихідний текст збійного рядка, а orjsonl читає й пише стиснуті gzip/bzip2/zstd файли напряму; npm-пакет ndjson для Node не випускав релізів із 2020 року — встроєного readline достатньо.

4. Перетворення на JSON-масив і назад

Напрямок має значення; переплутаєте — будь-яка команда буде невірною. Запам'ятайте одне правило: масив → JSONL — це map (кожен запис стискається в один рядок), JSONL → масив — це collect (рядки збираються в список).

# JSON-масив → JSONL: -c стискає кожен елемент в один рядок
jq -c '.[]' records.json > records.jsonl

# JSONL → JSON-масив: -s збирає весь вхід в один масив
jq -s '.' events.jsonl > records.json

# Фільтрація JSONL построково: -s не потрібен, jq обробляє вхід як потік
jq -c 'select(.active == true)' events.jsonl > active.jsonl

Однорядкові команди Python еквівалентні: масив → JSONL пише json.dumps(r, ensure_ascii=False) + "\n" на запис, зворотний напрям збирає json.loads построково в список. У кожного напряму одна пастка. У прямому (масив → JSONL) будь-який реальний перенос рядка всередині запису мусить бути екранованим json.dumps — інакше один запис розірветься на кілька рядків. У зворотному (JSONL → масив) не склеюйте рядки в один гігантський рядок і не парсіть його — на мільйонах рядків це зайва повна копія в пам'яті. Агрегації (суми, групування, середні) вимагають -s, який вантажить усе; на багатогігабайтних файлах переходьте на awk або потоковий акумулятор на Python.

5. Шість видів битих рядків і як їх діагностувати

Майже кожна помилка JSONL живе на рівні рядка. Валідація построково з виводом номерів рядків — єдина надійна діагностика. За частотою:

Симптом Коренева причина Виправлення
Extra data у кінці рядка Між рядками додали коми Видаліть; кожен рядок має бути самостійним
Один запис розбито на рядки Неекранований реальний перенос усередині запису (відформатували вручну) Пересисніть json.dumps / jq -c
Перший рядок падає з Unexpected token ï Файл із BOM Перезбережіть у UTF-8 без BOM
Помилка лише в останньому рядку, хоча виглядає нормально Відсутній завершальний перенос або зайві пробіли Додайте \n; порожні рядки пропускайте за домовленістю
Помилка через перенос у лапках Літеральний перенос усередині рядка, а не \n Виправте генератор; ніколи не збирайте JSON-рядки конкатенацією вручну
Великі цілі змінюють значення Парсинг через double (усічення 2^53) Використовуйте парсер із збереженням точності, див. точність BigInt

Діагностичний алгоритм завжди з трьох кроків: head -n 1 для перевірки кодування та структури першого рядка; построковий скрипт, який повідомляє номери всіх битих рядків, а не зупиняється на першому; потім витягніть биті рядки по одному — один рядок це один повний запис, і вивчати його ізольовано нічого не коштує. Для пакетних імпортів рекомендовано політику «карантин, а не скасування»: записуйте биті рядки з номерами в errors.jsonl, продовжуйте завантажувати коректні, а рішення про повторний імпорт ухвалюйте за часткою битих рядків.

6. JSONL поверх HTTP: построкове споживання та зворотний тиск

Типова схема NDJSON поверх HTTP: сервер пише безперервно, клієнт читає безперервно. На клієнті не викликайте response.json() — він буферизує все тіло перед парсингом, що знецінює потоковість. Читайте потік тіла напряму, ріжте по \n і переносіть незавершений хвіст у наступну ітерацію:

const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;
  const lines = buffer.split("\n");
  buffer = lines.pop();               // останній кусок може бути неповним рядком
  for (const line of lines) {
    if (line.trim()) handle(JSON.parse(line));
  }
}

На сервері обов'язкові дві речі: по-перше, коли res.write() повернув false, дочекайтеся події drain (зворотний тиск), інакше пам'ять зростатиме без меж щоразу, коли споживач повільніший за виробника; по-друге, вимкніть gzip-прошарку на потокових маршрутах — gzip мусить буферизувати всю відповідь, перш ніж щось стиснути, і це повністю зводить нанівець потоковість NDJSON. Обидві поведінки підтверджено локальними тестами та кількома незалежними англомовними джерелами, а не здогадками.

7. Коли JSONL — поганий вибір

Негативний простір вибору формату також важливий. Три сценарії мають лишитися з JSON-масивом:

Вимога Чому JSONL не підходить Використовуйте замість
Валідація структури всього документа Немає загальної структури; дрейф полів лишиться непоміченим JSON-масив + Schema
Довільний доступ до запису N Довжини рядків змінні; бінарний пошук неможливий SQLite / Parquet
Глибока вкладеність, посилання між записами Рядки ізольовані; посиланням немає де жити Звичайний JSON-документ

Емпіричне правило в один рядок: якщо записи приходять з часом або споживаються построково (логи, події, вивантаження, навчальні вибірки) — беріть JSONL; якщо невеликий обсяг створюється один раз, споживається один раз і вимагає семантики цілого документа — беріть JSON-масив. Похідні варіанти (JSON Text Sequences з RS-роздільником) — окрема тема, див. статтю про RFC 7464.

FAQ

Чи є реальна різниця між JSONL і NDJSON?

В інженерному сенсі немає. Обидва означають «одне повне JSON-значення в рядку, без зовнішньої обгортки, без ком між рядками», і інструмент, що підтримує один формат, підтримає й інший. Різниця в назвах і звичках екосистем: .jsonl домінує в інженерії даних та ML-датасетах, .ndjson і application/x-ndjson — у потокових HTTP API. При інтеграції із зовнішньою системою дотримуйтеся розширення та media type з її документації.

Чи можна читати .jsonl напряму через JSON.parse?

Файл цілком — ні. JSON.parse приймає рівно один JSON-текст, а JSONL — це безліч незалежних значень, викладених у рядок, тому парсинг цілком завжди падає з «Unexpected token» або «Extra data». Розбийте на рядки та парсьте кожен непорожній окремо, повідомляючи номер рядка при збої.

Якщо один рядок у JSONL-файлі пошкоджено, зіпсований чи весь файл?

Ні — построкова ізоляція збоїв є ядром дизайну формату. Битий рядок впливає лише на себе: пропустіть його, запишіть номер і продовжуйте читання. Для пакетних імпортів поміщайте биті рядки в окремий файл-карантин, завантажуйте коректні записи як зазвичай, а рішення про лагодження та повторний імпорт ухвалюйте за часткою битих рядків.

Як конвертувати JSONL в Excel або CSV?

Спершу перетворіть його на масив або витягніть поля построково, потім прогоніть через табличне перетворення: стисніть через jq -c '.[]' data.jsonl і подайте конвертеру, або прокачуйте кожен рядок через json.loads у pandas. Обережно: розгорнуті вкладені поля дають назви колонок із крапками, масиви всередині клітинок неминуче перетворюються на текст, а довгі ID-колонки варто заздалегідь оголосити рядками, щоб не втратити точність.

Джерела

Відповідні інструменти

Схожі статті

JSON Lines і NDJSON: потокові дані

Одне значення JSON на рядок дає дописуваність, потоковість і пошкодження, ізольоване одним рядком; ціна — відсутність загальної структури, тож перевіряти доводиться рядок за рядком.

Середній~10 хв читанняОновлено 2026-10-30

Текстові послідовності JSON (RFC 7464): потоковий JSON із роздільником RS

RFC 7464 використовує роздільник 0x1E для потокової передачі текстів JSON, які можна читати й писати водночас. Охоплює правила, відмінність від NDJSON і реалізацію «розділити, потім розібрати».

Середній~9 хв читанняОновлено 2026-10-31

Розбір, відображення й потокова обробка великого JSON

Розбір часто не є вузьким місцем; ним є відображення й копіювання. Оптимізуйте вздовж чотирьох шарів «розбір → обхід → відображення → передача», щоб ваші зусилля не змарнувалися.

Середній~13 хв читанняОновлено 2026-10-31