JSON Lines и JSON: как читать записи без потери строк
Разбираем JSONL по строкам, отличаем его от массива JSON и проверяем пустые строки, переносы и ошибки на синтетическом примере.
Файл с двумя объектами, разделёнными переводом строки, может быть исправным JSON Lines и одновременно не проходить проверку обычного JSON. Это не противоречие: получатели ожидают разное обрамление записей. Поэтому сначала выясните формат источника, а потом исправляйте документ.
В обычном JSON весь текст представляет одно значение. JSON Lines, часто обозначаемый расширением .jsonl, содержит отдельное значение JSON на каждой строке. В описании формата требуются UTF-8 и отсутствие пустых записей; финальный перевод строки рекомендован. Описание JSON Lines.
Два представления одного набора
Синтетический массив JSON:
[{"id":"DEMO-1","event":"open"},{"id":"DEMO-2","event":"close"}]
Те же записи в JSON Lines:
{"id":"DEMO-1","event":"open"}
{"id":"DEMO-2","event":"close"}
У второго представления нет общих квадратных скобок и запятой между строками. Если вставить его целиком в JSON validator Neraviko, операция ожидает одно значение, поэтому не подтверждает корректность такого потока. Для разбора отдельной записи вставьте ровно одну строку. У Neraviko нет заявленного режима загрузки JSONL или автоматической проверки файла по строкам.
Не добавляйте общие скобки механически к большому потоку. Получателю может требоваться именно JSONL, а сборка массива меняет способ потребления и потребность в памяти. Сначала сверьте документацию импорта: формат файла, допустимые значения строки и требования к ошибкам.
Локальная проверка с номером строки
Следующий Python-пример использует только стандартную библиотеку и проверяет две искусственные записи. Его можно выполнить целиком; сеть и файлы не нужны:
import io
import json
def reject_constant(value):
raise ValueError(f"Non-JSON constant: {value}")
source = '{"id":"DEMO-1","event":"open"}\n' \
'{"id":"DEMO-2","event":"close"}\n'
count = 0
for number, line in enumerate(io.StringIO(source), 1):
if not line.strip():
raise ValueError(f"Blank record at line {number}")
try:
record = json.loads(line, parse_constant=reject_constant)
except ValueError as error:
raise ValueError(f"Invalid JSON at line {number}") from error
if not isinstance(record, dict):
raise ValueError(f"Expected object at line {number}")
count += 1
for constant in ("NaN", "Infinity", "-Infinity"):
try:
json.loads('{"value":' + constant + '}',
parse_constant=reject_constant)
except ValueError:
continue
raise AssertionError(f"Accepted non-JSON constant: {constant}")
print(count)
Результат — 2. Требование объекта в коде относится к нашему учебному контракту. Сам JSON Lines разрешает другие JSON-значения: например, null. Для реального файла замените io.StringIO(source) на открытый текстовый файл с encoding="utf-8". Возможности json.loads описаны в документации Python.
Стандартный Python decoder по умолчанию принимает NaN и Infinity как расширения. Здесь callback parse_constant явно отклоняет их, включая -Infinity; отрицательные проверки находятся в том же примере. Это важно и для вложенного поля объекта: проверка одного лишь типа корневого значения не обнаружила бы такой литерал.
Пример останавливается на первой ошибке. Это намеренная политика: он не считает частичный импорт успешным. Если рабочий процесс должен пропускать повреждённые записи, заведите явный отчёт отказов и согласуйте допустимость потерь. Молчаливое продолжение после ошибки трудно отличить от полного успеха.
Перенос внутри сообщения
Текстовое поле может содержать логический перевод строки:
{"id":"DEMO-3","message":"first\nsecond"}
Здесь последовательность \n находится внутри строки, а запись физически занимает одну строку. После разбора значение содержит перевод строки. Если редактор заменит escape реальным переносом между кавычками, одна запись окажется разделена на две физические строки и перестанет соответствовать примеру.
Не форматируйте весь JSONL как набор красивых многострочных объектов. Для просмотра можно отформатировать одну запись, сохранив её исходную строку отдельно. Для экспорта обратно нужен однострочный сериализованный результат каждой записи и разделитель записей.
После аварийной остановки записи последний объект может оказаться обрезанным. Не считайте отсутствие финального перевода строки само по себе повреждением: оно допускается описанием формата. Проверяйте, разбирается ли последний текст как завершённое значение. Если нет, сохраните отказ с номером строки и выясните, можно ли восстановить запись из источника. Автоматическое удаление последней строки создаёт потерю, которую нужно явно учесть.
Что проверять помимо количества
Подсчёт физических переводов строки не равен проверке данных. Финальный перенос, пустая строка и повреждённая запись могут изменить такой счётчик. Наш пример считает успешно разобранные объекты. Для импорта также нужны обязательные поля, уникальность идентификаторов и типы значений.
Проверка синтаксиса каждой строки не гарантирует отсутствие повторяющихся ключей или совместимость больших чисел с получателем. Эти проверки нужно добавить к контракту отдельно. Если записи имеют одинаковую структуру, проверяйте каждую по JSON Schema в пределах поддерживаемого подмножества инструмента.
У JSON-массива тоже бывают потоковые парсеры: формат сам по себе не требует обязательно загружать всё в память. Преимущество JSONL здесь практическое — явные границы отдельных записей. Для HTTP-потока сетевой фрагмент не обязан совпадать со строкой: читатель сначала накапливает байты до разделителя, затем разбирает запись.
При использовании Neraviko текст отправляется на сервер. Для диагностики журнала возьмите минимальную синтетическую запись, а не полный рабочий лог с токенами. Операция JSON-проверки имеет лимит 262 144 байта запроса и общие ограничения API; этот лимит не обещает пакетный импорт JSONL.