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