Velké XML a JSON soubory

Velký dokument zpracuj po záznamech a uvolňuj již dokončené části. Streamové čtení pomáhá pouze tehdy, když další kroky znovu neshromáždí celý výsledek v paměti.

Autor: Aktualizováno

Vyber vhodný model

DOM drží celý strom a zabere víc než velikost vstupních bajtů. SAX, StAX nebo pull parser čte události. XPath nad celým stromem a globální třídění mohou i tak vyžadovat paměť pro celý dokument.

XML po přímých potomcích v Pythonu

Ukázka předpokládá důvěryhodný dokument s kořenem katalog a knihami jako přímými potomky. Po zpracování odstraní knihu z rodiče; samotné clear by nechalo v rodiči odkazy na prázdné elementy. Pro namespace použij rozšířené jméno a pro nedůvěryhodné XML bezpečný parser a limity.

XML po přímých potomcích v Pythonu
import xml.etree.ElementTree as ET
stack = []
for event, elem in ET.iterparse("katalog.xml", events=("start", "end")):
    if event == "start":
        stack.append(elem)
        continue
    if len(stack) == 2 and elem.tag == "kniha":
        print(elem.get("id"), elem.findtext("nazev"))
        stack[-2].remove(elem)
        elem.clear()
    stack.pop()

Průběžné čtení a zápis JSON

JSON.parse načítá celý text a vytváří celou hodnotu. Pro velké pole potřebuješ parser, který čte a zpracovává data postupně; dělení na řádky není parser obecného JSON. Pokud kontroluješ formát exportu, použij záznamy po řádcích. Při zápisu čekej, dokud je výstup připraven přijmout další data. Tomuto řízení tempa se říká backpressure. Jinak se nezapsané záznamy hromadí v paměti.

Měř celý řetězec

Sleduj špičkovou paměť, čas a počet záznamů na reprezentativních datech. Náš online nástroj pracuje se vstupem v paměti prohlížeče; velké provozní exporty zpracuj lokálně. Limity nastav i pro jeden extrémně velký záznam, hluboký strom a komprimovaný vstup.

JSON Lines v Node.js s řízením rychlosti zápisu

stream-jsonl.mjs — node stream-jsonl.mjs; odlišné vstupní a výstupní soubory
import {createReadStream, createWriteStream} from "node:fs";
import {pipeline} from "node:stream/promises";
const MAX_LINE = 1024 * 1024;
await pipeline(
  createReadStream("data.jsonl", {encoding:"utf8"}),
  async function* (source) {
    let pending = "";
    let number = 0;
    for await (const chunk of source) {
      pending += chunk;
      let index;
      while ((index = pending.indexOf("\n")) !== -1) {
        const line = pending.slice(0, index);
        pending = pending.slice(index + 1);
        number++;
        if (Buffer.byteLength(line) > MAX_LINE) throw new Error(`Řádek ${number}: limit`);
        if (line.trim() === "") throw new Error(`Řádek ${number}: prázdný záznam`);
        let record;
        try { record = JSON.parse(line); }
        catch (error) { throw new Error(`Řádek ${number}: ${error.message}`); }
        yield JSON.stringify(record) + "\n";
      }
      if (Buffer.byteLength(pending) > MAX_LINE) throw new Error("Limit rozpracovaného řádku");
    }
    if (pending !== "") throw new Error("Tento NDJSON kontrakt vyžaduje závěrečný newline");
  },
  createWriteStream("copy.jsonl", {encoding:"utf8"})
);

Pipeline předává výstup podle kapacity zapisovače a zpracuje chyby jednotlivých fází. Generátor nezadržuje celé pole záznamů; pending má limit rozpracovaného řádku plus velikost právě přijatého bloku. Jedna JSON hodnota se stále parsuje celá. Kromě bajtů proto při citlivém provozu kontroluj i hloubku a strukturu.

Pro dva úvodní záznamy z JSON Lines vznikne copy.jsonl se dvěma kompaktními řádky. Chybný třetí řádek ukončí pipeline. Výstupní soubor může být částečný: aby příjemce neviděl nedokončený export, piš do dočasného souboru a přejmenuj jej až po úspěšném dokončení. Tento příklad vyžaduje jeden záznam na řádek a znak nového řádku i za posledním záznamem. Není určen pro čtení běžného JSON pole.

Jak měřit a navrhnout celý export

Nejdřív ověř počet a obsah záznamů na malé sadě. Potom zvyš počet řádově a sleduj špičkovou paměť, například lokálním měřením procesu nebo systémovým time s přepínačem pro paměť. Přesný příkaz závisí na systému: na macOS /usr/bin/time -l node stream-jsonl.mjs, na GNU systémech /usr/bin/time -v.

Průběžné čtení nepomůže, pokud výsledky uložíš do rostoucího pole, jedním krokem třídíš všechny záznamy nebo zapisovač neomezuje frontu. Zpracování s globálním pořadím navrhni s externím tříděním či databází. U komprimovaného vstupu omez také rozbalenou velikost. Dokumentaci pipeline a backpressure poskytuje Node.js streams.