JSON Toolkit

Thực hành với tệp JSONL: mở, chuyển đổi và xử lý dòng lỗi

Kết luận: Ném cả tệp .jsonl vào JSON.parse rồi nhận lỗi gần như chưa bao giờ có nghĩa là tệp hỏng — mà nghĩa là cách đọc sai. Bài viết này bao gồm các cách chuẩn để kiểm tra và đọc JSONL từng dòng, các lệnh chuyển đổi hai chiều với mảng JSON, bảng sáu loại dòng lỗi và cách đúng để tiêu thụ NDJSON từ phản hồi HTTP dạng stream.

Trung cấpĐọc khoảng 9 phútXuất bản 2026-11-06Cập nhật 2026-11-06
Hướng dẫn thực hành đọc tệp JSONL theo từng dòng và cô lập dòng lỗi

Tóm lại: mỗi dòng của tệp JSONL là một giá trị JSON hợp lệ và độc lập, nhưng cả tệp không phải là một tài liệu JSON duy nhất. Nếu đưa cả tệp vào JSON.parse hay json.load mà thất bại, chín trên mười lần tệp vẫn tốt — chiến lược đọc của bạn mới sai.

Một bản ghi mỗi dòng, không dấu phẩy giữa các dòng, không mảng bao ngoài — JSONL từ bỏ "cấu trúc toàn tài liệu" để đổi lấy ghi nối tiếp, đọc dạng stream và cô lập lỗi theo từng dòng. Cả ba lợi ích chỉ hiện thực khi bạn xử lý tệp theo từng dòng, và cả ba đều biến thành lỗi ngay khi bạn cố parse cả tệp một lần.

1. Ba cái tên, một định dạng: JSONL, NDJSON, JSON Lines

Người mới thường tưởng sự khác biệt tên gọi kéo theo sự khác biệt cú pháp. Không phải:

Tên Bối cảnh thường gặp Phần mở rộng / media type
JSON Lines Kỹ thuật dữ liệu, dataset ML (pandas, Spark, Hugging Face) .jsonl
NDJSON API HTTP dạng stream, dữ liệu thời gian thực .ndjson, application/x-ndjson
JSONL Cách gọi tắt thông dụng cho một trong hai —

Công cụ đọc được cái này thì đọc được cái kia. Tiêu chí duy nhất khi chọn tên là quy ước hệ sinh thái hạ nguồn của bạn: viết .jsonl khi chuẩn bị dataset cho Hugging Face, đặt application/x-ndjson khi cung cấp API stream. Một tệp JSONL chuẩn chuyển đổi phải thỏa ba quy tắc: mã hóa UTF-8 (không BOM), một giá trị JSON hoàn chỉnh mỗi dòng, các dòng ngăn bởi \n. Xuống dòng cuối tệp được khuyến nghị mạnh mẽ, còn dòng trống không phải là bản ghi hợp lệ. Chi tiết giao thức và lý do thiết kế đã có bài tổng quan JSON Lines và NDJSON của chúng tôi; bài này chỉ bận tâm đến việc cần làm khi tệp đã nằm trong tay.

2. Kiểm tra: head trước, grep sau, trình soạn thảo cuối cùng

Nhấp đúp vào tệp .jsonl 500 MB để mở trong trình soạn thảo là sai lầm phổ biến nhất: ba triệu dòng văn bản cộng tô màu cú pháp cộng bộ nhớ hoàn tác sẽ treo hầu hết mọi trình soạn thảo — và JSON không hề có lỗi. Phân tích một tài liệu 45 MB tốn khoảng 208 ms trong Node 22; treo là ở khâu hiển thị. Định dạng hướng dòng sinh ra cho các công cụ dòng, vì bản ghi trùng khớp với dòng vật lý:

# Trước tiên đo quy mô: số bản ghi và dung lượng tệp, không cần đọc nội dung
wc -l events.jsonl && du -h events.jsonl

# Xem một bản ghi trông thế nào (xác nhận trường và cấu trúc)
head -n 1 events.jsonl | jq .

# Tìm bản ghi cần thiết ngay lập tức: công cụ dòng cứ thế mà chạy — đặc quyền của JSONL
grep '"level":"error"' events.jsonl | head -n 5
tail -n 100 events.jsonl | jq 'select(.latency_ms > 500)'

Để ý lệnh cuối: mỗi dòng ra khỏi tail vẫn là một giá trị JSON độc lập, nên đi thẳng vào jq mà không cần dựng lại mảng. Cách này hoạt động y hệt trên tệp nhiều gigabyte với chi phí bộ nhớ gần bằng không — cùng nguyên lý với vì sao tệp JSON lớn làm trang chậm.

3. Ba mẫu chuẩn để đọc theo từng dòng

Đọc theo dòng phải xử lý ba việc: bỏ qua dòng trống (đặc biệt là ký tự xuống dòng cuối tệp), báo lỗi kèm số dòng, và không để một dòng hỏng làm chết cả công việc. Các mẫu chuẩn của ba ngôn ngữ đều xoay quanh ba điểm này.

Python, chỉ dùng thư viện chuẩn:

import json

with open("events.jsonl", encoding="utf-8") as f:
    for line_number, line in enumerate(f, start=1):
        if not line.strip():          # bỏ qua dòng trống
            continue
        try:
            record = json.loads(line) # phân tích từng dòng độc lập
        except json.JSONDecodeError as e:
            print(f"Dòng {line_number} thất bại: {e}")  # báo cáo, không dừng
            continue                  # cô lập dòng hỏng
        handle(record)

Node với module readline có sẵn, bộ nhớ cố định bất kể kích thước tệp:

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(`Dòng ${lineNumber}: ${e.message}`);
  }
}

Ở Go 1.27, encoding/json/v2 trở thành chuẩn mặc định, bộ giải mã stream theo giá trị khớp hoàn hảo với JSONL; cần nhúng biểu thức jq thì dùng gojq. Về thư viện, đừng vội cài thêm gói: jsonlines của Python báo số dòng và văn bản thô của dòng lỗi, còn orjsonl đọc ghi thẳng tệp nén gzip/bzip2/zstd; gói Node ndjson không phát hành bản nào từ 2020 — readline có sẵn là đủ.

4. Chuyển đổi hai chiều với mảng JSON

Chiều chuyển rất quan trọng; đảo ngược là mọi lệnh đều sai. Ghi nhớ một quy tắc: mảng → JSONL là map (mỗi bản ghi nén thành một dòng), JSONL → mảng là collect (các dòng gom vào một danh sách).

# Mảng JSON → JSONL: -c nén mỗi phần tử thành một dòng
jq -c '.[]' records.json > records.jsonl

# JSONL → mảng JSON: -s gom toàn bộ đầu vào thành một mảng
jq -s '.' events.jsonl > records.json

# Lọc JSONL theo dòng: không cần -s, jq xử lý đầu vào như stream
jq -c 'select(.active == true)' events.jsonl > active.jsonl

Các dòng lệnh Python tương đương: mảng → JSONL ghi json.dumps(r, ensure_ascii=False) + "\n" cho mỗi bản ghi; chiều ngược lại gom kết quả json.loads theo dòng vào danh sách. Mỗi chiều có một cái bẫy. Chiều đi (mảng → JSONL), mọi ký tự xuống dòng thật bên trong bản ghi phải được json.dumps escape — nếu không một bản ghi sẽ bị xé thành nhiều dòng. Chiều về (JSONL → mảng), đừng nối các dòng thành một chuỗi khổng lồ rồi parse — với hàng triệu dòng đó là một bản sao toàn phần không cần thiết trong bộ nhớ. Tổng hợp (tổng, nhóm, trung bình) cần -s vốn nạp tất cả; trên tệp vài GB hãy chuyển sang awk hoặc bộ tích lũy stream trong Python.

5. Sáu loại dòng lỗi và cách chẩn đoán

Hầu như mọi lỗi JSONL đều nằm ở cấp dòng. Kiểm tra từng dòng và báo số dòng là chẩn đoán duy nhất đáng tin. Xếp theo tần suất:

Triệu chứng Nguyên nhân gốc Cách sửa
Extra data cuối dòng Thêm dấu phẩy giữa các dòng Xóa đi; mỗi dòng phải tự đứng vững
Một bản ghi bị xé thành nhiều dòng Ký tự xuống dòng thật chưa escape trong bản ghi (format bằng tay) Nén lại bằng json.dumps / jq -c
Dòng đầu tiên lỗi Unexpected token ï Tệp có BOM Lưu lại thành UTF-8 không BOM
Chỉ dòng cuối lỗi nhưng nhìn có vẻ đúng Thiếu xuống dòng cuối hoặc thừa khoảng trắng Thêm \n; theo quy ước bỏ qua dòng trống
Lỗi xuống dòng bên trong chuỗi Xuống dòng thật trong chuỗi, không phải \n Sửa phần sinh dữ liệu; đừng bao giờ nối chuỗi JSON bằng tay
Số nguyên lớn đổi giá trị Parse qua double (cắt tại 2^53) Dùng parser giữ nguyên độ chính xác, xem độ chính xác bigint

Quy trình chẩn đoán luôn là ba bước: head -n 1 để xác nhận mã hóa và cấu trúc dòng đầu; chạy một script theo dòng báo số mọi dòng lỗi thay vì dừng ở dòng đầu tiên; rồi tách từng dòng lỗi ra xem riêng — một dòng là một bản ghi hoàn chỉnh, soi nó riêng lẻ không tốn gì. Với import hàng loạt, chính thức khuyến nghị chính sách "cách ly chứ không dừng": ghi dòng lỗi kèm số dòng vào errors.jsonl, tiếp tục nạp các bản ghi tốt, rồi quyết định nạp lại dựa trên tỉ lệ dòng lỗi.

6. JSONL qua HTTP: tiêu thụ theo dòng và backpressure

Kiểu hình điển hình của NDJSON qua HTTP: server viết liên tục, client tiêu thụ liên tục. Ở client đừng gọi response.json() — nó đệm toàn bộ body trước khi phân tích, triệt tiêu ý nghĩa của stream. Đọc stream body trực tiếp, tách theo \n và mang phần đuôi chưa hoàn chỉnh sang vòng lặp sau:

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();               // đoạn cuối có thể là dòng chưa trọn
  for (const line of lines) {
    if (line.trim()) handle(JSON.parse(line));
  }
}

Ở server hai việc bắt buộc: thứ nhất, khi res.write() trả về false thì chờ sự kiện drain (backpressure), nếu không bộ nhớ sẽ tăng vô hạn mỗi khi bên tiêu thụ chậm hơn bên sản xuất; thứ hai, tắt middleware gzip trên các route stream — gzip phải đệm cả phản hồi trước khi nén được gì, điều này vô hiệu hoàn toàn stream của NDJSON. Cả hai hành vi đều được xác minh bằng kiểm thử cục bộ và nhiều nguồn độc lập tiếng Anh, không phải phỏng đoán.

7. Khi nào JSONL là lựa chọn sai

Khoảng âm của việc chọn định dạng cũng quan trọng. Ba tình huống nên giữ nguyên mảng JSON:

Yêu cầu Vì sao JSONL không vừa Dùng thay thế
Kiểm tra cấu trúc của cả tài liệu Không có cấu trúc tổng thể; lệch trường lọt qua mắt Mảng JSON + Schema
Truy cập ngẫu nhiên bản ghi thứ N Độ dài dòng thay đổi; không tìm kiếm nhị phân được SQLite / Parquet
Lồng nhau sâu, tham chiếu giữa các bản ghi Mỗi dòng tự cô lập; tham chiếu không có chỗ ở Tài liệu JSON chuẩn

Quy tắc một câu: bản ghi đến theo thời gian hoặc được tiêu thụ theo dòng (log, sự kiện, xuất dữ liệu, tập huấn luyện) thì dùng JSONL; dữ liệu nhỏ tạo một lần, dùng một lần và cần ngữ nghĩa toàn tài liệu thì dùng mảng JSON. Các biến thể phái sinh (JSON Text Sequences với dấu ngăn RS) là chủ đề khác — xem bài về RFC 7464.

FAQ

JSONL và NDJSON có khác nhau thật không?

Về mặt kỹ thuật là không. Cả hai đều nghĩa là "một giá trị JSON hoàn chỉnh mỗi dòng, không lớp bọc ngoài, không dấu phẩy giữa các dòng", và công cụ nào hỗ trợ một sẽ hỗ trợ cả hai. Khác biệt là tên gọi và thói quen hệ sinh thái: .jsonl thống trị kỹ thuật dữ liệu và dataset ML; .ndjson và application/x-ndjson thống trị các API HTTP stream. Khi tích hợp với hệ thống bên ngoài, hãy theo phần mở rộng và media type trong tài liệu của nó.

Đọc tệp .jsonl trực tiếp bằng JSON.parse được không?

Cả tệp thì không. JSON.parse nhận đúng một văn bản JSON, còn JSONL là hàng loạt giá trị độc lập xếp theo dòng, nên parse cả tệp luôn thất bại với "Unexpected token" hoặc "Extra data". Tách thành từng dòng và parse riêng từng dòng không trống, thất bại thì báo số dòng.

Một dòng trong tệp JSONL hỏng thì cả tệp có mất không?

Không — cô lập lỗi theo dòng là cốt lõi thiết kế của định dạng này. Dòng hỏng chỉ ảnh hưởng chính nó: bỏ qua, ghi lại số dòng rồi đọc tiếp. Với import hàng loạt, cách ly dòng lỗi vào một tệp riêng, nạp các bản ghi tốt như bình thường, rồi quyết định sửa và nạp lại dựa trên tỉ lệ dòng lỗi.

Chuyển JSONL sang Excel hay CSV thế nào?

Trước hết biến nó thành mảng hoặc trích trường theo từng dòng, rồi chạy chuyển đổi bảng: nén bằng jq -c '.[]' data.jsonl đưa cho bộ chuyển đổi, hoặc đẩy từng dòng qua json.loads vào pandas. Lưu ý: trường lồng nhau khi làm phẳng tạo tên cột có dấu chấm, trường dạng mảng chỉ có thể được lưu trong ô dưới dạng văn bản, còn cột ID dài nên khai báo là chuỗi ngay từ đầu để giữ độ chính xác.

Tài liệu tham khảo

Công cụ phù hợp

Bài viết liên quan

JSON Lines và NDJSON: dữ liệu dạng luồng

Mỗi dòng một giá trị JSON mang lại khả năng ghi nối thêm, truyền luồng và cô lập hư hỏng trong một dòng; cái giá là thiếu cấu trúc tổng thể, nên việc kiểm tra phải diễn ra theo từng dòng.

Trung cấpĐọc khoảng 10 phútCập nhật 2026-10-30

JSON Text Sequences (RFC 7464): JSON dạng luồng phân tách bằng RS

RFC 7464 dùng ký tự phân tách 0x1E để truyền luồng các văn bản JSON mà bạn có thể đọc và ghi cùng lúc. Bài viết trình bày quy tắc, khác biệt so với NDJSON, và cách hiện thực tách-rồi-phân-tích.

Trung cấpĐọc khoảng 9 phútCập nhật 2026-10-31

Phân tích, kết xuất và truyền luồng JSON lớn

Phân tích thường không phải nút cổ chai; kết xuất và sao chép mới là. Hãy tối ưu theo bốn lớp "phân tích → duyệt → kết xuất → truyền tải" để công sức của bạn không bị lãng phí.

Trung cấpĐọc khoảng 13 phútCập nhật 2026-10-31