JSON Lines ו-NDJSON: נתוני זרימה
שורה תחתונה: ערך JSON אחד לשורה קונה לכם הוספה, זרימה ונזק מבודד לשורה אחת; המחיר הוא היעדר מבנה כולל, ולכן האימות חייב להיעשות שורה אחר שורה.

שורה תחתונה: השתמשו במערך JSON כשמעבדים אצווה שלמה בבת אחת, וב-JSON Lines כשצריך הוספות בזרימה ובידוד שורות פגומות; אם עדיין צריך לאמת את המבנה הכולל, אל תעברו לפורמט מופרד שורות.
ברגע שהנתונים עוברים את גבול הזיכרון, או ממשיכים להגיע ברציפות, מבנה "מערך גדול אחד" הופך לאויב.
1. כללי הפורמט
JSON Lines (.jsonl) ו-NDJSON (.ndjson) הם כמעט מילים נרדפות, והכללים קצרים בשני המקרים:
- הקידוד הוא UTF-8, בלי BOM;
- כל שורה מחזיקה ערך JSON שלם ותקין אחד;
- השורות מופרדות ב-
\n; השורה האחרונה יכולה להסתיים בשורה חדשה או לא; - אסור שיופיע שורה חדשה לא מהומלטת בתוך שורה — שורה חדשה בתוך מחרוזת חייבת להיכתב כ-
\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 מוסיף כללי מימוש קפדניים יותר כמו "אין שורות ריקות ואין סקלרים ברמה העליונה". בהנדסה, פעולה לפי הקבוצה הקפדנית יותר מונעת צרות.
2. השוואה למערך JSON
| ממד | מערך JSON | JSON Lines |
|---|---|---|
| הוספת רשומה | כתיבה מחדש של כל הקובץ (שינוי ה-] הסוגר) |
רק הוספת שורה אחת |
| קריאה בזרימה | דורש מפרסר זרימה שעוקב אחרי עומק סוגריים | פיצול לפי שורה |
| רשומה פגומה אחת | כל הקובץ הופך לבלתי ניתן לפרסור | רק השורה ההיא מושפעת |
| אימות הקובץ כולו | אימות הכל במעבר אחד | רק שורה אחר שורה |
| דחיסה | גודל דומה אחרי gzip | זהה, ואפשר לדחוס בלוקים במקביל |
| ביטוי קינון | נתמך באופן מקורי | כל שורה יכולה להתקנן פנימית; שורות אינן קשורות זו לזו |
| צרכנים טיפוסיים | כל כלי JSON | מערכות לוגים, צינורות ביג-דאטה, מערכי אימון LLM |
המסקנה ישירה: האם הנתונים הם "אצווה של רשומות" או "מסמך אחד" קובע איזה פורמט להשתמש.
3. קריאה: זרימה שורה אחר שורה
ב-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 };
}
השימוש בזיכרון של הקוד הזה תלוי רק בגודל שורה בודדת, לא בגודל הקובץ הכולל — זהו הערך הגדול ביותר של פורמט מופרד שורות.
3.1 כשצריך רק כמה שדות, אל תפרסרו קודם עם JSON.parse
אם בכל רשומה יש 50 שדות ואתם צריכים רק 3, JSON.parse בונה אובייקטים עבור כל 50.
מפרסר זרימה (stream-json, clarinet) יכול לחלץ רק את שדות היעד, במחיר מורכבות מימוש גבוהה יותר.
כלל אצבע: כשרשומה בודדת קטנה מכמה מאות KB, קריאה ישירה ל-JSON.parse היא בדרך כלל עדיין העסקה הטובה יותר.
3.2 טקסט סיני ו-BOM
קובצי .jsonl שנוצרים ב-Windows נושאים לעתים קרובות BOM, ו-JSON.parse על השורה הראשונה ייכשל בוודאות. הסירו את ה-BOM לפני הקריאה, או בדקו את שלושת הבתים הראשונים בזמן זרימת ה-chunk הראשון.
4. כתיבה: הוספות ואטומיות
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של מחרוזת מהומלט לשני תווים), כך ששורה אחת היא תמיד בדיוק רשומה אחת; - כשכמה תהליכים מוסיפים במקביל,
writeבודד שקטן מ-PIPE_BUF (בדרך כלל 4096 בתים בלינוקס) הוא אטומי; כל דבר גדול יותר אינו מובטח. בתרחישי מקביליות גבוהה, עברו ל"קובץ אחד לכל תהליך, מיזוג אחרי שטיפה לדיסק", או השתמשו באחסון עם טרנזקציות.
5. טיפול בשורות פגומות
זו ההחלטה שפורמט מופרד שורות מכריח אתכם לקבל מראש:
| אסטרטגיה | מתאים ל | עלות |
|---|---|---|
| דילוג וספירה | נתונים שסובלים פערים, כמו לוגים ואירועי אנליטיקה | נתונים הולכים לאיבוד בשקט, ולכן התראות חובה |
| כשל מיידי | נתונים שאסור לאבד, כמו חיוב וחשבוניות | שורה פגומה אחת חוסמת את כל האצווה |
| ניתוב לקובץ dead-letter | מקרים שדורשים השלמה מאוחר יותר | אחסון נוסף ותהליך הרצה חוזרת |
בכל בחירה, תעדו את מספר השורה וקטע מהתוכן המקורי, אחרת לא תוכלו לאתר את הבעיה אחר כך. מספר השורה אינו מופיע בהודעת השגיאה של JSON.parse, ולכן עליכם לתחזק אותו בעצמכם.
6. דחיסה ופיצול
- gzip דוחס
.jsonlביחס גבוה ועקבי (שמות שדות חוזרים), ולכן.jsonl.gzמומלץ; - gzip דוחס בבלוקים ותומך באופן מקורי בפריסה בזרימה, כך שאין צורך לפרוס את כל הקובץ;
- פצלו קבצים גדולים לפרוסות של בערך
1GB, עם מספרי סדר בשמות, כדי להקל על עיבוד מקבילי והעברה הניתנת להמשך; - לעולם אל תעבדו מראש
.jsonlעל ידי "מיזוג למערך" — זה זורק את כל היתרונות שלו.
7. פשרות מול פורמטי ביג-דאטה אחרים
| פורמט | מבנה | כולל סכימה | טוב ל |
|---|---|---|---|
| JSON Lines | עצמאי בכל שורה | לא (דורש חוזה חיצוני) | לוגים, ייבוא וייצוא, נתוני אימון |
| CSV | טבלאי | לא | דוחות, החלפה עם Excel |
| Parquet | עמודתי | כן | שאילתות אנליטיות, מערכי נתונים עצומים offline |
| Avro | מבוסס שורות | כן | הודעות Kafka, אבולוציית סכימה |
היתרון של JSON Lines הוא שכל שפה וכל כלי יכולים לקרוא ולכתוב אותו מיד; החיסרון הוא היעדר סכימה והיעדר דחיסה עמודתית. ברגע שהנתונים כבר לא נכנסים למכונה אחת וצריך שאילתות עמודתיות תכופות, הגיע הזמן לשקול Parquet.
8. רשימת בדיקה
- [ ] סיומת הקובץ עקבית (
.jsonlאו.ndjson) ואינה מתערבבת לעולם; - [ ] הכותב מבטיח "רשומה אחת לשורה", כאשר שורות חדשות בתוך מחרוזות כבר מהומלטות;
- [ ] הקורא מסיר את ה-BOM, מדלג על שורות ריקות ומתחזק מספרי שורות;
- [ ] אסטרטגיית השורות הפגומות מוגדרת ומנוטרת במפורש (שורות פגומות / סך השורות);
- [ ] קבצים גדולים מפוצלים לפי גודל ומשולבים עם gzip וקריאה מקבילית;
- [ ] במקום שבו קיים חוזה שדות, מסופקת סכימה נוספת והיא מאומתת שורה אחר שורה.
FAQ
איך בוחרים בין מערך JSON ל-JSON Lines?
הסתכלו על האם הנתונים הם אצווה של רשומות או מסמך אחד. בחרו JSON Lines כשרשומות נוספות ונקראות כזרם: הוספה משמעה כתיבת שורה בסוף, וקריאה דורשת רק פיצול לפי שורה. שמרו מערך JSON כשצריך לאמת את המבנה כולו במעבר אחד, כי מערך JSON מאמת הכל בבת אחת בעוד JSON Lines יכול להיות מאומת רק שורה אחר שורה.
למה שורה פגומה אחת לא שוברת את כל הקובץ?
כל שורה ב-JSON Lines היא ערך JSON עצמאי ושלם, והפרסור הוא מטבעו שורה אחר שורה, כך שנתונים פגומים משפיעים רק על השורה ההיא. במערך JSON, שגיאת תחביר אחת הופכת את כל הקובץ לבלתי ניתן לפרסור, וזו בדיוק הסיבה לכך שנתונים מופרדי שורות מתאימים ללוגים ולאירועי אנליטיקה.
כדאי להמיר JSON Lines למערך JSON?
אפשר: לקרוא שורה אחר שורה, לקרוא ל-JSON.parse על כל רשומה ולאסוף אותן למערך. זה דורש שכל התוכן ייכנס לזיכרון ומוותר על קריאה בזרימה ועל הוספת שורה אחת. עיבוד מראש של .jsonl למערך זורק את כל היתרונות של הפורמט, ולכן הוא הגיוני רק למסירה חד-פעמית שבה הנתונים נכנסים לזיכרון.