کار عملی با فایلهای JSONL: باز کردن، تبدیل و رفع اشکال خطوط خراب
جمعبندی: اگر کل فایل .jsonl را به JSON.parse بدهید و خطا بگیرید، تقریباً هرگز به معنی خراب بودن فایل نیست — یعنی روش خواندن اشتباه بوده. این راهنما روشهای استاندارد بازرسی و خواندن خط به خط JSONL، دستورات تبدیل دوطرفه با آرایه JSON، جدول شش نوع خط خراب و روش درست مصرف NDJSON از پاسخهای HTTP استریمی را پوشش میدهد.

خلاصه: هر خط از یک فایل JSONL یک مقدار JSON مستقل و معتبر است، اما کل فایل یک سند JSON واحد نیست. اگر دادن کل فایل به JSON.parse یا json.load شکست بخورد، در نه مورد از ده بار فایل سالم است — استراتژی خواندن شما غلط است.
یک رکورد در هر خط، بدون ویرگول بین خطوط، بدون آرایه بیرونی — JSONL از «ساختار کلی سند» صرفنظر میکند و در عوض نوشتن الحاقی، خواندن استریمی و ایزولهکردن خطای هر خط را به دست میآورد. هر سه مزیت فقط وقتی محقق میشوند که فایل را خط به خط پردازش کنید، و هر سه در لحظهای که بخواهید کل فایل را یکجا پارس کنید به خطا تبدیل میشوند.
۱. سه نام، یک فرمت: JSONL، NDJSON، JSON Lines
تازهکارها اغلب تفاوت نامها را تفاوت نحوی میپندارند. اینطور نیست:
| نام | بافت رایج | پسوند / media type |
|---|---|---|
| JSON Lines | مهندسی داده، دیتاستهای ML (pandas، Spark، Hugging Face) | .jsonl |
| NDJSON | APIهای استریم HTTP، دادههای بیدرنگ | .ndjson، application/x-ndjson |
| JSONL | مخفف روزمره برای هر دو | — |
هر ابزاری که یکی را میخواند، دیگری را هم میخواند. تنها معیار انتخاب نام، عرف اکوسیستم پاییندستی شماست: هنگام آمادهسازی دیتاست برای Hugging Face بنویسید .jsonl، و هنگام ارائه API استریمی application/x-ndjson را تنظیم کنید. یک فایل JSONL قابلحمل سه قاعده را برآورده میکند: کدگذاری UTF-8 (بدون BOM)، یک مقدار JSON کامل در هر خط، و جداسازی خطوط با \n. وجود یک خط جدید پایانی در انتهای فایل بهشدت توصیه میشود و خطوط خالی رکورد معتبری نیستند. جزئیات پروتکل و منطق طراحی در مرور JSON Lines و NDJSON ما پوشش داده شده؛ این مقاله صرفاً درباره کاری است که وقتی فایل دستتان است باید انجام دهید.
۲. بازرسی: اول head، بعد grep، ویرایشگر در آخر
دابلکلیک روی یک .jsonl پانصد مگابایتی برای باز کردن در ویرایشگر، رایجترین اشتباه است: سه میلیون خط متن بهعلاوه هایلایت نحوی بهعلاوه بافر Undo تقریباً هر ویرایشگری را قفل میکند — و تقصیر JSON نیست. پارس سند ۴۵ مگابایتی در Node 22 حدود ۲۰۸ میلیثانیه طول میکشد؛ چیزی که هنگ میکند رندر است. فرمتهای خطمحور برای ابزارهای خطی ساخته شدهاند، چون رکوردها با خطوط فیزیکی منطبقاند:
# اول اندازه را بسنجید: تعداد رکوردها و حجم فایل، بدون خواندن محتوا
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 بزرگ صفحهها را کند میکنند توضیح داده شده است.
۳. سه الگوی استاندارد خواندن خط به خط
خواندن خط به خط باید سه چیز را مدیریت کند: پرش از خطوط خالی (مخصوصاً خط جدید پایانی)، گزارش خطاها با شماره خط، و اینکه هرگز یک خط خراب کل کار را نکشد. الگوهای استاندارد هر سه زبان حول همین سه نکتهاند.
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 استفاده کنید. درباره کتابخانهها عجلهای برای نصب پکیج نداشته باشید: کتابخانه jsonlines در پایتون شماره خط و متن خام خط خراب را گزارش میدهد و orjsonl فایلهای فشرده gzip/bzip2/zstd را مستقیم میخواند و مینویسد؛ پکیج ndjson در Node از سال ۲۰۲۰ نسخهای منتشر نکرده — readline داخلی کافی است.
۴. تبدیل به آرایه 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
دستورهای یکخطی پایتون معادلاند: آرایه ← JSONL برای هر رکورد json.dumps(r, ensure_ascii=False) + "\n" مینویسد؛ جهت معکوس خروجی json.loads هر خط را در یک لیست جمع میکند. هر جهت یک تله دارد. در رفت (آرایه ← JSONL) هر خط جدید واقعی داخل رکورد باید توسط json.dumps escape شده باشد — وگرنه یک رکورد به چند خط پاره میشود. در برگشت (JSONL ← آرایه) خطوط را به یک رشته غولآسا چسبانده و پارس نکنید — با میلیونها خط این یک کپی کامل غیرضروری در حافظه است. تجمیع (مجموع، گروهبندی، میانگین) به -s نیاز دارد که همهچیز را لود میکند؛ روی فایلهای چند گیگابایتی به awk یا انباشتگر استریمی پایتون سوئیچ کنید.
۵. شش نوع خط خراب و روش عیبیابی
تقریباً هر خطای JSONL در سطح خط رخ میدهد. اعتبارسنجی خط به خط همراه با گزارش شماره خط، تنها تشخیص قابلاتکاست. بر حسب فراوانی:
| علامت | علت ریشهای | اصلاح |
|---|---|---|
Extra data در انتهای خط |
ویرگول بین خطوط اضافه شده | حذف کنید؛ هر خط باید بهتنهایی کامل باشد |
| یک رکورد به چند خط شکسته | خط جدید واقعی بدون escape داخل رکورد (فرمت دستی) | با json.dumps / jq -c دوباره فشرده کنید |
خط اول با Unexpected token ï شکست میخورد |
فایل BOM دارد | با UTF-8 بدون BOM دوباره ذخیره کنید |
| فقط خط آخر خطا میدهد ولی سالم بهنظر میرسد | خط جدید پایانی جا افتاده یا فاصلههای اضافی | \n اضافه کنید؛ طبق عرف خطوط خالی را رد کنید |
| خطای newline داخل رشته | newline واقعی داخل رشته، نه \n |
تولیدکننده را اصلاح کنید؛ هرگز رشتههای JSON را دستی به هم نچسبانید |
| اعداد صحیح بزرگ تغییر مقدار میدهند | پارس از طریق double (برش در 2^53) | از پارسرِ حفظکننده دقت استفاده کنید، ببینید دقت BigInt |
روال تشخیص همیشه سه مرحله دارد: با head -n 1 کدگذاری و ساختار خط اول را تأیید کنید؛ اسکریپت خط به خطی اجرا کنید که شماره همه خطوط خراب را گزارش کند نه اینکه در اولین خطا متوقف شود؛ سپس خطوط خراب را یکییکی بیرون بکشید — یک خط یعنی یک رکورد کامل و بررسی مجزای آن هیچ هزینهای ندارد. برای ایمپورتهای دستهای سیاست «قرنطینه، نه توقف» توصیه میشود: خطوط خراب را با شمارهشان در errors.jsonl بنویسید، به بارگذاری رکوردهای سالم ادامه دهید و تصمیم به ایمپورت دوباره را بر اساس نسبت خطوط خراب بگیرید.
۶. JSONL روی HTTP: مصرف خط به خط و backpressure
الگوی معمول 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 بمانید (backpressure)، وگرنه هر وقت مصرفکننده کندتر از تولیدکننده باشد حافظه بیحد رشد میکند؛ دوم، میدلور gzip را روی مسیرهای استریمی غیرفعال کنید — gzip باید کل پاسخ را قبل از فشردهسازی بافر کند که این، استریم NDJSON را کاملاً بیاثر میکند. هر دو رفتار با تست محلی و چند منبع مستقل انگلیسیزبان تأیید شدهاند، نه حدس و گمان.
۷. کجا 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 در APIهای استریم HTTP غالباند. هنگام یکپارچهسازی با سیستم خارجی، به پسوند و 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 بلند را برای حفظ دقت از ابتدا رشته اعلام کنید.