Praktyczna praca z plikami JSONL: otwieranie, konwersja i szukanie uszkodzonych linii
Podsumowanie: Przekazanie całego pliku .jsonl do JSON.parse i dostanie błędu prawie nigdy nie oznacza, że plik jest uszkodzony — oznacza, że strategia odczytu jest błędna. Ten przewodnik obejmuje standardowe sposoby inspekcji i odczytu JSONL linia po linii, komendy konwersji w obie strony z tablicą JSON, tabelę sześciu typów uszkodzonych linii oraz poprawny sposób pobierania NDJSON ze strumieniowych odpowiedzi HTTP.

Sedno sprawy: każda linia pliku JSONL to niezależna, poprawna wartość JSON, ale plik jako całość nie jest jednym dokumentem JSON. Jeśli przekazanie całego pliku do JSON.parse lub json.load kończy się błędem, w dziewięciu na dziesięć przypadków plik jest w porządku — błędna jest twoja strategia odczytu.
Jeden rekord na linię, brak przecinków między liniami, brak zewnętrznej tablicy — JSONL rezygnuje z „struktury całego dokumentu" w zamian za dopisywanie, odczyt strumieniowy i izolację awarii na poziomie jednej linii. Wszystkie trzy korzyści urzeczywistniają się tylko przy przetwarzaniu linia po linii, a wszystkie trzy zamieniają się w błędy w momencie próby sparsowania całego pliku naraz.
1. Trzy nazwy, jeden format: JSONL, NDJSON, JSON Lines
Początkujący często przyjmują, że różnica nazw oznacza różnicę składni. Nie oznacza:
| Nazwa | Typowy kontekst | Rozszerzenie / media type |
|---|---|---|
| JSON Lines | Inżynieria danych, zbiory ML (pandas, Spark, Hugging Face) | .jsonl |
| NDJSON | Strumieniowe API HTTP, dane w czasie rzeczywistym | .ndjson, application/x-ndjson |
| JSONL | Potoczny skrót dla dowolnego z nich | — |
Każde narzędzie czytające jeden format czyta też drugi. Jedynym kryterium wyboru nazwy są konwencje twojego ekosystemu: pisz .jsonl, przygotowując zbiory dla Hugging Face, ustawiaj application/x-ndjson, udostępniając strumieniowe API. Przenośny plik JSONL spełnia trzy reguły: kodowanie UTF-8 (bez BOM), jedna pełna wartość JSON na linię, linie rozdzielone \n. Końcowy znak nowej linii na końcu pliku jest silnie zalecany, a puste linie nie są poprawnymi rekordami. Szczegóły protokołu i uzasadnienie konstrukcji opisuje nasz przegląd JSON Lines i NDJSON; ten artykuł dotyczy ściśle tego, co zrobić, gdy plik masz już w rękach.
2. Inspekcja: najpierw head, potem grep, edytor na końcu
Podwójne kliknięcie na 500-megabajtowy .jsonl, żeby otworzyć go w edytorze, to najczęstszy błąd: trzy miliony linii tekstu plus podświetlanie składni plus bufor cofania zawieszą praktycznie każdy edytor — i JSON nie ma się czego wstydzić. Parsowanie dokumentu 45 MB zajmuje w Node 22 około 208 ms; zawiesza się dopiero renderowanie. Formaty zorientowane na linie istnieją dla narzędzi liniowych, bo rekordy pokrywają się z fizycznymi liniami:
# Najpierw zmierz skalę: liczba rekordów i rozmiar pliku, bez czytania zawartości
wc -l events.jsonl && du -h events.jsonl
# Zobacz, jak wygląda rekord (potwierdź pola i strukturę)
head -n 1 events.jsonl | jq .
# Znajdź docelowe rekordy bezpośrednio: narzędzia liniowe po prostu działają — przywilej JSONL
grep '"level":"error"' events.jsonl | head -n 5
tail -n 100 events.jsonl | jq 'select(.latency_ms > 500)'
Zwróć uwagę na ostatnią komendę: każda linia wychodząca z tail to wciąż niezależna wartość JSON, więc trafia prosto do jq bez potrzeby odbudowywania tablicy. To podejście działa identycznie na plikach wielogigabajtowych z niemal zerowym narzutem pamięci — to ta sama zasada, co w dlaczego duże pliki JSON spowalniają strony.
3. Trzy standardowe wzorce odczytu linia po linii
Odczyt linia po linii musi ogarnąć trzy rzeczy: pomijanie pustych linii (zwłaszcza końcowego znaku nowej linii), raportowanie błędów z numerem linii i niedopuszczanie, by jedna uszkodzona linia zabiła całą robotę. Standardowe wzorce w trzech językach krążą wokół tych punktów.
Python, wyłącznie biblioteka standardowa:
import json
with open("events.jsonl", encoding="utf-8") as f:
for line_number, line in enumerate(f, start=1):
if not line.strip(): # pomijaj puste linie
continue
try:
record = json.loads(line) # parsuj każdą linię niezależnie
except json.JSONDecodeError as e:
print(f"Linia {line_number} nieudana: {e}") # zgłoś, nie przerywaj
continue # odizoluj uszkodzoną linię
handle(record)
Node z wbudowanym modułem readline — stała pamięć niezależnie od rozmiaru pliku:
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(`Linia ${lineNumber}: ${e.message}`);
}
}
W Go 1.27 encoding/json/v2 staje się stabilnym domyślnym modułem, a jego strumieniowe dekodowanie wartość po wartości idealnie pasuje do JSONL; do osadzania wyrażeń jq użyj gojq. Przy bibliotekach nie śpiesz się z instalacją pakietów: jsonlines w Pythonie zgłasza numer i surowy tekst każdej wadliwej linii, a orjsonl czyta i zapisuje wprost skompresowane pliki gzip/bzip2/zstd; pakiet Node ndjson nie wydał wersji od 2020 roku — wbudowane readline wystarczy.
4. Konwersja w obie strony z tablicą JSON
Kierunek ma znaczenie; pomył go i każda komenda będzie zła. Zapamiętaj jedną regułę: tablica → JSONL to map (każdy rekord zbijany jest do jednej linii), JSONL → tablica to collect (linie zbierane są do listy).
# Tablica JSON → JSONL: -c zbija każdy element do jednej linii
jq -c '.[]' records.json > records.jsonl
# JSONL → tablica JSON: -s zbiera całe wejście w jedną tablicę
jq -s '.' events.jsonl > records.json
# Filtrowanie JSONL linia po linii: -s niepotrzebne, jq przetwarza wejście jako strumień
jq -c 'select(.active == true)' events.jsonl > active.jsonl
Jednolinijkowce w Pythonie są równoważne: tablica → JSONL zapisuje json.dumps(r, ensure_ascii=False) + "\n" na rekord; odwrotnie zbiera wyniki json.loads linia po linii do listy. Każdy kierunek ma jedną pułapkę. W drodze tam (tablica → JSONL) każdy prawdziwy znak nowej linii wewnątrz rekordu musi być escapowany przez json.dumps — inaczej jeden rekord porwie się na wiele linii. W drodze powrotną (JSONL → tablica) nie sklejaj linii w jeden gigantyczny ciąg i go nie parsuj — przy milionach linii to zbędna, pełna kopia w pamięci. Agregacje (sumy, grupowania, średnie) wymagają -s, który ładuje wszystko; przy plikach wielogigabajtowych przejdź na awk albo strumieniowy akumulator w Pythonie.
5. Sześć rodzajów uszkodzonych linii i jak je diagnozować
Prawie każdy błąd JSONL żyje na poziomie linii. Walidacja linia po linii z raportowaniem numerów to jedyne wiarygodne rozpoznanie. Uporządkowane według częstotliwości:
| Objaw | Przyczyna źródłowa | Naprawa |
|---|---|---|
Extra data na końcu linii |
Przecinki dodane między liniami | Usuń je; każda linia musi obyć się sama |
| Jeden rekord rozczłonkowany na linie | Nieescapowany prawdziwy znak nowej linii wewnątrz rekordu (sformatowany ręcznie) | Zbij ponownie przez json.dumps / jq -c |
Pierwsza linia pada z Unexpected token ï |
Plik ma BOM | Zapisz ponownie jako UTF-8 bez BOM |
| Błąd tylko w ostatniej linii, choć wygląda dobrze | Brak końcowego znaku nowej linii albo zbędne białe znaki | Dodaj \n; puste linie pomijaj z konwencji |
| Błąd nowej linii wewnątrz ciągu | Dosłowny znak nowej linii w ciągu, nie \n |
Napraw generator; nigdy nie sklejaj ciągów JSON ręcznie |
| Duże liczby całkowite zmieniają wartość | Parsowanie przez double (ucięcie przy 2^53) | Użyj parsera zachowującego precyzję, zob. precyzja bigint |
Rutyna diagnostyczna to zawsze trzy kroki: head -n 1, żeby potwierdzić kodowanie i strukturę pierwszej linii; uruchomienie skryptu linia po linii, który zgłasza numery wszystkich uszkodzonych linii zamiast stanąć na pierwszej; potem wyciągnięcie uszkodzonych linii pojedynczo — jedna linia to jeden pełny rekord, a jego izolowane oględziny nic nie kosztują. Przy importach hurtowych zalecana polityka to „kwarantanna, nie przerwanie": zapisuj uszkodzone linie z numerami do errors.jsonl, ładuj poprawne dalej, a decyzję o ponownym imporcie podejmuj na podstawie odsetka uszkodzonych linii.
6. JSONL przez HTTP: konsumpcja linia po linii i backpressure
Typowy schemat NDJSON przez HTTP: serwer pisze nieustannie, klient czyta nieustannie. Po stronie klienta nie wołaj response.json() — buforuje całe ciało przed parsowaniem, co traci sens strumieniowania. Czytaj strumień ciała bezpośrednio, tnij po \n i przenoś niekompletny ogon do następnej iteracji:
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(); // ostatni kawałek może być niepełną linią
for (const line of lines) {
if (line.trim()) handle(JSON.parse(line));
}
}
Po stronie serwera dwie rzeczy są obowiązkowe: po pierwsze, gdy res.write() zwróci false, czekaj na zdarzenie drain (backpressure), inaczej pamięć rośnie bez granic za każdym razem, gdy konsument jest wolniejszy od producenta; po drugie, na trasach strumieniowych wyłącz middleware gzip — gzip musi zbuforować całą odpowiedź, zanim coś skompresuje, co całkowicie niweczy strumieniowanie NDJSON. Oba zachowania zostały potwierdzone testami lokalnymi i kilkoma niezależnymi anglojęzycznymi źródłami, to nie zgadywanie.
7. Kiedy JSONL to zły wybór
Negatywna przestrzeń wyboru formatu też się liczy. Trzy scenariusze powinny zostać przy tablicy JSON:
| Wymaganie | Dlaczego JSONL pasuje słabo | Użyj zamiast tego |
|---|---|---|
| Walidacja struktury całego dokumentu | Brak struktury całościowej; dryf pól umyka uwadze | Tablica JSON + Schema |
| Dostęp losowy do rekordu N | Zmienne długości linii; brak wyszukiwania binarnego | SQLite / Parquet |
| Głębokie zagnieżdżenie, odwołania między rekordami | Każda linia jest izolowana; odwołania nie mają gdzie mieszkać | Standardowy dokument JSON |
Reguła kciuka w jednym zdaniu: jeśli rekordy przychodzą w czasie albo są konsumowane linia po linii (logi, zdarzenia, eksporty, zbiory treningowe), bierz JSONL; jeśli mały wolumen powstaje raz, konsumowany jest raz i wymaga semantyki całego dokumentu, bierz tablicę JSON. Warianty pochodne (JSON Text Sequences z separatorem RS) to osobny temat — zob. artykuł o RFC 7464.
FAQ
Czy istnieje realna różnica między JSONL a NDJSON?
Inżyniersko nie. Oba znaczą „jedna pełna wartość JSON na linię, bez zewnętrznej obudowy, bez przecinków między liniami", a każde narzędzie obsługujące jeden obsłuży drugi. Różnica tkwi w nazewnictwie i zwyczajach ekosystemu: .jsonl dominuje w inżynierii danych i zbiorach ML; .ndjson i application/x-ndjson dominują w strumieniowych API HTTP. Przy integracji z systemem zewnętrznym trzymaj się rozszerzenia i media type z jego dokumentacji.
Czy mogę czytać plik .jsonl bezpośrednio przez JSON.parse?
Nie cały plik. JSON.parse przyjmuje dokładnie jeden tekst JSON, a JSONL to wiele niezależnych wartości ułożonych linia po linii, więc parsowanie całości zawsze kończy się „Unexpected token" albo „Extra data". Podziel na linie i parsuj każdą niepustą osobno, zgłaszając numer linii przy błędzie.
Jeśli jedna linia pliku JSONL jest uszkodzona, czy cały plik przepada?
Nie — izolacja awarii linia po linii to rdzeń projektu formatu. Uszkodzona linia szkodzi tylko sobie: pomiń ją, zapisz numer i czytaj dalej. Przy importach hurtowych odłóż uszkodzone linie do osobnego pliku-kwarantanny, ładuj poprawne rekordy normalnie, a decyzję o naprawie i ponownym imporcie podejmuj na podstawie odsetka uszkodzonych linii.
Jak przekonwertować JSONL na Excel lub CSV?
Najpierw zamień na tablicę albo wyciągnij pola linia po linii, potem przepuść przez konwersję tabelaryczną: zbij przez jq -c '.[]' data.jsonl i podaj konwerterowi, albo przepuszczaj każdą linię przez json.loads do pandas. Uwaga: spłaszczone pola zagnieżdżone tworzą nazwy kolumn z kropkami, pola typu array da się zapisać w komórce tylko jako tekst, a kolumny z długimi ID warto z góry zadeklarować jako ciągi, żeby nie stracić precyzji.