JSONL w Rust: serde_json, BufReader i abstrakcje o zerowym koszcie
Kompletny przewodnik pracy z plikami JSONL (JSON Lines) w Rust. Naucz sie czytac, zapisywac, parsowac i strumieniowac dane JSONL przy uzyciu serde_json, BufReader, rayon do rownoleglosci i tokio do asynchronicznego I/O.
Ostatnia aktualizacja: luty 2026
Dlaczego Rust do JSONL?
Rust to doskonaly wybor do przetwarzania JSONL, gdy liczy sie wydajnosc, bezpieczenstwo pamieci i poprawnosc. Model wlasnosci eliminuje wyscigy danych na etapie kompilacji, abstrakcje o zerowym koszcie pozwalaja pisac lancuchy iteratorow wysokiego poziomu kompilowane do ciaslych petli, a serde_json jest jednym z najszybszych parserow JSON dostepnych w jakimkolwiek jezyku. Jesli budujesz potok danych, ktory musi przetwarzac miliony rekordow JSONL na sekunde, Rust daje Ci kontrole, aby to osiagnac bez poswiecania bezpieczenstwa.
JSONL (JSON Lines) przechowuje jeden obiekt JSON na linie, co czyni go idealnym do strumieniowania, logowania w trybie append-only i przetwarzania duzych zbiorow danych. BufReader w Rust czyta pliki linia po linii bez ladowania calego pliku do pamieci, a model iteratorow idealnie pasuje do liniowej struktury JSONL. W tym przewodniku nauczysz sie, jak czytac i zapisywac JSONL za pomoca BufReader, parsowac rekordy do silnie typowanych struktur za pomoca serde, przetwarzac rekordy rownolegle za pomoca rayon, obslugiwac asynchroniczne I/O za pomoca tokio i budowac solidna obsluge bledow za pomoca Result i operatora ?.
Czytanie plikow JSONL za pomoca BufReader
Biblioteka standardowa Rust udostepnia BufReader do wydajnego buforowanego czytania plikow. W polaczeniu z iteratorem lines() czyta pliki JSONL linia po linii z minimalnym narzutem pamieci. Kazda linia jest parsowana niezaleznie, co sprawia, ze podejscie to nadaje sie do plikow dowolnego rozmiaru.
Najprostsze podejscie opakowuje File w BufReader, iteruje po lines() i parsuje kazda linie za pomoca serde_json. Utrzymuje w pamieci tylko jedna linie naraz, niezaleznie od rozmiaru pliku.
use std::fs::File;use std::io::{BufRead, BufReader};use serde_json::Value;fn read_jsonl(path: &str) -> std::io::Result<Vec<Value>> {let file = File::open(path)?;let reader = BufReader::new(file);let mut records = Vec::new();for (line_num, line) in reader.lines().enumerate() {let line = line?;let trimmed = line.trim();if trimmed.is_empty() {continue; // Pomin puste linie}match serde_json::from_str::<Value>(trimmed) {Ok(value) => records.push(value),Err(e) => {eprintln!("Pomijanie nieprawidlowego JSON w linii {}: {}", line_num + 1, e);}}}Ok(records)}fn main() -> std::io::Result<()> {let records = read_jsonl("data.jsonl")?;println!("Zaladowano {} rekordow", records.len());if let Some(first) = records.first() {println!("Pierwszy rekord: {}", first);}Ok(())}
Dodaj serde i serde_json do swojego Cargo.toml. Funkcja derive wlacza makra derive Serialize i Deserialize do definiowania typowanych struktur.
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"# Opcjonalnie: do rownoleglego przetwarzaniarayon = "1.10"# Opcjonalnie: do asynchronicznego I/Otokio = { version = "1", features = ["full"] }
Bezpieczne typowo parsowanie za pomoca serde_json
Jedna z najwiekszych zalet Rust do przetwarzania JSONL sa makra derive serde, ktore pozwalaja deserializowac kazda linie JSON bezposrednio do silnie typowanej struktury. Kompilator weryfikuje, ze Twoj kod poprawnie obsluguje dane, wylapujac niezgodnosci typow, brakujace pola i bledy strukturalne na etapie kompilacji, a nie w czasie dzialania.
Zdefiniuj strukture z Deserialize i uzyj serde_json::from_str do parsowania kazdej linii JSONL. Brakujace lub znieksztalcone pola generuja jasne komunikaty o bledach w momencie parsowania, zamiast powodowac paniki pozniej w programie.
use serde::Deserialize;use std::fs::File;use std::io::{BufRead, BufReader};#[derive(Debug, Deserialize)]struct LogEntry {timestamp: String,level: String,message: String,#[serde(default)]metadata: Option<serde_json::Value>,}fn read_typed_jsonl(path: &str) -> anyhow::Result<Vec<LogEntry>> {let file = File::open(path)?;let reader = BufReader::new(file);let mut entries = Vec::new();for line in reader.lines() {let line = line?;let trimmed = line.trim();if trimmed.is_empty() {continue;}let entry: LogEntry = serde_json::from_str(trimmed)?;entries.push(entry);}Ok(entries)}fn main() -> anyhow::Result<()> {let entries = read_typed_jsonl("logs.jsonl")?;for entry in &entries {println!("[{}] {}: {}", entry.level, entry.timestamp, entry.message);}Ok(())}
Gdy rekordy JSONL maja zmienne schematy, uzyj serde_json::Value do elastycznego dostepu. Mozesz indeksowac wartosci za pomoca notacji nawiasowej lub uzywac metod as_str(), as_i64() do bezpiecznej konwersji typow.
use serde_json::Value;use std::fs::File;use std::io::{BufRead, BufReader};fn process_dynamic_jsonl(path: &str) -> anyhow::Result<()> {let file = File::open(path)?;let reader = BufReader::new(file);for line in reader.lines() {let line = line?;let trimmed = line.trim();if trimmed.is_empty() {continue;}let value: Value = serde_json::from_str(trimmed)?;// Dostep do pol dynamicznieif let Some(name) = value.get("name").and_then(Value::as_str) {println!("Imie: {}", name);}if let Some(age) = value.get("age").and_then(Value::as_i64) {println!("Wiek: {}", age);}// Sprawdz czy pole istniejeif value.get("email").is_some() {println!("Posiada pole email");}}Ok(())}
Zapisywanie plikow JSONL w Rust
Zapisywanie JSONL to odwrotnosc czytania: serializuj kazdy rekord do ciagu JSON, dodaj nowa linie i zapisz do pliku. Uzycie BufWriter i serde_json::to_string zapewnia zarowno poprawnosc, jak i wydajnosc.
Serializuj kazda strukture za pomoca serde_json::to_string() i zapisz ja z nowa linia. Makro derive Serialize automatycznie obsluguje konwersje z typow Rust do JSON.
use serde::Serialize;use std::fs::File;use std::io::Write;#[derive(Serialize)]struct Record {id: u64,name: String,score: f64,}fn write_jsonl(path: &str, records: &[Record]) -> std::io::Result<()> {let mut file = File::create(path)?;for record in records {let json = serde_json::to_string(record).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;writeln!(file, "{}", json)?;}Ok(())}fn main() -> std::io::Result<()> {let records = vec![Record { id: 1, name: "Alice".into(), score: 95.5 },Record { id: 2, name: "Bob".into(), score: 87.3 },Record { id: 3, name: "Charlie".into(), score: 92.1 },];write_jsonl("output.jsonl", &records)?;println!("Zapisano {} rekordow", records.len());Ok(())}
Przy zapisywaniu duzych plikow JSONL opakuj File w BufWriter, aby zmniejszyc liczbe wywolan systemowych. Laczy to male zapisy w wieksze operacje oprozniana bufora, znacznie poprawiajac przepustowosc.
use serde::Serialize;use std::fs::File;use std::io::{BufWriter, Write};#[derive(Serialize)]struct Event {timestamp: u64,event_type: String,payload: serde_json::Value,}fn write_buffered_jsonl(path: &str, events: &[Event]) -> std::io::Result<()> {let file = File::create(path)?;let mut writer = BufWriter::new(file);for event in events {serde_json::to_writer(&mut writer, event).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;writer.write_all(b"\n")?;}writer.flush()?;Ok(())}
Rownolegle przetwarzanie za pomoca rayon
Crate rayon w Rust zapewnia rownolegle przetwarzanie danych z minimalnymi zmianami w kodzie. Wywolujac par_iter() zamiast iter(), rayon automatycznie rozdziela prace miedzy wszystkie rdzenie procesora za pomoca puli watkow z kradziezy zadan. Jest to szczegolnie skuteczne dla transformacji JSONL intensywnie obciazajacych procesor, gdzie parsowanie i przetwarzanie dominuja nad czasem I/O.
use rayon::prelude::*;use serde::{Deserialize, Serialize};use std::fs::File;use std::io::{BufRead, BufReader, BufWriter, Write};#[derive(Debug, Deserialize, Serialize)]struct Record {id: u64,text: String,#[serde(default)]word_count: Option<usize>,}fn parallel_process_jsonl(input_path: &str,output_path: &str,) -> anyhow::Result<usize> {// Krok 1: Wczytaj wszystkie linie do pamiecilet file = File::open(input_path)?;let reader = BufReader::new(file);let lines: Vec<String> = reader.lines().collect::<Result<_, _>>()?;// Krok 2: Parsuj i transformuj rownoleglelet processed: Vec<Record> = lines.par_iter().filter(|line| !line.trim().is_empty()).filter_map(|line| {serde_json::from_str::<Record>(line.trim()).ok()}).map(|mut record| {record.word_count = Some(record.text.split_whitespace().count());record}).collect();// Krok 3: Zapisz wynikilet file = File::create(output_path)?;let mut writer = BufWriter::new(file);let count = processed.len();for record in &processed {serde_json::to_writer(&mut writer, record)?;writer.write_all(b"\n")?;}writer.flush()?;Ok(count)}fn main() -> anyhow::Result<()> {let count = parallel_process_jsonl("input.jsonl", "output.jsonl")?;println!("Przetworzono {} rekordow rownolegle", count);Ok(())}
Ten wzorzec czyta wszystkie linie, przetwarza je rownolegle za pomoca par_iter() rayon i zapisuje wyniki sekwencyjnie. Krok rownolegle obsluguje parsowanie i transformacje na wszystkich dostepnych rdzeniach procesora. Dla plikow mieszczacych sie w pamieci mozna osiagnac niemal liniowe przyspieszenie wraz z liczba rdzeni. Dla bardzo duzych plikow rozwaiz dzielenie danych wejsciowych, aby zrownowazyc zuzycie pamieci i rownolegle przetwarzanie.
Asynchroniczne I/O za pomoca tokio
Gdy przetwarzanie JSONL obejmuje sieciowe I/O, takie jak czytanie z endpointow HTTP lub zapis do zdalnego magazynu, srodowisko uruchomieniowe tokio pozwala nakladac oczekiwanie na I/O z obliczeniami. Trait tokio::io::AsyncBufReadExt zapewnia asynchroniczna metode lines(), ktora odzwierciedla synchroniczne API BufReader.
use tokio::fs::File;use tokio::io::{AsyncBufReadExt, BufReader};use serde::Deserialize;#[derive(Debug, Deserialize)]struct ApiRecord {id: u64,url: String,status: String,}async fn read_jsonl_async(path: &str) -> anyhow::Result<Vec<ApiRecord>> {let file = File::open(path).await?;let reader = BufReader::new(file);let mut lines = reader.lines();let mut records = Vec::new();while let Some(line) = lines.next_line().await? {let trimmed = line.trim().to_string();if trimmed.is_empty() {continue;}let record: ApiRecord = serde_json::from_str(&trimmed)?;records.push(record);}Ok(records)}async fn write_jsonl_async(path: &str,records: &[ApiRecord],) -> anyhow::Result<()> {use tokio::io::AsyncWriteExt;let mut file = File::create(path).await?;for record in records {let json = serde_json::to_string(record)?;file.write_all(json.as_bytes()).await?;file.write_all(b"\n").await?;}file.flush().await?;Ok(())}#[tokio::main]async fn main() -> anyhow::Result<()> {let records = read_jsonl_async("api_logs.jsonl").await?;println!("Odczytano {} rekordow asynchronicznie", records.len());write_jsonl_async("output.jsonl", &records).await?;Ok(())}
Podejscie asynchroniczne jest najbardziej korzystne w polaczeniu z operacjami sieciowymi. Dla czystego plikowego I/O na lokalnych dyskach synchroniczny BufReader jest czesto szybszy, poniewaz async dodaje narzut na planowanie zadan. Uzyj tokio, gdy musisz czytac JSONL ze strumieni HTTP, koszy S3 lub innych zrodel sieciowych, gdzie opoznienie I/O dominuje.
Obsluga bledow: Result i operator ?
Typ Result w Rust i operator ? zapewniaja czysty, komponowalny wzorzec obslugi bledow dla przetwarzania JSONL. Zamiast blokow try-catch kazda operacja mogaca zakonczyc sie niepowodzeniem zwraca Result, ktory mozesz propagowac za pomoca ? lub obsluzyc lokalnie za pomoca match. Sprawia to, ze sciezki bledow sa jawne i niemozliwe do przypadkowego zignorowania.
use serde::Deserialize;use std::fs::File;use std::io::{BufRead, BufReader};use thiserror::Error;#[derive(Error, Debug)]enum JsonlError {#[error("Blad I/O: {0}")]Io(#[from] std::io::Error),#[error("Blad parsowania JSON w linii {line}: {source}")]Parse {line: usize,source: serde_json::Error,},#[error("Blad walidacji w linii {line}: {message}")]Validation {line: usize,message: String,},}#[derive(Debug, Deserialize)]struct Record {id: u64,name: String,value: f64,}fn validate_record(record: &Record, line: usize) -> Result<(), JsonlError> {if record.name.is_empty() {return Err(JsonlError::Validation {line,message: "nazwa nie moze byc pusta".into(),});}if record.value < 0.0 {return Err(JsonlError::Validation {line,message: format!("wartosc musi byc nieujemna, otrzymano {}", record.value),});}Ok(())}fn process_jsonl_with_errors(path: &str,) -> Result<Vec<Record>, JsonlError> {let file = File::open(path)?; // Blad Io konwertowany automatycznie przez #[from]let reader = BufReader::new(file);let mut records = Vec::new();for (idx, line) in reader.lines().enumerate() {let line_num = idx + 1;let line = line?; // Propagacja bledow I/Olet trimmed = line.trim();if trimmed.is_empty() {continue;}let record: Record = serde_json::from_str(trimmed).map_err(|e| JsonlError::Parse { line: line_num, source: e })?;validate_record(&record, line_num)?;records.push(record);}Ok(records)}fn main() {match process_jsonl_with_errors("data.jsonl") {Ok(records) => println!("Pomyslnie przetworzono {} rekordow", records.len()),Err(JsonlError::Io(e)) => eprintln!("Blad pliku: {}", e),Err(JsonlError::Parse { line, source }) => {eprintln!("Blad JSON w linii {}: {}", line, source);}Err(JsonlError::Validation { line, message }) => {eprintln!("Blad walidacji w linii {}: {}", line, message);}}}
Ten wzorzec definiuje niestandardowe wyliczenie bledow za pomoca thiserror, ktore obejmuje wszystkie tryby awarii: bledy I/O, bledy parsowania JSON z numerami linii i bledy walidacji specyficzne dla domeny. Atrybut #[from] umozliwia automatyczna konwersje z std::io::Error, a operator ? propaguje bledy w gore stosu wywolan. Kazdy wariant bledu niesie wystarczajacy kontekst, aby wygenerowac jasny komunikat diagnostyczny, ulatwiajac dokladne wskazanie, ktora linia spowodowala problem.
Crate'y Rust do przetwarzania JSON
Ekosystem Rust oferuje kilka crate'ow do parsowania JSON o roznych charakterystykach wydajnosci. serde_json to standardowy wybor, podczas gdy simd-json i sonic-rs przesuwa granice wydajnosci za pomoca instrukcji SIMD.
serde_json
StandardDe facto standard dla JSON w Rust. Integruje sie bezproblemowo z makrami derive serde do bezkosztowej serializacji. Doskonaly dla wiekszosci zadan JSONL z silnym bezpieczenstwem typow, kompleksowymi komunikatami o bledach i szerokim wsparciem ekosystemu.
simd-json
NajszybszyPort simdjson do Rust wykorzystujacy instrukcje SIMD do parsowania JSON z predkoscia wielu gigabajtow na sekunde. Wymaga mutowalnych buforow wejsciowych i zapewnia inne API niz serde_json, ale moze byc 2-4x szybszy dla obciazen intensywnie parsujacych. Najlepiej nadaje sie do potokow o duzej przepustowosci.
sonic-rs
SIMD + serdeBiblioteka JSON z akceleracja SIMD z API kompatybilnym z serde. Laczy szybkosc simd-json z ergonomia serde_json. Obsluguje zarowno leniwe, jak i zachlanne tryby parsowania, co czyni ja dobrym wyborem, gdy potrzebujesz wydajnosci bez przepisywania kodu.
Wyprobuj nasze darmowe narzedzia JSONL
Nie chcesz pisac kodu? Uzyj naszych darmowych narzedzi online, aby przegladac, walidowac i konwertowac pliki JSONL bezposrednio w przegladarce.