JSON Lines و NDJSON: داده جریانی
جمعبندی: یک مقدار JSON در هر خط برای شما قابلیت افزودن، جریان و آسیب محدود به یک خط را میخرد؛ هزینهاش نبود ساختار کلی است، پس اعتبارسنجی باید خطبهخط باشد.

جمعبندی: وقتی یک دسته را یکجا پردازش میکنید از آرایه JSON استفاده کنید، و وقتی افزودن جریانی و جداسازی خط بد لازم دارید از JSON Lines؛ اگر همچنان باید ساختار کلی را اعتبارسنجی کنید، به قالب خطمحدود روی نیاورید.
وقتی داده از حافظه بگذرد یا مدام برسد، ساختار «یک آرایه بزرگ» دشمن میشود.
۱. قواعد قالب
JSON Lines (.jsonl) و NDJSON (.ndjson) عملاً مترادفاند و قواعد در هر دو کوتاه است:
- رمزگذاری UTF-8 است، بدون BOM؛
- هر خط یک مقدار JSON کامل و معتبر دارد؛
- خطوط با
\nجدا میشوند؛ خط آخر ممکن است خط جدید داشته باشد یا نه؛ - هیچ خط جدید escapeنشدهای نباید داخل یک خط ظاهر شود — خط جدید داخل رشته باید
\nنوشته شود؛ - خطوط خالی معمولاً نادیده گرفته میشوند، اما پیادهسازی سختگیر باید آنها را نامعتبر بداند.
{"time":"2026-10-30T10:41:45+08:00","level":"info","message":"boot"}
{"time":"2026-10-30T10:41:46+08:00","level":"warn","message":"slow query","costMs":812}
{"time":"2026-10-30T10:41:47+08:00","level":"error","message":"timeout","traceId":"a1b2"}
تفاوت ظریف دو: وبسایت JSON Lines تأکید میکند «یک مقدار در هر خط، و مقدار میتواند هر JSON باشد (شامل اسکالرها)»، در حالی که مشخصات NDJSON قواعد پیادهسازی سختگیرتری مانند «بدون خط خالی و بدون اسکالر سطح بالا» اضافه میکند. در مهندسی، پیروی از مجموعه سختگیرتر شما را از دردسر دور نگه میدارد.
۲. مقایسه با آرایه JSON
| بُعد | آرایه JSON | JSON Lines |
|---|---|---|
| افزودن یک رکورد | بازنویسی کل فایل (تغییر ] پایانی) |
فقط افزودن یک خط |
| خواندن جریانی | پارسر جریانی لازم دارد که عمق براکت را ردیابی کند | تقسیم بر اساس خط |
| یک ورودی خراب | کل فایل غیرقابل تجزیه میشود | فقط همان خط تحت تأثیر است |
| اعتبارسنجی کل فایل | همهچیز در یک مرور | فقط خطبهخط |
| فشردهسازی | اندازه مشابه پس از gzip | همان، و بلوکها میتوانند موازی فشرده شوند |
| بیان تودرتویی | بومی پشتیبانی میشود | هر خط میتواند درونی تودرتو شود؛ خطوط به هم مرتبط نیستند |
| مصرفکنندههای معمول | همه ابزارهای JSON | سیستمهای لاگ، خطوط داده بزرگ، مجموعههای آموزش LLM |
نتیجه مستقیم است: اینکه داده «دستهای رکورد» است یا «یک سند» تعیین میکند کدام قالب استفاده شود.
۳. خواندن: جریان خطبهخط
در Node، سادهترین رویکرد readline است:
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
/**
* Reads a JSON Lines file line by line and invokes a callback for each record.
* @param filePath Path to the file
* @param onRecord Handler for a single record
* @returns Statistics containing the number of successful and failed lines
*/
async function readJsonLines(
filePath: string,
onRecord: (record: unknown, lineNumber: number) => void,
): Promise<{ okCount: number; badCount: number }> {
const lineReader = createInterface({
input: createReadStream(filePath, { encoding: 'utf8' }),
crlfDelay: Infinity,
});
let okCount = 0;
let badCount = 0;
for await (const rawLine of lineReader) {
const line = rawLine.trim();
if (!line) {
continue;
}
try {
onRecord(JSON.parse(line), okCount + badCount + 1);
okCount += 1;
} catch {
badCount += 1;
}
}
return { okCount, badCount };
}
مصرف حافظه این کد فقط به اندازه یک خط بستگی دارد، نه حجم کل فایل — همان بزرگترین ارزش یک قالب خطمحدود.
۳.۱ وقتی فقط چند فیلد میخواهید، اول JSON.parse نکنید
اگر هر رکورد ۵۰ فیلد دارد و شما فقط ۳ را میخواهید، JSON.parse برای همه ۵۰ شیء میسازد.
پارسر جریانی (stream-json، clarinet) میتواند فقط فیلدهای هدف را استخراج کند، به قیمت پیچیدگی پیادهسازی بیشتر.
قاعده سرانگشتی: وقتی یک رکورد زیر چند صد کیلوبایت است، صدا زدن مستقیم JSON.parse معمولاً همچنان معامله بهتری است.
۳.۲ متن چینی و BOM
فایلهای .jsonl تولیدشده روی ویندوز غالباً BOM دارند و JSON.parse روی خط اول قطعاً شکست میخورد. BOM را پیش از خواندن حذف کنید، یا هنگام جریان دادن نخستین تکه سه بایت اول را بررسی کنید.
۴. نوشتن: افزودن و اتمیک بودن
import { appendFile } from 'node:fs/promises';
/**
* Appends a single JSON Lines record.
* @param filePath Target file
* @param record The record object to write
* @returns A promise that resolves when the write completes
*/
async function appendRecord(filePath: string, record: unknown): Promise<void> {
const line = `${JSON.stringify(record)}\n`;
await appendFile(filePath, line, { encoding: 'utf8' });
}
دو نکته:
- خروجی
JSON.stringifyخط جدید ندارد (مگر اینکه\nیک رشته به دو نویسه escape شود)، پس یک خط همیشه دقیقاً یک رکورد است؛ - وقتی چند فرایند همزمان افزودن میکنند، یک
writeکوچکتر از PIPE_BUF (معمولاً ۴۰۹۶ بایت روی Linux) اتمیک است؛ بزرگتر از آن تضمین نمیشود. برای سناریوهای پرهمزمانی، به «یک فایل به ازای هر فرایند، ادغام پس از flush روی دیسک» روی آورید، یا از ذخیرهسازی دارای تراکنش استفاده کنید.
۵. برخورد با خطوط بد
این تصمیمی است که قالب خطمحدود از قبل مجبورت میکند بگیرید:
| راهبرد | مناسب برای | هزینه |
|---|---|---|
| رد کردن و شمارش | دادهای که شکاف را تحمل میکند، مانند لاگ و رویداد تحلیلی | داده بیصدا از دست میرود، پس هشداردادن الزامی است |
| شکست فوری | دادهای که نباید از دست برود، مانند صورتحساب و فاکتور | یک خط بد کل دسته را مسدود میکند |
| مسیردهی به فایل dead-letter | مواردی که بعداً بازپرش (backfill) لازم دارند | ذخیرهسازی اضافه و فرایند بازپخش |
هر کدام را انتخاب کنید، شماره خط و تکهای از محتوای اصلی را ثبت کنید، وگرنه بعداً نمیتوانید مشکل را مکانیابی کنید. شماره خط در پیام خطای JSON.parse نیست، پس باید خودتان نگهش دارید.
۶. فشردهسازی و تکهتکه کردن
- gzip فایل
.jsonlرا با نسبت بالا و یکدست فشرده میکند (نام فیلدها تکرار میشوند)، پس.jsonl.gzتوصیه میشود؛ - gzip بلوکی فشرده میکند و بومی از decompression جریانی پشتیبانی میکند، پس نیازی به decompress کل فایل نیست؛
- فایلهای بزرگ را به برشهایی حدوداً
1GBتقسیم کنید، با شماره ترتیب در نامها، تا پردازش موازی و انتقال قابل ادامه آسان شود؛ - هرگز
.jsonlرا با «ادغام در آرایه» پیشپردازش نکنید — همه مزایایش را دور میریزد.
۷. مصالحه با قالبهای دیگر داده بزرگ
| قالب | ساختار | Schema دارد | مناسب برای |
|---|---|---|---|
| JSON Lines | مستقل در هر خط | نه (قرارداد بیرونی لازم دارد) | لاگ، ورود و خروج، داده آموزش |
| CSV | جدولی | نه | گزارش، تبادل Excel |
| Parquet | ستونی | بله | پرسوجوهای تحلیلی، مجموعهدادههای آفلاین بسیار بزرگ |
| Avro | ردیفی | بله | پیامهای Kafka، تکامل schema |
مزیت JSON Lines این است که هر زبان و هر ابزاری میتواند فوراً بخواند و بنویسدش؛ عیبش نبود schema و فشردهسازی ستونی است. وقتی داده دیگر در یک ماشین جا نمیشود و به پرسوجوهای ستونمحور مکرر نیاز دارید، زمان در نظر گرفتن Parquet است.
۸. چکلیست
- [ ] پسوند فایل یکدست است (
.jsonlیا.ndjson) و هرگز مخلوط نمیشود؛ - [ ] نویسنده «یک رکورد در هر خط» را تضمین میکند، با خطوط جدید داخل رشتهها از قبل escapeشده؛
- [ ] خواننده BOM را حذف میکند، خطوط خالی را رد میکند و شماره خط را نگه میدارد؛
- [ ] راهبرد خط بد صریحاً تعریف و پایش میشود (خطوط بد / کل خطوط)؛
- [ ] فایلهای بزرگ بر اساس اندازه برش میخورند و با gzip و خواندن موازی همراهاند؛
- [ ] هر جا قرارداد فیلد وجود دارد، schema اضافه و خطبهخط اعتبارسنجی میشود.
پرسشهای متداول
چگونه بین آرایه JSON و JSON Lines انتخاب کنم؟
ببینید داده دستهای رکورد است یا یک سند. وقتی رکوردها افزوده و بهصورت جریان خوانده میشوند JSON Lines را انتخاب کنید: افزودن یعنی نوشتن یک خط در انتها، و خواندن فقط تقسیم بر اساس خط لازم دارد. وقتی کل ساختار باید در یک مرور اعتبارسنجی شود آرایه JSON را نگه دارید، چون آرایه JSON همهچیز را یکجا اعتبارسنجی میکند در حالی که JSON Lines فقط خطبهخط.
چرا یک خط خراب کل فایل را نمیشکند؟
هر خط در JSON Lines یک مقدار JSON مستقل و کامل است و تجزیه طبیعتاً خطبهخط است، پس داده خراب فقط همان خط را تحت تأثیر قرار میدهد. در آرایه JSON، یک خطای نحوی کل فایل را غیرقابل تجزیه میکند، که دقیقاً دلیل مناسب بودن داده خطمحدود برای لاگ و رویداد تحلیلی است.
آیا باید JSON Lines را به آرایه JSON تبدیل کنم؟
میتوانید: خطبهخط بخوانید، JSON.parse روی هر رکورد صدا بزنید و در آرایه جمع کنید. این کار کل محتوا را در حافظه میخواهد و خواندن جریانی و افزودن یکخطی را رها میکند. پیشپردازش .jsonl به آرایه همه مزایای قالب را دور میریزد، پس فقط برای تحویل یکبارهای که داده در حافظه جا میشود معقول است.