JSON Lines และ NDJSON: ข้อมูลแบบ Streaming
สรุปสำคัญ: หนึ่งค่า JSON ต่อบรรทัดแลกมาด้วยความสามารถในการต่อท้าย การ streaming และความเสียหายที่จำกัดอยู่เพียงบรรทัดเดียว ต้นทุนคือการไม่มีโครงสร้างโดยรวม การตรวจสอบจึงต้องทำทีละบรรทัด

สรุปสั้น: ใช้ JSON array เมื่อประมวลผลทั้งชุดในครั้งเดียว และใช้ JSON Lines เมื่อต้องต่อท้ายแบบ streaming และแยกความเสียหายเฉพาะบรรทัด; หากยังต้องตรวจโครงสร้างโดยรวม อย่าเปลี่ยนไปใช้รูปแบบที่คั่นด้วยบรรทัด
เมื่อข้อมูลเกินหน่วยความจำ หรือไหลเข้ามาอย่างต่อเนื่อง โครงสร้าง "array ใหญ่ก้อนเดียว" ก็กลายเป็นศัตรู
1. กฎของรูปแบบ
JSON Lines (.jsonl) และ NDJSON (.ndjson) ในทางปฏิบัติเป็นคำพ้องกัน และกฎสั้นทั้งสองกรณี:
- การเข้ารหัสเป็น UTF-8 โดยไม่มี BOM;
- แต่ละบรรทัดมีค่า JSON ที่สมบูรณ์และถูกต้องหนึ่งค่า;
- บรรทัดคั่นด้วย
\n; บรรทัดสุดท้ายอาจมีหรือไม่มีอักขระขึ้นบรรทัดใหม่ก็ได้; - ต้องไม่มีอักขระขึ้นบรรทัดใหม่ที่ไม่ได้ escape ภายในบรรทัด — อักขระขึ้นบรรทัดใหม่ใน string ต้องเขียนเป็น
\n; - บรรทัดว่างมักถูกเพิกเฉย แต่ implementation ที่เข้มงวดควรถือว่าไม่ถูกต้อง
{"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 อะไรก็ได้ (รวมถึง scalar)" ขณะที่สเปก NDJSON เพิ่มกฎการนำไปใช้ที่เข้มงวดกว่า เช่น "ไม่มีบรรทัดว่างและไม่มี scalar ระดับบนสุด" ในงานวิศวกรรม การทำตามชุดที่เข้มงวดกว่าจะช่วยให้พ้นปัญหา
2. เปรียบเทียบกับ JSON array
| มิติ | JSON array | JSON Lines |
|---|---|---|
| ต่อท้ายหนึ่งเรกคอร์ด | เขียนไฟล์ใหม่ทั้งไฟล์ (เปลี่ยน ] ปิด) |
เพิ่มเพียงหนึ่งบรรทัด |
| อ่านแบบ streaming | ต้องใช้ streaming parser ที่ติดตามความลึกของวงเล็บ | แยกตามบรรทัด |
| รายการเดียวเสียหาย | ทั้งไฟล์ parse ไม่ได้ | กระทบเฉพาะบรรทัดนั้น |
| ตรวจทั้งไฟล์ | ตรวจทั้งหมดในรอบเดียว | ได้เพียงทีละบรรทัด |
| การบีบอัด | ขนาดใกล้เคียงกันหลัง gzip | เท่ากัน และบีบอัดบล็อกแบบขนานได้ |
| การแสดงการซ้อน | รองรับโดยธรรมชาติ | แต่ละบรรทัดซ้อนภายในได้; บรรทัดไม่เกี่ยวข้องกัน |
| ผู้บริโภคทั่วไป | เครื่องมือ JSON ทั้งหมด | ระบบ log, pipeline big data, ชุดฝึก LLM |
ข้อสรุปตรงไปตรงมา: ข้อมูลเป็น "ชุดของเรกคอร์ด" หรือ "เอกสารเดียว" เป็นตัวตัดสินว่าจะใช้รูปแบบไหน
3. การอ่าน: streaming ทีละบรรทัด
ใน 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 สร้าง object ให้ทั้ง 50
streaming parser (stream-json, clarinet) ดึงเฉพาะฟิลด์เป้าหมายได้ โดยมีต้นทุนเป็นความซับซ้อนในการนำไปใช้ที่สูงขึ้น
กฎง่าย ๆ: เมื่อเรกคอร์ดเดียวมีขนาดต่ำกว่าสองสามร้อย KB การเรียก JSON.parse โดยตรงมักยังคุ้มกว่า
3.2 ข้อความภาษาจีนและ BOM
ไฟล์ .jsonl ที่สร้างบน Windows มักมี BOM และ JSON.parse บนบรรทัดแรกจะล้มเหลวแน่นอน ให้ตัด BOM ออกก่อนอ่าน หรือตรวจสามไบต์แรกขณะ stream chunk แรก
4. การเขียน: การต่อท้ายและ atomicity
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ใน string ถูก escape เป็นสองอักขระ) หนึ่งบรรทัดจึงเป็นหนึ่งเรกคอร์ดเสมอ; - เมื่อหลายโปรเซสต่อท้ายพร้อมกัน
writeเดียวที่เล็กกว่า PIPE_BUF (ปกติ 4096 ไบต์บน Linux) เป็น atomic; ขนาดที่ใหญ่กว่านั้นไม่รับประกัน สำหรับสถานการณ์ที่มี concurrency สูง ให้เปลี่ยนเป็น "หนึ่งไฟล์ต่อโปรเซส รวมภายหลังเมื่อ flush ลงดิสก์" หรือใช้ที่เก็บข้อมูลที่มี transaction
5. การจัดการบรรทัดเสีย
นี่คือการตัดสินใจที่รูปแบบคั่นด้วยบรรทัดบังคับให้คุณทำล่วงหน้า:
| กลยุทธ์ | เหมาะกับ | ต้นทุน |
|---|---|---|
| ข้ามและนับ | ข้อมูลที่ทนต่อช่องว่างได้ เช่น log และเหตุการณ์ analytics | ข้อมูลหายอย่างเงียบ ๆ จึงต้องมี alert |
| ล้มเหลวทันที | ข้อมูลที่ห้ามหาย เช่น การเรียกเก็บเงินและใบแจ้งหนี้ | บรรทัดเสียหนึ่งบรรทัดบล็อกทั้งชุด |
| ส่งไปยังไฟล์ dead-letter | กรณีที่ต้องเติมย้อนหลัง | พื้นที่เก็บเพิ่มและกระบวนการ replay |
ไม่ว่าเลือกแบบใด ให้บันทึกเลขบรรทัดและส่วนหนึ่งของเนื้อหาต้นฉบับ ไม่อย่างนั้นคุณระบุปัญหาในภายหลังไม่ได้ เลขบรรทัดไม่มีในข้อความ error ของ JSON.parse คุณจึงต้องดูแลมันเอง
6. การบีบอัดและการแบ่งส่วน
- gzip บีบอัด
.jsonlในอัตราสูงสม่ำเสมอ (ชื่อฟิลด์ซ้ำ) จึงแนะนำ.jsonl.gz; - gzip บีบอัดเป็นบล็อกและรองรับการแตกแบบ streaming โดยธรรมชาติ ไม่ต้องแตกไฟล์ทั้งหมด;
- แบ่งไฟล์ใหญ่เป็นชิ้นขนาดประมาณ
1GBพร้อมเลขลำดับในชื่อ เพื่อให้ประมวลผลแบบขนานและโอนต่อได้ง่ายขึ้น; - อย่าประมวลผลล่วงหน้า
.jsonlด้วยการ "รวมเป็น array" — นั่นทิ้งข้อดีทั้งหมดของมัน
7. ข้อแลกเปลี่ยนกับรูปแบบ big data อื่น
| รูปแบบ | โครงสร้าง | มี schema | เหมาะกับ |
|---|---|---|---|
| JSON Lines | อิสระต่อบรรทัด | ไม่มี (ต้องมีสัญญาภายนอก) | log, นำเข้าและส่งออก, ข้อมูลฝึก |
| CSV | แบบตาราง | ไม่มี | รายงาน, แลกเปลี่ยนกับ Excel |
| Parquet | แบบคอลัมน์ | มี | query เชิงวิเคราะห์, ชุดข้อมูลออฟไลน์ขนาดใหญ่มาก |
| Avro | แบบแถว | มี | ข้อความ Kafka, การวิวัฒน์ schema |
ข้อดีของ JSON Lines คือ ภาษาใดและเครื่องมือใดก็อ่านและเขียนได้ทันที; ข้อเสียคือขาด schema และขาดการบีบอัดแบบคอลัมน์ เมื่อข้อมูลไม่พอดีกับเครื่องเดียวและต้อง query เชิงคอลัมน์บ่อย ๆ ก็ถึงเวลาพิจารณา Parquet
8. รายการตรวจสอบ
- [ ] นามสกุลไฟล์สม่ำเสมอ (
.jsonlหรือ.ndjson) และไม่ผสมกัน; - [ ] ตัวเขียนรับประกัน "หนึ่งเรกคอร์ดต่อบรรทัด" โดยอักขระขึ้นบรรทัดใหม่ใน string ถูก escape แล้ว;
- [ ] ตัวอ่านตัด BOM, ข้ามบรรทัดว่าง และดูแลเลขบรรทัด;
- [ ] กลยุทธ์บรรทัดเสียถูกกำหนดและติดตามอย่างชัดเจน (บรรทัดเสีย / บรรทัดทั้งหมด);
- [ ] ไฟล์ใหญ่ถูกแบ่งตามขนาดและใช้ร่วมกับ gzip และการอ่านแบบขนาน;
- [ ] ที่มีสัญญาฟิลด์ มี schema เพิ่มเติมและตรวจทีละบรรทัด
คำถามที่พบบ่อย
จะเลือกระหว่าง JSON array กับ JSON Lines อย่างไร?
ดูว่าข้อมูลเป็นชุดของเรกคอร์ดหรือเอกสารเดียว เลือก JSON Lines เมื่อเรกคอร์ดถูกต่อท้ายและอ่านแบบ stream: การต่อท้ายคือเขียนหนึ่งบรรทัดท้ายไฟล์ และการอ่านเพียงแยกตามบรรทัด เก็บ JSON array ไว้เมื่อต้องตรวจโครงสร้างทั้งหมดในรอบเดียว เพราะ JSON array ตรวจทั้งหมดพร้อมกันได้ ขณะที่ JSON Lines ตรวจได้เพียงทีละบรรทัด
ทำไมบรรทัดเสียหนึ่งบรรทัดจึงไม่ทำให้ทั้งไฟล์พัง?
แต่ละบรรทัดใน JSON Lines เป็นค่า JSON ที่สมบูรณ์และเป็นอิสระ และการ parse เป็นทีละบรรทัดโดยธรรมชาติ ข้อมูลเสียจึงกระทบเฉพาะบรรทัดนั้น ใน JSON array syntax error หนึ่งจุดทำให้ทั้งไฟล์ parse ไม่ได้ ซึ่งเป็นเหตุผลตรง ๆ ที่ข้อมูลคั่นด้วยบรรทัดเหมาะกับ log และเหตุการณ์ analytics
ควรแปลง JSON Lines เป็น JSON array หรือไม่?
ทำได้: อ่านทีละบรรทัด เรียก JSON.parse กับแต่ละเรกคอร์ด แล้วรวบรวมเป็น array การทำเช่นนั้นต้องให้เนื้อหาทั้งหมดพอดีกับหน่วยความจำ และเสียการอ่านแบบ streaming กับการต่อท้ายหนึ่งบรรทัดไป การประมวลผลล่วงหน้า .jsonl เป็น array ทิ้งข้อดีทั้งหมดของรูปแบบ จึงสมเหตุสมผลเฉพาะการส่งมอบครั้งเดียวที่ข้อมูลพอดีกับหน่วยความจำ