العمل العملي مع ملفات JSONL: الفتح والتحويل وتشخيص الأسطر التالفة
الخلاصة: تمرير ملف .jsonl كامل إلى JSON.parse والحصول على خطأ لا يعني في الغالب أن الملف تالف — بل يعني أن طريقة القراءة خاطئة. يغطي هذا الدليل الطرق القياسية لفحص وقراءة JSONL سطرًا بسطر، وأوامر التحويل ذي الاتجاهين مع مصفوفات JSON، وجدولًا بستة أنواع من الأسطر التالفة، والطريقة الصحيحة لاستهلاك NDJSON من استجابات HTTP المتدفقة.

الخلاصة: كل سطر في ملف JSONL قيمة JSON صحيحة ومستقلة، لكن الملف ككل ليس مستند JSON واحدًا. إذا فشل تمرير الملف بأكمله إلى JSON.parse أو json.load، ففي تسع حالات من عشر يكون الملف سليمًا — واستراتيجية القراءة هي الخاطئة.
سجل واحد في كل سطر، بلا فواصل بين الأسطر، بلا مصفوفة خارجية — يتنازل JSONL عن "بنية المستند الكاملة" مقابل كتابة بالإضافة وقراءة متدفقة وعزل الأعطال على مستوى السطر الواحد. المزايا الثلاث لا تتحقق إلا عند معالجة الملف سطرًا بسطر، وتتحول الثلاثة جميعًا إلى أخطاء في اللحظة التي تحاول فيها تحليل الملف كله دفعة واحدة.
1. ثلاثة أسماء، صيغة واحدة: JSONL وNDJSON وJSON Lines
كثيرًا ما يظن المبتدئون أن اختلاف الأسماء يعني اختلاف صياغة. ليس كذلك:
| الاسم | السياق الشائع | الامتداد / نوع الوسائط |
|---|---|---|
| JSON Lines | هندسة البيانات، مجموعات بيانات تعلم الآلة (pandas وSpark وHugging Face) | .jsonl |
| NDJSON | واجهات HTTP المتدفقة، البيانات اللحظية | .ndjson، application/x-ndjson |
| JSONL | الاسم العامي المختصر لأي من الاثنين | — |
أي أداة تقرأ أحدهما تقرأ الآخر. المعيار الوحيد لاختيار الاسم هو عرف منظومتك البيئية اللاحقة: اكتب .jsonl عند تجهيز مجموعات بيانات Hugging Face، واضبط application/x-ndjson عند تقديم واجهة متدفقة. ملف JSONL قابل للنقل يستوفي ثلاث قواعد: ترميز UTF-8 (بدون BOM)، قيمة JSON كاملة واحدة في كل سطر، وفصل الأسطر بـ \n. وجود سطر جديد ختامي في نهاية الملف مستحسن بشدة، والأسطر الفارغة ليست سجلات صحيحة. تفاصيل البروتوكول ومبررات التصميم مغطاة في مقالنا عن JSON Lines وNDJSON؛ هذا المقال يقتصر على ما تفعله بعد أن يكون الملف بين يديك.
2. الفحص: head أولًا ثم grep، والمحرر آخر شيء
النقر المزدوج على ملف .jsonl بحجم 500 ميغابايت لفتحه في محرر هو الخطأ الأكثر شيوعًا: ثلاثة ملايين سطر من النص مع تلوين الصياغة مع مخزن التراجع سيتجمدان أي محرر تقريبًا — وJSON بريء من ذلك. تحليل مستند بحجم 45 ميغابايت يستغرق نحو 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 تمامًا؛ واستخدم gojq إذا احتجت تضمين تعبيرات jq. أما المكتبات فلا تستعجل تثبيت الحزم: jsonlines في Python تبلغ برقم السطر ونصه الخام عند فشله، وorjsonl تقرأ وتكتب ملفات gzip/bzip2/zstd المضغوطة مباشرة؛ حزمة ndjson في Node لم تنشر إصدارًا منذ 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
أوامر Python أحادية السطر مكافئة: المصفوفة → JSONL تكتب json.dumps(r, ensure_ascii=False) + "\n" لكل سجل؛ والاتجاه العكسي يجمع نتائج json.loads سطرًا بسطر في قائمة. لكل اتجاه فخ واحد. في الذهاب (المصفوفة → JSONL) يجب أن تكون كل سطرية جديدة حقيقية داخل السجل مهرَّبةً بواسطة json.dumps — وإلا انشق السجل الواحد إلى أسطر متعددة. في العودة (JSONL → المصفوفة) لا تلصق الأسطر في سلسلة عملاقة واحدة ثم حللها — مع ملايين الأسطر فهذه نسخة كاملة غير ضرورية في الذاكرة. التجميعات (المجاميع والتجميعات والمتوسطات) تتطلب -s التي تحمل كل شيء؛ في الملفات بالأحجام الجيجابايتية انتقل إلى awk أو مجمع تدفقي بلغة Python.
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 (الضغط العكسي)، وإلا نما الذاكرة بلا حدود كلما كان المستهلك أبطأ من المنتج؛ ثانيًا، عطّل وسيط 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 على هندسة البيانات ومجموعات تعلم الآلة؛ وتهيمن .ndjson وapplication/x-ndjson على واجهات HTTP المتدفقة. عند التكامل مع نظام خارجي، التزم بالامتداد ونوع الوسائط الذي تحدده وثائقته.
هل يمكنني قراءة ملف .jsonl مباشرة بـ JSON.parse؟
ليس الملف كله. JSON.parse يقبل نص JSON واحدًا بالضبط، بينما JSONL مجموعة قيم مستقلة مصفوفة سطرًا بسطر، فتحليل الكل يفشل دائمًا بـ "Unexpected token" أو "Extra data". قسّم إلى أسطر وحلل كل سطر غير فارغ منفردًا، وبلّغ برقم السطر عند الفشل.
إذا كان سطر واحد في ملف JSONL تالفًا، هل تخرب الملف بأكمله؟
لا — عزل الأعطال سطرًا بسطر هو جوهر تصميم هذه الصيغة. السطر التالف يضر بنفسه وحده: تخطَّه، وسجّل رقمه، وواصل القراءة. في الاستيراد الدفعي، ضع الأسطر التالفة في حجر صحي بملف منفصل، وحمّل السجلات الصحيحة كالمعتاد، ثم قرر الإصلاح وإعادة الاستيراد بحسب نسبة الأسطر التالفة.
كيف أحوّل JSONL إلى Excel أو CSV؟
حوّله أولًا إلى مصفوفة أو استخرج الحقول سطرًا بسطر، ثم شغّل تحويلًا جدوليًا: اضغط بـ jq -c '.[]' data.jsonl وسلّم الناتج لمحوّل، أو مرر كل سطر عبر json.loads إلى pandas. انتبه: الحقول المتداخلة المفلطحة تنتج أسماء أعمدة بنقاط، والحقول المصفوفية لا تُسلسل داخل الخلية إلا كنص، وأعمدة المعرفات الطويلة ينبغي إعلانها سلاسل منذ البداية تفاديًا لفقدان الدقة.