JSON Toolkit

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

Kết luận: 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útXuất bản 2026-10-30Cập nhật 2026-10-30
Ảnh bìa cho JSON Lines và NDJSON dữ liệu dạng luồng

Kết luận: dùng mảng JSON khi bạn xử lý cả lô cùng lúc, và dùng JSON Lines khi bạn cần ghi nối thêm dạng luồng cùng khả năng cô lập dòng lỗi; nếu vẫn phải kiểm tra cấu trúc tổng thể, đừng chuyển sang định dạng theo dòng.

Khi dữ liệu vượt quá bộ nhớ, hoặc cứ đến liên tục, cấu trúc "một mảng lớn" trở thành kẻ thù.

1. Quy tắc định dạng

JSON Lines (.jsonl) và NDJSON (.ndjson) thực tế là đồng nghĩa, và quy tắc của cả hai đều ngắn:

  1. Mã hóa là UTF-8, không có BOM;
  2. Mỗi dòng chứa một giá trị JSON hoàn chỉnh và hợp lệ;
  3. Các dòng được phân tách bằng \n; dòng cuối có thể có hoặc không có ký tự xuống dòng;
  4. Không được có ký tự xuống dòng chưa escape bên trong một dòng — xuống dòng trong chuỗi phải viết là \n;
  5. Các dòng trống thường bị bỏ qua, nhưng cách hiện thực nghiêm ngặt nên coi chúng là không hợp lệ.
{"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"}

Khác biệt tinh tế giữa hai bên: trang web JSON Lines nhấn mạnh "mỗi dòng một giá trị, và giá trị có thể là JSON bất kỳ (kể cả giá trị vô hướng)", trong khi đặc tả NDJSON bổ sung quy tắc hiện thực chặt hơn như "không có dòng trống và không có giá trị vô hướng ở cấp cao nhất". Trong kỹ thuật, bám theo bộ quy tắc chặt hơn sẽ giúp bạn tránh rắc rối.

2. So sánh với mảng JSON

Khía cạnh Mảng JSON JSON Lines
Thêm một bản ghi Phải ghi lại toàn bộ tệp (đổi dấu ] đóng) Chỉ cần nối thêm một dòng
Đọc dạng luồng Cần trình phân tích dạng luồng theo dõi độ sâu ngoặc Tách theo dòng
Một mục bị hỏng Toàn bộ tệp không phân tích được Chỉ dòng đó bị ảnh hưởng
Kiểm tra toàn tệp Kiểm tra tất cả trong một lượt Chỉ kiểm tra từng dòng
Nén Kích thước tương đương sau gzip Tương tự, và các khối có thể nén song song
Biểu diễn cấu trúc lồng nhau Được hỗ trợ sẵn Mỗi dòng có thể lồng bên trong; các dòng không liên quan nhau
Bên tiêu thụ điển hình Mọi công cụ JSON Hệ thống nhật ký, pipeline dữ liệu lớn, tập huấn luyện LLM

Kết luận rất trực tiếp: dữ liệu là "một lô bản ghi" hay "một tài liệu" sẽ quyết định định dạng cần dùng.

3. Đọc: truyền luồng theo từng dòng

Trong Node, cách đơn giản nhất là 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 };
}

Mức sử dụng bộ nhớ của đoạn mã này chỉ phụ thuộc kích thước một dòng, không phụ thuộc tổng kích thước tệp — đó là giá trị lớn nhất của định dạng theo dòng.

3.1 Khi chỉ cần vài trường, đừng JSON.parse trước

Nếu mỗi bản ghi có 50 trường mà bạn chỉ cần 3, JSON.parse sẽ dựng đối tượng cho cả 50 trường. Một trình phân tích dạng luồng (stream-json, clarinet) có thể chỉ trích ra các trường cần, với cái giá là độ phức tạp hiện thực cao hơn. Quy tắc ngón tay cái: khi một bản ghi dưới vài trăm KB, gọi JSON.parse trực tiếp thường vẫn là lựa chọn tốt hơn.

3.2 Văn bản tiếng Trung và BOM

Các tệp .jsonl tạo trên Windows thường mang BOM, và JSON.parse trên dòng đầu chắc chắn sẽ lỗi. Hãy loại BOM trước khi đọc, hoặc kiểm tra ba byte đầu khi đang truyền luồng khối đầu tiên.

4. Ghi: nối thêm và tính nguyên tử

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' });
}

Hai điều cần lưu ý:

  1. Đầu ra của JSON.stringify không chứa ký tự xuống dòng (trừ khi \n trong một chuỗi bị escape thành hai ký tự), nên một dòng luôn đúng là một bản ghi;
  2. Khi nhiều tiến trình ghi nối thêm đồng thời, một lệnh write nhỏ hơn PIPE_BUF (thường là 4096 byte trên Linux) là nguyên tử; kích thước lớn hơn không được bảo đảm. Với tình huống đồng thời cao, hãy chuyển sang "mỗi tiến trình một tệp, gộp lại sau khi ghi xuống đĩa", hoặc dùng lưu trữ có giao dịch.

5. Xử lý dòng lỗi

Đây là quyết định mà định dạng theo dòng buộc bạn phải đưa ra trước:

Chiến lược Phù hợp với Cái giá
Bỏ qua và đếm Dữ liệu chịu được khoảng trống, như nhật ký và sự kiện phân tích Dữ liệu mất âm thầm, nên cảnh báo là bắt buộc
Thất bại ngay Dữ liệu không được phép mất, như thanh toán và hóa đơn Một dòng lỗi chặn cả lô
Chuyển sang tệp thư chết Trường hợp cần bổ sung sau Thêm lưu trữ và quy trình phát lại

Dù chọn cách nào, hãy ghi lại số dòng và một đoạn nội dung gốc, nếu không bạn không thể định vị vấn đề về sau. Số dòng không có trong thông báo lỗi của JSON.parse, nên bạn phải tự duy trì nó.

6. Nén và chia mảnh

  • gzip nén .jsonl với tỉ lệ cao ổn định (tên trường lặp lại), nên khuyến nghị dùng .jsonl.gz;
  • gzip nén theo khối và hỗ trợ sẵn việc giải nén dạng luồng, nên không cần giải nén toàn bộ tệp;
  • Chia tệp lớn thành các lát khoảng 1GB, có số thứ tự trong tên, để xử lý song song và truyền tiếp tục dễ hơn;
  • Đừng bao giờ tiền xử lý .jsonl bằng cách "gộp thành một mảng" — làm vậy sẽ vứt bỏ mọi lợi thế của nó.

7. Cân nhắc so với các định dạng dữ liệu lớn khác

Định dạng Cấu trúc Kèm lược đồ Phù hợp với
JSON Lines Độc lập theo từng dòng Không (cần hợp đồng bên ngoài) Nhật ký, nhập xuất, dữ liệu huấn luyện
CSV Dạng bảng Không Báo cáo, trao đổi với Excel
Parquet Dạng cột Có Truy vấn phân tích, tập dữ liệu ngoại tuyến rất lớn
Avro Dạng hàng Có Tin nhắn Kafka, tiến hóa lược đồ

Ưu điểm của JSON Lines là bất kỳ ngôn ngữ và công cụ nào cũng đọc ghi được ngay; nhược điểm là thiếu lược đồ và thiếu nén dạng cột. Khi dữ liệu không còn vừa một máy và bạn cần truy vấn theo cột thường xuyên, đã đến lúc cân nhắc Parquet.

8. Danh sách kiểm tra

  • [ ] Phần mở rộng tệp nhất quán (.jsonl hoặc .ndjson) và không bao giờ trộn lẫn;
  • [ ] Bên ghi bảo đảm "mỗi dòng một bản ghi", với ký tự xuống dòng trong chuỗi đã được escape;
  • [ ] Bên đọc loại BOM, bỏ qua dòng trống và duy trì số dòng;
  • [ ] Chiến lược xử lý dòng lỗi được định nghĩa rõ ràng và được giám sát (dòng lỗi / tổng số dòng);
  • [ ] Tệp lớn được chia lát theo kích thước và đi kèm gzip cùng đọc song song;
  • [ ] Nơi có hợp đồng trường, cung cấp thêm lược đồ và kiểm tra theo từng dòng.

FAQ

Tôi chọn mảng JSON hay JSON Lines?

Hãy xem dữ liệu là một lô bản ghi hay một tài liệu duy nhất. Chọn JSON Lines khi các bản ghi được ghi nối thêm và đọc dạng luồng: ghi nối thêm nghĩa là viết một dòng ở cuối, còn đọc chỉ cần tách theo dòng. Giữ mảng JSON khi toàn bộ cấu trúc phải được kiểm tra trong một lượt, vì mảng JSON kiểm tra tất cả cùng lúc trong khi JSON Lines chỉ kiểm tra được từng dòng.

Tại sao một dòng hỏng không làm hỏng toàn bộ tệp?

Mỗi dòng trong JSON Lines là một giá trị JSON hoàn chỉnh và độc lập, và việc phân tích vốn diễn ra theo từng dòng, nên dữ liệu hỏng chỉ ảnh hưởng dòng đó. Trong một mảng JSON, một lỗi cú pháp làm cả tệp không phân tích được, đó chính là lý do dữ liệu theo dòng phù hợp với nhật ký và sự kiện phân tích.

Tôi có nên chuyển JSON Lines thành mảng JSON không?

Có thể: đọc từng dòng, gọi JSON.parse cho mỗi bản ghi, và gom chúng vào một mảng. Làm vậy đòi hỏi toàn bộ nội dung phải vừa bộ nhớ và đánh mất việc đọc dạng luồng cùng khả năng nối thêm một dòng. Tiền xử lý .jsonl thành mảng sẽ vứt bỏ mọi lợi thế của định dạng, nên chỉ hợp lý cho việc giao hàng một lần khi dữ liệu vừa bộ nhớ.