JSON Toolkit

JSON Lines i NDJSON: dane strumieniowe

Podsumowanie: Jedna wartość JSON w wierszu kupuje możliwość dopisywania, strumieniowanie i izolację uszkodzenia do jednej linii; kosztem jest brak struktury całościowej, więc walidację trzeba wykonywać linia po linii.

Średniozaawansowany~10 min czytaniaOpublikowano 2026-10-30Zaktualizowano 2026-10-30
Okładka artykułu o JSON Lines i NDJSON dla danych strumieniowych

Najważniejsze: używaj tablicy JSON, gdy przetwarzasz całą partię naraz, a JSON Lines, gdy potrzebujesz strumieniowego dopisywania i izolacji złych linii; jeśli nadal musisz walidować strukturę całościową, nie przechodź na format liniowy.

Gdy dane przekraczają pamięć albo napływają ciągle, struktura „jednej wielkiej tablicy” staje się wrogiem.

1. Reguły formatu

JSON Lines (.jsonl) i NDJSON (.ndjson) to praktycznie synonimy, a reguły w obu przypadkach są krótkie:

  1. Kodowanie to UTF-8, bez BOM;
  2. Każdy wiersz zawiera jedną kompletną, poprawną wartość JSON;
  3. Wiersze są rozdzielane \n; ostatni wiersz może, ale nie musi kończyć się nową linią;
  4. W wierszu nie może wystąpić nieescape'owany znak nowej linii — nową linię wewnątrz łańcucha trzeba zapisać jako \n;
  5. Puste wiersze są zwykle ignorowane, ale ścisła implementacja powinna traktować je jako niepoprawne.
{"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"}

Subtelna różnica między nimi: strona JSON Lines podkreśla „jedna wartość na wiersz, a wartość może być dowolnym JSON-em (w tym skalarem)”, podczas gdy specyfikacja NDJSON dodaje ściślejsze reguły implementacyjne, takie jak „brak pustych wierszy i brak skalarnych wierzchołków”. W inżynierii trzymanie się bardziej rygorystycznego zestawu chroni przed kłopotami.

2. Porównanie z tablicą JSON

Wymiar Tablica JSON JSON Lines
Dopisanie jednego rekordu Przepisanie całego pliku (zmiana zamykającego ]) Dopisanie jednej linii
Odczyt strumieniowy Wymaga parsera strumieniowego śledzącego głębokość nawiasów Podział po wierszach
Jedna uszkodzona pozycja Cały plik staje się nieparsowalny Dotknięta tylko ta linia
Walidacja całego pliku Walidacja wszystkiego jednym przebiegiem Tylko linia po linii
Kompresja Podobny rozmiar po gzip To samo, a bloki można kompresować równolegle
Wyrażanie zagnieżdżenia Natywne wsparcie Każda linia może zagnieżdżać wewnętrznie; linie są niezależne
Typowi konsumenci Wszystkie narzędzia JSON Systemy logów, potoki big data, zestawy treningowe LLM

Wniosek jest bezpośredni: to, czy dane są „partię rekordów”, czy „jednym dokumentem”, decyduje o wyborze formatu.

3. Odczyt: strumieniowanie linia po linii

W Node najprostszym podejściem jest 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 };
}

Zużycie pamięci tego kodu zależy tylko od rozmiaru pojedynczej linii, a nie od całkowitego rozmiaru pliku — to największa wartość formatu liniowego.

3.1 Gdy potrzebujesz tylko kilku pól, nie wywołuj najpierw JSON.parse

Jeśli każdy rekord ma 50 pól, a potrzebujesz 3, JSON.parse buduje obiekty dla wszystkich 50. Parser strumieniowy (stream-json, clarinet) potrafi wyciągnąć tylko docelowe pola, kosztem wyższej złożoności implementacyjnej. Zasada praktyczna: gdy pojedynczy rekord ma poniżej kilkuset KB, bezpośrednie wywołanie JSON.parse jest zwykle wciąż lepszym interesem.

3.2 Chiński tekst i BOM

Pliki .jsonl generowane w Windows często niosą BOM, a JSON.parse na pierwszej linii z pewnością zawiedzie. Usuń BOM przed odczytem albo sprawdź pierwsze trzy bajty podczas strumieniowania pierwszego fragmentu.

4. Zapis: dopisywanie i atomowość

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

Dwie rzeczy do zapamiętania:

  1. Wyjście JSON.stringify nie zawiera nowych linii (chyba że \n w łańcuchu jest escape'owane do dwóch znaków), więc jedna linia zawsze odpowiada dokładnie jednemu rekordowi;
  2. Gdy kilka procesów dopisuje współbieżnie, pojedynczy write mniejszy niż PIPE_BUF (zwykle 4096 bajtów na Linuksie) jest atomowy; wszystko większe nie ma gwarancji. W scenariuszach o wysokiej współbieżności przejdź na „jeden plik per proces, scalane po zrzuceniu na dysk” albo użyj magazynu z transakcjami.

5. Obsługa złych linii

To decyzja, którą format liniowy wymusza z góry:

Strategia Pasuje do Koszt
Pomijanie i zliczanie Danych tolerujących luki, takich jak logi i zdarzenia analityczne Dane giną po cichu, więc alerty są obowiązkowe
Natychmiastowa porażka Danych, których nie wolno zgubić, takich jak rozliczenia i faktury Jedna zła linia blokuje całą partię
Przekierowanie do pliku dead-letter Przypadków wymagających późniejszego uzupełnienia Dodatkowe miejsce i proces odtwarzania

Niezależnie od wyboru zapisz numer wiersza i fragment oryginalnej treści, inaczej później nie zlokalizujesz problemu. Numer wiersza nie występuje w komunikacie błędu JSON.parse, więc musisz utrzymywać go samodzielnie.

6. Kompresja i dzielenie na fragmenty

  • gzip kompresuje .jsonl w konsekwentnie wysokim stosunku (nazwy pól się powtarzają), więc zalecany jest .jsonl.gz;
  • gzip kompresuje blokami i natywnie wspiera strumieniową dekompresję, więc nie ma potrzeby dekompresowania całego pliku;
  • podziel duże pliki na fragmenty o rozmiarze około 1GB, z numerami sekwencji w nazwach, aby ułatwić równoległe przetwarzanie i wznawialny transfer;
  • nigdy nie przetwarzaj .jsonl przez „scalanie do tablicy” — to wyrzuca wszystkie jego zalety.

7. Kompromisy względem innych formatów big data

Format Struktura Zawiera schemat Dobre do
JSON Lines Niezależny per wiersz Nie (wymaga zewnętrznego kontraktu) Logi, importy i eksporty, dane treningowe
CSV Tabelaryczny Nie Raporty, wymiana z Excelem
Parquet Kolumnowy Tak Zapytania analityczne, bardzo duże zbiory offline
Avro Wierszowy Tak Komunikaty Kafka, ewolucja schematu

Zaletą JSON Lines jest to, że każdy język i każde narzędzie potrafi go natychmiast czytać i zapisywać; wadą jest brak schematu i kompresji kolumnowej. Gdy dane przestają mieścić się na jednej maszynie i potrzebne są częste zapytania kolumnowe, czas rozważyć Parquet.

8. Lista kontrolna

  • [ ] Rozszerzenie pliku jest spójne (.jsonl lub .ndjson) i nigdy nie mieszane;
  • [ ] Writer gwarantuje „jeden rekord na wiersz”, a nowe linie w łańcuchach są już escape'owane;
  • [ ] Reader usuwa BOM, pomija puste wiersze i utrzymuje numery wierszy;
  • [ ] Strategia złych linii jest jawnie zdefiniowana i monitorowana (złe linie / wszystkie linie);
  • [ ] Duże pliki są dzielone według rozmiaru i połączone z gzip oraz równoległym odczytem;
  • [ ] Tam, gdzie istnieje kontrakt pól, dostarczany jest dodatkowy schemat i walidacja linia po linii.

FAQ

Jak wybrać między tablicą JSON a JSON Lines?

Spójrz, czy dane są partią rekordów, czy jednym dokumentem. Wybierz JSON Lines, gdy rekordy są dopisywane i czytane jako strumień: dopisywanie oznacza zapisanie jednej linii na końcu, a odczyt wymaga tylko podziału po wierszach. Zachowaj tablicę JSON, gdy całą strukturę trzeba zwalidować jednym przebiegiem, ponieważ tablica JSON waliduje wszystko naraz, a JSON Lines tylko linia po linii.

Dlaczego jedna uszkodzona linia nie psuje całego pliku?

Każda linia w JSON Lines jest niezależną, kompletną wartością JSON, a parsowanie jest naturalnie linia po linii, więc uszkodzone dane dotykają tylko tej linii. W tablicy JSON jedna błąd składni czyni cały plik nieparsowalnym, i właśnie dlatego dane liniowe pasują do logów i zdarzeń analitycznych.

Czy konwertować JSON Lines do tablicy JSON?

Można: czytaj linia po linii, wywołaj JSON.parse na każdym rekordzie i zbierz je do tablicy. Wymaga to jednak zmieszczenia całej treści w pamięci i rezygnuje ze strumieniowego odczytu oraz dopisywania jednej linii. Przetwarzanie .jsonl do tablicy wyrzuca wszystkie zalety formatu, więc jest rozsądne tylko przy jednorazowym dostarczeniu, gdy dane mieszczą się w pamięci.