Ориентация главы

Главы 32 и 33 решили, как «один документ» входит и выходит из дерева значений; в инженерии не менее распространена вторая форма — «последовательность записей»: серверные журналы с одним событием на строку, массовый импорт по записи на строку, обмен между процессами по сообщению на строку. JSONL (JSON Lines) превращает каждую строку в одно полное значение JSON; XLONL — в одно полное значение XLON — много строк вместе образуют Array из xvalue, при этом каждая строка отдельно разбирается, локализуется и пропускается. Это не новый парсер: правила кодирования отдельных записей, строгость и домены ошибок дословно наследуются из глав 32/33; добавляются ровно три вещи — семантика кадрирования строк, накопительные бюджеты между записями и пересчёт позиции ошибки внутри записи в глобальный номер строки и индекс записи. Когда нужен «один большой документ», а когда «последовательность записей» — эта глава даёт правила выбора.

34.1引入

Три сценария говорят об одном: естественная единица данных — строка, а не документ. Серверный журнал: каждый обработанный запрос дописывает строку события — при воспроизведении и статистике разбор идёт построчно, плохие строки пропускаются с записью позиции, и лишняя запятая в строке 4096 не должна выбросить весь файл. Массовый импорт: миллионы записей, по одной на строку — DOM главы 32 превращает весь файл в одно дерево (память возрастает в разы), SAX потоковый, но не знает понятия «запись N»; построчный формат делает «разбор не удался на записи N» гражданином первого сорта. Обмен между процессами: внутренние каналы доставляют сообщения построчно — JSON-строки универсальны наружу, XLON-строки (bytes/time/set/intmap главы 33) дают полный круг типов внутри. Три хозяина используют один набор API: прочитать в один Array, записать как последовательность строк, ошибки с номерами строк — кадрирование — дело формата, семантика остаётся семантикой дерева значений.

34.2概念

Кадрирование строк: что считается записью

1РазделениеLF — граница; CRLF считается одной строкой (CR принадлежит концу строки, не записи)
2Пустые строкипустые и из одних ASCII-пробелов/Tab/CR → по умолчанию пропускаются (можно превратить в ошибку)
3Последняя строкапоследняя полная запись может быть без завершающего перевода
4Не разделителиодиночный CR / пробелы Юникода (NBSP не создаёт пустую строку)
5Строка = ровно одно полное корневое значениезначения на несколько строк, несколько корней в строке, BOM, оборванный хвост → ошибка

Правило, которое стоит запомнить, — последнее: строка обязана содержать ровно одно полное значение. Отсюда и семантика «записи»: строка — одновременно граница кадрирования и граница проверки; {"id":1} {"id":2} в одной строке — ошибка (корневых значений больше одного), {"id":\n1} на двух строках — тоже (ни одного полного корня). Экранированный \n внутри строки не участвует в кадрировании — "{\"a\":1}\n" — законная строковая запись.

Двухуровневые бюджеты: запись наследует, поток суммирует

УровеньБюджетСмысл
Запись (Record)MaxInputBytes / MaxOutputBytes / глубинаДословно как в главах 32/33; без разделителей CRLF/LF
Весь вводMaxInputBytesВключая пропущенные пустые строки и все разделители
Весь выводMaxOutputBytesВключая каждый дописанный LF; без завершающего NUL
Число записейMaxRecordsНепустые записи = верхняя граница элементов Array
Синтаксические значенияMaxTotalValuesНакопительно по всем записям; включая отброшенные политикой повторных ключей; без синтезируемого Array
Декодированные байты (только XLONL)MaxTotalDecodedBytesНакопительное декодирование встроенных тегов bytes; проверка перед выделением

Две точки, на которых чаще всего спотыкаются: бюджет всего вывода считает LF (при MaxOutputBytes=1 первая запись "1" фиксируется, а её LF уже даёт LIMIT); бюджет записи не считает разделители (строка длиной до 1 МиБ — это 1 МиБ для MaxInputBytes). Значения по умолчанию следуют обще-библиотечной конвенции: 64 МиБ целиком, 1000000 записей и синтаксических значений — для недоверенного ввода сначала ужесточьте.

Глобальная позиция ошибки: три координаты и один индекс

Ошибка внутри записи (главы 32/33) несёт строку/столбец внутри записи; построчный формат пересчитывает её в глобальные координаты — xrtJsonlErrorLocation возвращает смещение в байтах с нуля, физический номер строки с единицы, байтовый столбец с единицы и индекс записи с нуля. Ошибка пустой строки в строгом режиме указывает на индекс «следующей ожидаемой записи»; пропущенные пустые строки всё равно увеличивают физический номер строки — line=2 record=1 значит «строка 2 пустая, а должна была быть записью 1». Цепочка причин (Cause) сохраняет конкретную причину и Kind разбора записи; обёртка добавляет оболочку только в RECORD/OUTPUT/IO.

Запись: две формы — обратный вызов и файл

Сериализация пишет каждый элемент Array одной компактной строкой: форма с обратным вызовом (xrtJsonlWrite) отдаёт куски sink-у, заимствованные байты действительны только внутри вызова — для потоковой передачи прямо в сеть; файловая форма (xrtJsonlWriteFile) сначала полностью сериализует в памяти и затем атомарно заменяет целевой файл — при ошибке сериализации исходный файл остаётся нетронутым. Общая семантика обеих форм: компактно + LF (PRETTY отвергается), корень обязан быть XVALUE_ARRAY, конфигурация снимается по значению (изменение конфигурации в обратном вызове не влияет на текущий вызов).

JSONL, XLONL или без построчного формата

СценарийВыборПричина
Внешние журналы / открытые данныеJSONLЛюбой jq/скрипт/чужой клиент читает построчно
Внутренние журналы и каналыXLONLbytes/time/set ложатся в строки как есть; воспроизведение без конверсий
Одна конфигурация, целикомДокументы глав 32/33Документная семантика (политика повторных ключей, полные пути SAX/Writer)
Единичные гигантские записи (>неск. МиБ)Документ + потокПострочные бюджеты при ужесточении «на строку» лопаются от одной записи

Переменная выбора изоморфна главе 33: приёмник под контролем → XLONL с полными типами; вне контроля → JSONL для совместимости; если данные — «одна конфигурация», не дробите их на строки; если «поток событий» — не запихивайте в один большой массив.

34.3示例

Полная программа: круг JSONL и позиционирование ошибок

Из примера репозитория examples/data/jsonl/main.c — разбор, валидация, чтение, запись через обратный вызов, атомарный файловый круг и локализация пустой строки за один заход:

examples/data/jsonl/main.c源码 ↗











 */
#include <stdio.h>
#include <xrt.h>

static bool discard(xbytesview Data, ptr pUserData)
{
	(void)Data;
	(void)pUserData;
	return true;
}

int main(void)
{
	xjsonlreadconfig Read;
	xjsonlwriteconfig Write;
	xjsonllocation Location;
	xvalue* pArray = NULL;
	xvalue* pRead = NULL;
	str Text = NULL;
	size_t Size = 0;
	cstr Path = ".xrt-jsonl-example.jsonl";
	bool Created = false;
	int Result = 1;

	xrtJsonlReadConfigInit(&Read);
	xrtJsonlWriteConfigInit(&Write);
	pArray = xrtJsonlParse(XRT_STR_LITERAL("{\"id\":1}\n\n[2,3]\r\nnull\n"));
	if ( pArray == NULL ) goto done;
	Text = xrtJsonlStringify(pArray, &Size);
	if ( Text == NULL ) goto done;
	if ( !xrtJsonlValid((xstrview){ Text, Size }) ) goto done;
	pRead = xrtJsonlRead((xstrview){ Text, Size }, &Read);
	if ( pRead == NULL ) goto done;
	printf("JSONL: %zu records, %zu bytes\n", xrtValueCount(pRead), Size);
	xrtValueRelease(pRead);
	pRead = NULL;
	if ( !xrtJsonlWrite(pArray, &Write, discard, NULL) ) goto done;
	if ( !xrtJsonlStringifyFile(Path, pArray) ) goto done;
	Created = true;
	pRead = xrtJsonlParseFile(Path);
	if ( pRead == NULL ) goto done;
	xrtValueRelease(pRead);
	pRead = NULL;
	if ( !xrtJsonlWriteFile(Path, pArray, &Write) ) goto done;
	pRead = xrtJsonlReadFile(Path, &Read);
	if ( pRead == NULL ) goto done;
	xrtValueRelease(pRead);
	pRead = NULL;
	Read.Flags |= XJSONL_READ_REJECT_EMPTY_LINES;
	pRead = xrtJsonlRead(XRT_STR_LITERAL("{}\n\n"), &Read);
	if ( pRead != NULL ) goto done;
	if ( !xrtJsonlErrorLocation(xrtGetError(), &Location) ) goto done;
	printf("empty line: line=%zu record=%zu\n", Location.Line, Location.RecordIndex);
	xrtClearError();
	Result = 0;
done:
	xrtValueRelease(pRead);
	xrtValueRelease(pArray);
	xrtFree(Text);
	if ( Created && !xrtFileDelete(Path) ) Result = 1;
	return Result;
}

$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/data/jsonl/main.c -lws2_32 -liphlpapiJSONL: 3 records, 20 bytesempty line: line=2 record=1

Что только что произошло. (1) Кадрирование на практике: вход смешивает пустые строки с CRLF/LF — все 3 записи (объект, массив, null) на месте, 20 байт — фактический размер компактного вывода (с LF на запись). (2) Политика пустых строк: при пропуске по умолчанию Valid/Read зелёные; включите XJSONL_READ_REJECT_EMPTY_LINES — и "{}\n\n" сразу падает на пустой строке перед второй записью. (3) Глобальная позиция: ошибка даёт line=2 record=1 — пустая строка во второй строке, она разрезает позицию после первой записи; вызывающий получает «какую строку править», а не диапазон байтов. (4) Файловый контур: StringifyFile атомарно заменяет → ParseFile читает → WriteFile (с конфигурацией) заменяет снова → ReadFile читает ещё раз — любая неудача оставляет файл в последнем полном состоянии.

Полная программа: построчная запись и чтение XLONL

Из examples/data/xlonl/main.c — та же машина кадрирования и бюджетов, правила отдельной записи заменены на XLON:

examples/data/xlonl/main.c源码 ↗











 */
#include <stdio.h>
#include <xrt.h>

static bool discard(xbytesview Data, ptr pUserData)
{
	(void)Data;
	(void)pUserData;
	return true;
}

int main(void)
{
	xxlonlreadconfig Read;
	xxlonlwriteconfig Write;
	xxlonllocation Location;
	xvalue* pArray = NULL;
	xvalue* pRead = NULL;
	str Text = NULL;
	size_t Size = 0;
	cstr Path = ".xrt-xlonl-example.xlonl";
	bool Created = false;
	int Result = 1;

	xrtXlonlReadConfigInit(&Read);
	xrtXlonlWriteConfigInit(&Write);
	pArray = xrtXlonlParse(XRT_STR_LITERAL("{\"id\":1}\n\n[2,3]\r\nnull\n"));
	if ( pArray == NULL ) goto done;
	Text = xrtXlonlStringify(pArray, &Size);
	if ( Text == NULL ) goto done;
	if ( !xrtXlonlValid((xstrview){ Text, Size }) ) goto done;
	pRead = xrtXlonlRead((xstrview){ Text, Size }, &Read);
	if ( pRead == NULL ) goto done;
	printf("XLONL: %zu records, %zu bytes\n", xrtValueCount(pRead), Size);
	xrtValueRelease(pRead);
	pRead = NULL;
	if ( !xrtXlonlWrite(pArray, &Write, discard, NULL) ) goto done;
	if ( !xrtXlonlStringifyFile(Path, pArray) ) goto done;
	Created = true;
	pRead = xrtXlonlParseFile(Path);
	if ( pRead == NULL ) goto done;
	xrtValueRelease(pRead);
	pRead = NULL;
	if ( !xrtXlonlWriteFile(Path, pArray, &Write) ) goto done;
	pRead = xrtXlonlReadFile(Path, &Read);
	if ( pRead == NULL ) goto done;
	xrtValueRelease(pRead);
	pRead = NULL;
	Read.Flags |= XXLONL_READ_REJECT_EMPTY_LINES;
	pRead = xrtXlonlRead(XRT_STR_LITERAL("{}\n\n"), &Read);
	if ( pRead != NULL ) goto done;
	if ( !xrtXlonlErrorLocation(xrtGetError(), &Location) ) goto done;
	printf("empty line: line=%zu record=%zu\n", Location.Line, Location.RecordIndex);
	xrtClearError();
	Result = 0;
done:
	xrtValueRelease(pRead);
	xrtValueRelease(pArray);
	xrtFree(Text);
	if ( Created && !xrtFileDelete(Path) ) Result = 1;
	return Result;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/data/xlonl/main.c -lws2_32 -liphlpapiXLONL: 3 records, 20 bytesempty line: line=2 record=1

Что только что произошло. Поток построчно изоморфен примеру jsonl — это не лень, а зримое исполнение проектного обещания: смени формат — кадрирование, структура бюджетов и локализация ошибок не меняются, формы API соответствуют один к одному. Разница — в семантике отдельной записи: каждая строка здесь — полное значение XLON, и четыре расширенных типа главы 33 входят в построчный поток напрямую —

/* Полнотипные записи в потоке XLONL: после разбора каждая занимает своё место в дереве */
xvalue* pLog = xrtXlonlParse(XRT_STR_LITERAL(
    "{\"key\":bytes(\"AAEC/w==\"),\"at\":time(\"2026-07-31T00:00:00Z\")}\n"
    "set[80, 443]\n"));
/* pLog[0] — объект (поля Bytes/Time), pLog[1] — контейнер set; построчное воспроизведение без конверсий */

Когда встроенному тегу bytes нужен отдельный суммарный предел, MaxTotalDecodedBytes перехватывает до выделения буфера (по умолчанию 64 МиБ) — против дешифровальной бомбы «по чуть-чуть в каждой строке, а в сумме гора».

34.4契约

  • Одно значение на строку: каждая строка — ровно одно полное корневое значение; экранированные переводы внутри строк не кадрируют; последняя запись может быть без перевода.
  • Атомарность неудачи: чтение не возвращает частичный Array (при ошибке освобождаются все частичные результаты); файловая запись сначала сериализует целиком, затем атомарно заменяет — ошибка не трогает оригинал.
  • Смыслы бюджетов: весь ввод/вывод включают разделители и пустые строки; отдельная запись — нет; вывод включает каждый LF — все три смысла — обещания, а не детали реализации.
  • Заимствование и владение: текст ввода и конфигурация заимствуются до возврата вызова; результат Array освобождается через xrtValueRelease, строка — через xrtFree; запись не потребляет ссылки вызывающего (итерация по снимку backing).
  • Потоки: без неявных блокировок; каждый вызов владеет независимым рабочим пространством; общий изменяемый ввод синхронизирует вызывающий.
  • Зафиксированные байты не откатываются: байты записей, уже отданные до неудачи синхронного обратного вызова, вернуть нельзя — см. ловушку 2.

От примеров к инженерии: три хозяина построчных данных

Серверный журнал (самое частое): писатель на каждое событие пишет строку через Stringify и дописывает её или отдаёт через Write прямо в sink; читатель через Parse разбирает в Array для офлайн-статистики, плохие строки через ErrorLocation уходят в аудит-журнал, и поток продолжается. Массовый импорт: Read отдаёт все законные записи разом (любая ошибка — неудача целиком с отчётом по строкам), либо текст режется на куски и разбирается покусочно через Parse — «плохой кусок пропусти, импорт продолжь»; обе семантики стоят на «строка — граница». Межпроцессные потоки: изоморфно правилам главы 33 — наружу JSONL, внутрь XLONL; построчные потоки естественно ложатся на текстовые конвейеры (grep, перенаправление, построчные шлюзы) — это операционное свойство, которого документные форматы не дают.

Контраст: одна и та же пачка событий — два способа на диск

Пачку журналов запросов можно положить одним большим JSON-массивом (глава 32) или JSONL. Массив: дописывание — это прочитать старое, разобрать, добавить, сериализовать, записать (или хрупкий трюк с заплаткой на хвостовом ]); одна порча — и весь файл не разбирается; потоковому потребителю нужно ждать закрытия документа. JSONL: дописывание — обычный AppendFile; испорченная строка вредит только себе; tail -f потребляет по мере поступления. Суть контраста: дописывающая запись и терпимость к частичным порчам — сущностные преимущества потоков записей — пока данные остаются «только растущей последовательностью событий», построчный формат и есть правильная форма; наоборот, конфигурации, требующие цельного чтения и сильных связей полей, остаются территорией документов.

Типичные ошибки

Ловушка 1: ожидание, что стратегия SKIP пропустит «непредставимую» запись:Ловушка 1: ожидание, что стратегия SKIP пропустит «непредставимую» запись

Симптом: при записи в пачке встречается значение, которое JSON не может выразить (например, дескриптор функции); вы ждёте «эту пропустить и продолжить», а падает весь вызов Stringify.

Причина: SKIP действует только на члены внутри одного корневого значения; непредставимый корень валит запись целиком — это зеркальная сторона «одна строка = ровно одно полное значение», защищающая от физических пустых строк, ломающих кадрирование.

错误写法
/* В дереве затесалось корневое значение вне выразимости формата; надеемся на SKIP */
xrtJsonlStringify(pArray, &Size);   /* неудача целиком: непредставимый корень не пропустить */
正确写法
/* Фильтр до записи: убрать непредставимые записи или деградировать их в объекты-заполнители по конвенции */
/* Внешний канал — только JSON-типы; внутренний канал — сразу XLONL: расширенные типы выразимы нативно */
str Text = xrtXlonlStringify(pArray, &Size);
Ловушка 2: уверенность, что неудача записи через обратный вызов «откатит» уже записанное:Ловушка 2: уверенность, что неудача записи через обратный вызов «откатит» уже записанное

Симптом: обратный вызов Write на середине упирается в заполненную цель и возвращает false; вызывающий считает, что файл/поток восстановлен до состояния до записи, и повторяет на месте — в журнале появляется половина пачки.

Причина: синхронный обратный вызов — это потоковая доставка: байты фиксируются на дальней стороне (файл, сокет, пайп) в момент возврата true, и «канала отзыва» у библиотеки нет; это обещанная семантика (пункт 6 контракта), а не дефект.

错误写法
/* Повтор на месте: зафиксированная половина пачки записывается ещё раз */
while ( !xrtJsonlWrite(pArray, &Write, sink, Ctx) )
    ;   /* зафиксированные байты не отозвать — повтор = дубли записей */
正确写法

 * или файловая форма (атомарная замена: всё или ничего по построению) */
if ( !xrtJsonlWriteFile(Path, pArray, &Write) ) { /* оригинал не тронут; починить данные и начать заново */ }

Практические задания

Базовое: аудит плохих строк

Прочитать файл JSONL в Array; вывести таблицу аудита синтаксически ошибочных строк: номер строки, индекс записи и смещение в байтах (подсказка: резать текст на строки и прогонять xrtJsonValid по каждой, либо строгий режим пустых строк плюс ErrorLocation). Приёмка: на файле из 10 строк с 2 подложенными ошибками отчёт указывает ровно на них.

Продвинутое: конвертер JSONL ⇄ большой массив

Реализовать оба направления: файл JSONL → один документ-массив JSON; документ-массив → JSONL. Накопительные бюджеты обязаны ограничивать память (общий предел настраивается), ошибки конвертации сообщаются по индексу строки/элемента. Приёмка: Valid зелёный в обе стороны; круговой проход совпадает с исходным файлом запись к записи.

Вызов: потоковый импортёр с докачкой с контрольной точки

Собрать конвейер «плохой кусок пропусти — импорт продолжи»: разбор через Parse кусками по N строк; плохой кусок деградирует в построчный Parse с точным пропуском плохой строки; прогресс (число импортированных записей) сохраняется на диск для докачки без дублей. Приёмка: на файле 10 000 строк с 3 разбросанными ошибками один прогон импортирует 9 997 записей и рапортует 3 строки; второй прогон — ноль повторов.

Итоги

ТочкаШпаргалка
КадрированиеОдно значение на строку; LF/CRLF равноправны; пустые строки пропускаются по умолчанию; последняя строка может быть без перевода; одиночный CR/пробелы Юникода — не разделители
БюджетыВесь поток включает разделители и пустые строки; вывод включает LF; запись — без разделителей; по умолчанию 64 МиБ / 1 000 000 записей
ПозицияСмещение с нуля + строка/столбец с единицы + индекс записи с нуля; ошибка пустой строки указывает на следующую ожидаемую запись
ЧтениеВсё или ничего (без частичного Array); Valid — без DOM, без политики повторных ключей
ЗаписьКомпактно + LF (PRETTY отвергается); потоковый обратный вызов / атомарная замена файла; зафиксированные байты не откатываются
ВыборПотоки событий → построчный формат (наружу JSONL / внутрь XLONL); конфигурации → документы глав 32/33
Дельта XLONLЗапись = правила XLON; MaxTotalDecodedBytes ограничивает суммарное декодирование встроенных bytes

📖 Справочники API в этой главе:jsonl (17 записей)xlonl (17 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки