ใช้งานไฟล์ JSONL จริง: เปิด แปลง และแก้บรรทัดเสีย
สรุปสำคัญ: โยนไฟล์ .jsonl ทั้งไฟล์ให้ JSON.parse แล้วพบ error แทบไม่เคยหมายความว่าไฟล์เสีย — แต่หมายถึงวิธีอ่านผิด บทความนี้ครอบคลุมวิธีมาตรฐานในการตรวจสอบและอ่าน JSONL ทีละบรรทัด คำสั่งแปลงสองทางกับอาร์เรย์ JSON ตารางบรรทัดเสียหกประเภท และวิธีที่ถูกต้องในการบริโภค NDJSON จาก HTTP response แบบสตรีม

ใจความสำคัญ: แต่ละบรรทัดของไฟล์ JSONL เป็นค่า JSON ที่ถูกต้องและเป็นอิสระจากกัน แต่ทั้งไฟล์ไม่ใช่เอกสาร JSON ชิ้นเดียว ถ้ายัดทั้งไฟล์ให้ JSON.parse หรือ json.load แล้วล้มเหลว เก้าในสิบครั้งไฟล์ไม่ได้เสีย — วิธีอ่านของคุณต่างหากที่ผิด
หนึ่งเรกคอร์ดต่อบรรทัด ไม่มีคอมมาระหว่างบรรทัด ไม่มีอาร์เรย์ครอบนอก — JSONL ยอมสละ "โครงสร้างของทั้งเอกสาร" เพื่อแลกกับการเขียนต่อท้าย การอ่านแบบสตรีม และการกักขังความเสียหายไว้ที่บรรทัดเดียว ข้อดีทั้งสามจะเกิดจริงเฉพาะเมื่อประมวลผลไฟล์ทีละบรรทัด และทั้งสามกลายเป็น error ทันทีที่พยายาม parse ทั้งไฟล์ในครั้งเดียว
1. สามชื่อ หนึ่งรูปแบบ: JSONL, NDJSON, JSON Lines
มือใหม่มักคิดว่าชื่อต่างกันแปลว่าไวยากรณ์ต่างกัน ไม่ใช่:
| ชื่อ | บริบทที่ใช้บ่อย | นามสกุล / media type |
|---|---|---|
| JSON Lines | วิศวกรรมข้อมูล ชุดข้อมูล ML (pandas, Spark, Hugging Face) | .jsonl |
| NDJSON | HTTP API แบบสตรีม ข้อมูลเรียลไทม์ | .ndjson, application/x-ndjson |
| JSONL | ชื่อเรียกกันทั่วไปสำหรับทั้งสอง | — |
เครื่องมือที่อ่านได้อันหนึ่งย่อมอ่านได้อีกอันหนึ่ง เกณฑ์เดียวในการเลือกชื่อคือธรรมเนียมของระบบนิเวศปลายทางของคุณ: เขียน .jsonl เมื่อเตรียมชุดข้อมูลให้ Hugging Face ตั้ง application/x-ndjson เมื่อเปิด API แบบสตรีม ไฟล์ JSONL ที่พกพาได้ต้องผ่านสามกฎ: เข้ารหัส UTF-8 (ไม่มี BOM) หนึ่งค่า JSON สมบูรณ์ต่อบรรทัด และบรรทัดคั่นด้วย \n การมีบรรทัดว่างท้ายไฟล์ได้รับการแนะนำอย่างยิ่ง ส่วนบรรทัดว่างไม่ใช่เรกคอร์ดที่ถูกต้อง รายละเอียดโปรโตคอลและเหตุผลเชิงออกแบบมีใน บทความภาพรวม JSON Lines และ NDJSON ของเรา บทความนี้พูดถึงสิ่งที่ต้องทำเมื่อไฟล์อยู่ในมือแล้วเท่านั้น
2. การตรวจสอบ: head ก่อน แล้ว grep ส่วน editor ไว้ท้ายสุด
ดับเบิลคลิกไฟล์ .jsonl ขนาด 500 MB เพื่อเปิดใน editor คือความผิดพลาดที่พบบ่อยที่สุด: ข้อความสามล้านบรรทัดบวกการไฮไลต์ไวยากรณ์บวกบัฟเฟอร์ undo จะทำให้ editor แทบทุกตัวค้าง — และ JSON ไม่ได้ผิด การ parse เอกสาร 45 MB ใช้เวลาราว 208 ms ใน 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. สามรูปแบบมาตรฐานของการอ่านทีละบรรทัด
การอ่านทีละบรรทัดต้องจัดการสามเรื่อง: ข้ามบรรทัดว่าง (โดยเฉพาะบรรทัดสุดท้าย) รายงาน error พร้อมเลขบรรทัด และไม่ให้บรรทัดเสียเพียงบรรทัดเดียวทำให้งานทั้งหมดล้ม รูปแบบมาตรฐานของสามภาษาล้วนหมุนรอบสามประเด็นนี้
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) # parse แต่ละบรรทัดแยกกัน
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 ใน 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 รวมทั้ง input เป็นอาร์เรย์เดียว
jq -s '.' events.jsonl > records.json
# กรอง JSONL ทีละบรรทัด: ไม่ต้องใช้ -s, jq ประมวลผล input เป็นสตรีม
jq -c 'select(.active == true)' events.jsonl > active.jsonl
คำสั่งบรรทัดเดียวของ Python เทียบเท่ากัน: อาร์เรย์ → JSONL เขียน json.dumps(r, ensure_ascii=False) + "\n" ต่อเรกคอร์ด ทิศกลับรวมผล json.loads ทีละบรรทัดเป็นลิสต์ แต่ละทิศมีกับดักของตัวเองหนึ่งอัน ขาไป (อาร์เรย์ → JSONL) อักขระขึ้นบรรทัดจริงภายในเรกคอร์ดต้องถูก escape โดย json.dumps เสมอ — ไม่งั้นเรกคอร์ดหนึ่งตัวจะถูกฉีกเป็นหลายบรรทัด ขากลับ (JSONL → อาร์เรย์) อย่าเอาบรรทัดทั้งหมดมาต่อกันเป็นสตริงยักษ์แล้ว parse — กับหลายล้านบรรทัดนั่นคือการคัดลอกทั้งหมดเข้าหน่วยความจำโดยไม่จำเป็น การรวมค่า (ผลรวม จัดกลุ่ม ค่าเฉลี่ย) ต้องใช้ -s ซึ่งโหลดทุกอย่าง บนไฟล์หลาย GB ให้เปลี่ยนไปใช้ awk หรือตัวสะสมแบบสตรีมใน Python
5. บรรทัดเสียหกประเภทและวิธีวินิจฉัย
เกือบทุก error ของ JSONL เกิดในระดับบรรทัด การตรวจทีละบรรทัดพร้อมรายงานเลขบรรทัดคือการวินิจฉัยที่เชื่อถือได้เพียงหนทางเดียว เรียงตามความถี่:
| อาการ | สาเหตุราก | วิธีแก้ |
|---|---|---|
Extra data ท้ายบรรทัด |
ใส่คอมมาระหว่างบรรทัด | ลบออก แต่ละบรรทัดต้องยืนได้ด้วยตัวเอง |
| เรกคอร์ดหนึ่งถูกฉีกเป็นหลายบรรทัด | อักขระขึ้นบรรทัดจริงไม่ได้ escape ในเรกคอร์ด (จัดรูปแบบด้วยมือ) | บีบใหม่ด้วย json.dumps / jq -c |
บรรทัดแรกล้มด้วย Unexpected token ï |
ไฟล์มี BOM | บันทึกใหม่เป็น UTF-8 แบบไม่มี BOM |
| เฉพาะบรรทัดสุดท้าย error ทั้งที่ดูถูกต้อง | ขาดบรรทัดว่างท้ายไฟล์หรือมีช่องว่างแปลกปลอม | เติม \n ตามธรรมเนียมให้ข้ามบรรทัดว่าง |
| error จากอักขระขึ้นบรรทัดในสตริง | มีอักขระขึ้นบรรทัดจริงในสตริง ไม่ใช่ \n |
แก้ตัวสร้างข้อมูล ห้ามต่อสตริง JSON ด้วยมือ |
| เลขจำนวนเต็มใหญ่เปลี่ยนค่า | parse ผ่าน double (ตัดที่ 2^53) | ใช้ parser ที่รักษาความแม่นยำ ดู ความแม่นยำ bigint |
ขั้นตอนวินิจฉัยมีสามขั้นเสมอ: head -n 1 เพื่อยืนยันการเข้ารหัสและโครงสร้างบรรทัดแรก รันสคริปต์แบบทีละบรรทัดที่รายงานเลขทุกบรรทัดเสียแทนการหยุดที่บรรทัดแรก จากนั้นดึงบรรทัดเสียออกมาดูทีละบรรทัด — หนึ่งบรรทัดคือหนึ่งเรกคอร์ดสมบูรณ์ การส่องแยกจึงไม่มีต้นทุน สำหรับการนำเข้าเป็นชุด แนวนโยบายที่แนะนำคือ "กักกัน ไม่ใช่หยุดทั้งหมด": เขียนบรรทัดเสียพร้อมเลขบรรทัดลง errors.jsonl โหลดเรกคอร์ดที่ดีต่อไป แล้วตัดสินใจเรื่องนำเข้าซ้ำจากสัดส่วนบรรทัดเสีย
6. JSONL บน HTTP: การบริโภคทีละบรรทัดและ backpressure
รูปแบบคลาสสิกของ NDJSON บน HTTP คือเซิร์ฟเวอร์เขียนต่อเนื่อง ลูกข่ายบริโภคต่อเนื่อง ฝั่งลูกข่ายอย่าเรียก response.json() — มันบัฟเฟอร์ทั้ง body ก่อน parse ทำให้ความหมายของสตรีมหายไป อ่านสตรีมของ body โดยตรง ตัดที่ \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 (backpressure) ไม่งั้นหน่วยความจำจะโตไม่มีที่สิ้นสุดทุกครั้งที่ผู้บริโภคช้ากว่าผู้ผลิต สอง ปิด middleware gzip บนเส้นทางสตรีม — gzip ต้องบัฟเฟอร์ทั้ง response ก่อนบีบอัดอะไรได้ ซึ่งทำให้สตรีมของ 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 ครองพื้นที่ HTTP API แบบสตรีม เมื่อเชื่อมต่อกับระบบภายนอกให้ทำตามนามสกุลและ media type ที่เอกสารของระบบนั้นระบุ
อ่านไฟล์ .jsonl ด้วย JSON.parse ตรง ๆ ได้ไหม?
ทั้งไฟล์ไม่ได้ JSON.parse รับข้อความ JSON เพียงชิ้นเดียวเท่านั้น ขณะที่ JSONL คือค่าอิสระจำนวนมากเรียงกันเป็นบรรทัด การ parse ทั้งไฟล์จึงล้มเหลวเสมอด้วย "Unexpected token" หรือ "Extra data" ให้แยกเป็นบรรทัดแล้ว parse ทีละบรรทัดที่ไม่ว่าง พลาดเมื่อไรรายงานเลขบรรทัด
ถ้าบรรทัดหนึ่งในไฟล์ JSONL เสีย ทั้งไฟล์ใช้ไม่ได้หรือ?
ไม่ — การกักขังความเสียหายไว้ที่ระดับบรรทัดคือหัวใจของการออกแบบรูปแบบนี้ บรรทัดเสียกระทบแค่ตัวเอง: ข้ามไป จดเลขบรรทัดไว้ แล้วอ่านต่อ สำหรับการนำเข้าเป็นชุด ให้แยกบรรทัดเสียไปไว้ในไฟล์กักกัน โหลดเรกคอร์ดที่ดีตามปกติ แล้วตัดสินใจแก้ไขและนำเข้าซ้ำจากสัดส่วนบรรทัดเสีย
แปลง JSONL เป็น Excel หรือ CSV อย่างไร?
ก่อนอื่นเปลี่ยนเป็นอาร์เรย์หรือดึงฟิลด์ออกทีละบรรทัด แล้วรันการแปลงเป็นตาราง: บีบด้วย jq -c '.[]' data.jsonl แล้วส่งให้ตัวแปลง หรือส่งแต่ละบรรทัดผ่าน json.loads เข้า pandas ระวัง: ฟิลด์ซ้อนกันที่ถูกแผ่ออกจะได้ชื่อคอลัมน์แบบมีจุด ฟิลด์แบบอาร์เรย์จะเก็บในเซลล์ได้เพียงข้อความ และคอลัมน์ ID ยาวควรประกาศเป็นสตริงตั้งแต่ต้นเพื่อไม่ให้ความแม่นยำหายไป