JSON Lines ও NDJSON: স্ট্রিমিং ডেটা
সংক্ষেপে: প্রতি লাইনে একটি JSON ভ্যালু দিলে অ্যাপেন্ডযোগ্যতা, স্ট্রিমিং আর একক লাইনে সীমাবদ্ধ ক্ষতি পাওয়া যায়; দাম হলো সামগ্রিক কাঠামোর অনুপস্থিতি, তাই যাচাই লাইন ধরে ধরে করতে হয়।

মূল কথা: পুরো ব্যাচ একসঙ্গে প্রসেস করলে JSON অ্যারে ব্যবহার করুন, আর স্ট্রিমিং অ্যাপেন্ড ও খারাপ-লাইন বিচ্ছিন্নতা দরকার হলে JSON Lines; সামগ্রিক কাঠামো এখনও যাচাই করতে হলে লাইনভিত্তিক ফরম্যাটে যাবেন না।
ডেটা মেমরি ছাড়িয়ে গেলে, বা অবিরাম আসতেই থাকলে, "এক বড় অ্যারে" কাঠামোই শত্রু হয়ে ওঠে।
১. ফরম্যাটের নিয়ম
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 স্পেসিফিকেশন "খালি লাইন নেই ও টপ-লেভেল স্কেলার নেই"-র মতো আরও কড়া বাস্তবায়ন নিয়ম যোগ করে। ইঞ্জিনিয়ারিংয়ে কড়া সেটি মেনে চললে ঝামেলা এড়ানো যায়।
২. 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) কেবল লক্ষ্য ফিল্ড বের করতে পারে, তবে বাস্তবায়নের জটিলতা বেশি।
হাতের নিয়ম: একটি রেকর্ড কয়েকশো KB-এর নিচে থাকলে সরাসরি JSON.parse কল করাই সাধারণত লাভজনক।
৩.২ চীনা লেখা ও BOM
Windows-এ তৈরি .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দুই অক্ষরে এস্কেপ না হলে), তাই একটি লাইন মানে সবসময় ঠিক একটি রেকর্ড;- একাধিক প্রসেস একসঙ্গে অ্যাপেন্ড করলে PIPE_BUF-এর চেয়ে ছোট (Linux-এ সাধারণত ৪০৯৬ বাইট) একক
writeপারমাণবিক; তার চেয়ে বড় হলে নিশ্চয়তা নেই। উচ্চ-কনকনরেন্সি পরিস্থিতিতে "প্রতি প্রসেসে একটি ফাইল, ডিস্কে ফ্লাশের পর একত্র করা" পদ্ধতিতে যান, বা লেনদেনসহ স্টোরেজ ব্যবহার করুন।
৫. খারাপ লাইন সামলানো
লাইনভিত্তিক ফরম্যাট আপনাকে আগেই এই সিদ্ধান্ত নিতে বাধ্য করে:
| কৌশল | উপযুক্ত | দাম |
|---|---|---|
| এড়িয়ে যাওয়া ও গণনা | ফাঁক সহনশীল ডেটা, যেমন লগ ও অ্যানালিটিক্স ইভেন্ট | ডেটা নীরবে হারায়, তাই অ্যালার্ট বাধ্যতামূলক |
| সঙ্গে সঙ্গে ব্যর্থ হওয়া | যে ডেটা হারানো যাবে না, যেমন বিলিং ও ইনভয়েস | একটি খারাপ লাইন পুরো ব্যাচ আটকে দেয় |
| ডেড-লেটার ফাইলে পাঠানো | পরে ব্যাকফিল দরকার এমন ক্ষেত্রে | অতিরিক্ত স্টোরেজ ও রিপ্লে প্রসেস |
যাই বাছুন, লাইন নম্বর ও মূল কন্টেন্টের একটি অংশ রেকর্ড করুন, নাহলে পরে সমস্যা খুঁজে পাবেন না। JSON.parse-এর এরর মেসেজে লাইন নম্বর থাকে না, তাই নিজেই তা রাখতে হবে।
৬. কম্প্রেশন ও শার্ডিং
- gzip
.jsonl-কে ধারাবাহিকভাবে উচ্চ অনুপাতে কম্প্রেস করে (ফিল্ড-নাম পুনরাবৃত্তি হয়), তাই.jsonl.gzসুপারিশ করা হয়; - gzip ব্লকে কম্প্রেস করে এবং স্ট্রিমিং ডিকম্প্রেশন নেটিভভাবে সমর্থন করে, তাই পুরো ফাইল ডিকম্প্রেস করার দরকার নেই;
- বড় ফাইল প্রায়
1GBটুকরোয় ভাগ করুন, নামে ক্রম-নম্বর রাখুন, যাতে parallel প্রসেসিং ও রিজিউমযোগ্য ট্রান্সফার সহজ হয়; .jsonl-কে "অ্যারেতে মিলিয়ে" প্রিপ্রসেস করবেন না — তাতে এর সব সুবিধা নষ্ট হয়।
৭. অন্য বিগ-ডেটা ফরম্যাটের সঙ্গে সমঝোতা
| ফরম্যাট | কাঠামো | স্কিমা অন্তর্ভুক্ত | উপযুক্ত |
|---|---|---|---|
| JSON Lines | প্রতি লাইনে স্বাধীন | না (বাইরের চুক্তি দরকার) | লগ, ইমপোর্ট-এক্সপোর্ট, ট্রেনিং ডেটা |
| CSV | ট্যাবুলার | না | রিপোর্ট, Excel বিনিময় |
| Parquet | কলামার | হ্যাঁ | বিশ্লেষণী কুয়েরি, অত্যন্ত বড় অফলাইন ডেটাসেট |
| Avro | সারি-ভিত্তিক | হ্যাঁ | Kafka মেসেজ, স্কিমা বিবর্তন |
JSON Lines-এর সুবিধা হলো যেকোনো ভাষা ও যেকোনো টুল সঙ্গে সঙ্গে পড়তে ও লিখতে পারে; অসুবিধা হলো স্কিমা ও কলামার কম্প্রেশনের অভাব। ডেটা এক মেশিনে না ধরে গিয়ে ঘন ঘন কলাম-কেন্দ্রিক কুয়েরি দরকার হলে Parquet ভাবুন।
৮. চেকলিস্ট
- [ ] ফাইল এক্সটেনশন সামঞ্জস্যপূর্ণ (
.jsonlবা.ndjson) ও কখনো মিশ্রিত নয়; - [ ] লেখক "প্রতি লাইনে একটি রেকর্ড" নিশ্চিত করে, স্ট্রিংয়ের ভেতরের নিউলাইন আগেই এস্কেপ করা;
- [ ] পাঠক BOM সরায়, খালি লাইন এড়িয়ে যায় এবং লাইন নম্বর রাখে;
- [ ] খারাপ-লাইন কৌশল স্পষ্টভাবে নির্ধারিত ও মনিটর করা (খারাপ লাইন / মোট লাইন);
- [ ] বড় ফাইল আকার অনুযায়ী টুকরো করা এবং gzip ও parallel পাঠের সঙ্গে জোড়া;
- [ ] ফিল্ড চুক্তি থাকলে অতিরিক্ত স্কিমা দেওয়া এবং লাইন ধরে যাচাই করা।
সাধারণ প্রশ্ন
JSON অ্যারে নাকি JSON Lines — কীভাবে বাছাই করব?
ডেটা এক ব্যাচ রেকর্ড নাকি একক ডকুমেন্ট — সেটা দেখুন। রেকর্ড অ্যাপেন্ড ও স্ট্রিম হিসেবে পড়া হলে JSON Lines নিন: অ্যাপেন্ড মানে শেষে একটি লাইন লেখা, আর পড়তে কেবল লাইন ধরে ভাগ করতে হয়। পুরো কাঠামো একবারে যাচাই করা দরকার হলে JSON অ্যারে রাখুন, কারণ JSON অ্যারে একবারে সব যাচাই করে, অথচ JSON Lines কেবল লাইন ধরে যাচাই করা যায়।
একটি নষ্ট লাইন পুরো ফাইল ভাঙে না কেন?
JSON Lines-এ প্রতিটি লাইন স্বাধীন, সম্পূর্ণ JSON ভ্যালু, আর পার্সিং স্বাভাবিকভাবেই লাইন-ধরে-লাইন, তাই নষ্ট ডেটা কেবল সেই লাইনকেই প্রভাবিত করে। JSON অ্যারেতে একটি সিনট্যাক্স এরর পুরো ফাইল পার্স-অযোগ্য করে — এজন্যই লাইনভিত্তিক ডেটা লগ ও অ্যানালিটিক্স ইভেন্টের জন্য উপযুক্ত।
JSON Lines-কে JSON অ্যারেতে রূপান্তর করা উচিত?
পারেন: লাইন ধরে পড়ুন, প্রতিটি রেকর্ডে JSON.parse করুন এবং অ্যারেতে জমা করুন। এতে পুরো কন্টেন্ট মেমরিতে ফিট হতে হবে আর স্ট্রিমিং পাঠ ও এক-লাইন অ্যাপেন্ড হারায়। .jsonl-কে অ্যারেতে প্রিপ্রসেস করলে ফরম্যাটের সব সুবিধা নষ্ট হয়, তাই এটি কেবল ডেটা মেমরিতে ফিট এমন এককালীন ডেলিভারির জন্য যুক্তিসঙ্গত।