עבודה מעשית עם קבצי JSONL: פתיחה, המרה ואיתור שורות פגומות
שורה תחתונה: לתת קובץ .jsonl שלם ל-JSON.parse ולקבל שגיאה כמעט אף פעם לא אומר שהקובץ הרוסי — זה אומר שאסטרטגיית הקריאה שגויה. המדריך הזה מכסה את הדרכים הסטנדרטיות לבדיקה וקריאה שורה־שורה של JSONL, פקודות המרה דו־כיוונית מול מערכי JSON, טבלה של שישה סוגי שורות פגומות ואת הדרך הנכונה לצרוך NDJSON מתשובות HTTP בסטרימינג.

בגדול: כל שורה בקובץ JSONL היא ערך JSON תקין ועצמאי, אבל הקובץ כולו אינו מסמך JSON יחיד. אם העברת הקובץ כולו ל-JSON.parse או json.load נכשלת, בתשעה מתוך עשרה מקרים הקובץ בסדר גמור — אסטרטגיית הקריאה שלך היא ששגויה.
רשומה אחת בכל שורה, בלי פסיקים בין שורות, בלי מערך חיצוני — JSONL מוותר על "מבנה המסמך השלם" בתמורה לכתיבה בהוספה, קריאת סטרימינג ובידוד כשל ברמת שורה אחת. שלושת היתרונות מתממשים רק כשמעבדים את הקובץ שורה־שורה, וכולם הופכים לשגיאות ברגע שמנסים לנתח את כל הקובץ בבת אחת.
1. שלושה שמות, פורמט אחד: JSONL, NDJSON, JSON Lines
מתחילים נוטים להניח שהבדל בשמות מרמז על הבדל בתחביר. הוא לא:
| שם | הקשר טיפוסי | סיומת / media type |
|---|---|---|
| JSON Lines | הנדסת נתונים, מערכי נתונים ל-ML (pandas, Spark, Hugging Face) | .jsonl |
| NDJSON | API-ים זורמים של HTTP, נתוני זמן אמת | .ndjson, application/x-ndjson |
| JSONL | קיצור יומיומי לכל אחד מהם | — |
כל כלי שקורא את האחד קורא גם את השני. הקריטריון היחיד לבחירת שם הוא מוסכמות המערכת האקולוגית במורד הזרם שלך: כתבו .jsonl בהכנת מערכי נתונים ל-Hugging Face, והגדירו application/x-ndjson כשמספקים API זורם. קובץ JSONL נייד עומד בשלושה כללים: קידוד UTF-8 (ללא BOM), ערך JSON שלם אחד לכל שורה, ושורות מופרדות ב-\n. ירידת שורה סוגרת בסוף הקובץ מומלצת מאוד, ושורות ריקות אינן רשומות תקינות. פרטי הפרוטוקול והרציונל של העיצוב מוסברים בסקירת JSON Lines ו-NDJSON שלנו; המאמר הזה עוסק בדיוק במה שעושים ברגע שהקובץ ביד.
2. בדיקה: קודם head, אחר כך grep, העורך בסוף
לחיצה כפולה על קובץ .jsonl של 500 MB כדי לפתוח אותו בעורך היא הטעות הנפוצה ביותר: שלושה מיליוני שורות טקסט ועוד הדגשת תחביר ועוד חוצץ ביטול יקפיאו כל עורך — ול-JSON אין שום אשמה. ניתוח מסמך של 45 MB לוקח כ-208 מילישניות ב-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 גדולים מאטים דפים.
3. שלושה דפוסים סטנדרטיים לקריאה שורה־שורה
קריאה שורה־שורה צריכה להתמודד עם שלושה דברים: לדלג על שורות ריקות (במיוחד ירידת השורה הסוגרת), לדווח על שגיאות עם מספר שורה, ולא לאפשר לשורה פגומה אחת להרוג את כל העבודה. הדפוסים הסטנדרטיים בשלוש השפות כולם מתנועעים סביב הנקודות האלה.
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 ישירות; חבילת ה-Node ndjson לא הוציאה גרסה מאז 2020 — ה-readline המובנה מספיק.
4. המרה אל מערך 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 — אחרת רשומה אחת תיקרע לכמה שורות. בדרך חזרה (JSONL → מערך), אל תדביקו את השורות למחרוזת ענקית אחת ואל תנתחו אותה — עם מיליוני שורות זו עותק מלא מיותר בזיכרון. צבירות (סכומים, קיבוצים, ממוצעים) דורשות -s שטוען הכול; בקבצים של כמה GB עברו ל-awk או לצובר זרימה בפייתון.
5. שישה סוגים של שורות פגומות ואיך לאבחן אותן
כמעט כל שגיאת JSONL חיה ברמת השורה. בדיקה שורה־שורה עם דיווח מספרים היא האבחון האמין היחיד. לפי תדירות:
| תסמין | שורש הבעיה | תיקון |
|---|---|---|
Extra data בסוף שורה |
פסיקים שנוספו בין שורות | הסירו; כל שורה צריכה לעמוד בעצמה |
| רשומה אחת שנפרסה על כמה שורות | ירידת שורה אמיתית שלא הוברחה בתוך הרשומה (עוצב ידנית) | דחסו מחדש עם json.dumps / jq -c |
השורה הראשונה נכשלת עם Unexpected token ï |
לקובץ יש BOM | שמרו מחדש כ-UTF-8 בלי BOM |
| שגיאה רק בשורה האחרונה אף שנראית תקינה | חסרה ירידת שורה סוגרת או שיש רווחים תועים | הוסיפו \n; דלגו על שורות ריקות לפי המוסכמה |
| שגיאת ירידת שורה בתוך מחרוזת | ירידת שורה ליטרלית בתוך מחרוזת, לא \n |
תקנו את המחולל; לעולם אל תשרשרו מחרוזות JSON ידנית |
| מספרים שלמים גדולים משנים ערך | ניתוח דרך double (קטיעה ב-2^53) | השתמשו במנתח ששומר על דיוק, ראו דיוק bigint |
שגרת האבחון היא תמיד שלושה שלבים: head -n 1 לאימות הקידוד והמבנה של השורה הראשונה; הרצת סקריפט שורה־שורה שמדווח את המספר של כל שורה פגומה במקום לעצור בראשונה; ואז להוציא את השורות הפגומות אחת־אחת — שורה אחת היא רשומה שלמה, ובדיקתה בנפרד לא עולה דבר. בייבוא אצווה המדיניות המומלצת היא "הסגר, לא עצירה": כתבו שורות פגומות עם המספרים שלהן אל errors.jsonl, המשיכו לטעון את הרשומות הטובות, ואז החליטו על ייבוא מחדש לפי שיעור השורות הפגומות.
6. JSONL מעל HTTP: צריכה שורה־שורה ולחץ נגדי
התבנית הטיפוסית של 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 (לחץ נגדי), אחרת הזיכרון גדל ללא גבול בכל פעם שהצרכן איטי מהיצרן; שנית, כבו את middleware ה-gzip בנתיבי סטרימינג — gzip חייב לצבור את כל התשובה לפני שיוכל לדחוס משהו, וזה מנטרל לגמרי את הסטרימינג של NDJSON. שני ההתנהגויות אומתו בבדיקות מקומיות ובכמה מקורות אנגליים עצמאיים, לא הן ניחוש.
7. מתי 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. באינטגרציה עם מערכת חיצונית, עקבו אחרי הסיומת וסוג המדיה שהתיעוד שלה מציין.
אפשר לקרוא קובץ .jsonl ישירות עם JSON.parse?
לא את הקובץ כולו. JSON.parse מקבל בדיוק טקסט JSON אחד, ואילו JSONL הוא ערכים עצמאיים רבים המסודרים שורה־שורה, ולכן ניתוח הקובץ כולו תמיד נכשל עם "Unexpected token" או "Extra data". פרקו לשורות ונתחו כל שורה לא ריקה בנפרד, תוך דיווח מספר השורה בכישלון.
אם שורה אחת בקובץ JSONL פגומה, האם הקובץ כולו הרוס?
לא — בידוד כשלים שורה־שורה הוא ליבת העיצוב של הפורמט. שורה פגומה מזיקה רק לעצמה: דלגו עליה, רשמו את מספרה והמשיכו לקרוא. בייבוא אצווה, העבירו שורות פגומות להסגר בקובץ נפרד, טענו את הרשומות התקינות כרגיל, ואז החליטו על תיקון וייבוא מחדש לפי שיעור השורות הפגומות.
איך ממירים JSONL ל-Excel או CSV?
קודם הפכו אותו למערך או חלצו שדות שורה־שורה, ואז הריצו המרה טבלאית: דחסו עם jq -c '.[]' data.jsonl והעבירו לממיר, או הזרימו כל שורה דרך json.loads אל pandas. זהירות: שדות מקוננים שמושטים מניבים שמות עמודות עם נקודות, שדות מסוג array ניתנים לסידור בתא רק כטקסט, ועמודות מזהים ארוכים כדאי להצהיר כמחרוזות מראש כדי לא לאבד דיוק.