mitmario.dev

Synthese: die große Datei

Node.js Sandbox 3 Min Lesezeit 3 BeispieleLektion 6 von 7

Am Ende dieses Abschnitts kommen die drei Bausteine zusammen, und zwar in der Form, in der sie in echten Programmen zusammenkommen.

Ein Lesestrom liefert die Daten in Stücken. Dein Code verarbeitet jedes Stück und lässt es los. Ein Schreibstrom nimmt das Ergebnis entgegen. Und zwischendurch meldet jemand, wie weit es ist, denn ein Programm, das zehn Minuten schweigt, sieht aus wie ein abgestürztes.

Wohin der Fortschritt gehört

Die Regel dazu kennst du aus Lektion 4.3: Auf stdout gehört das Ergebnis, auf stderr alles über den Ablauf. Damals war das eine Verabredung. Hier gibt es endlich einen Anlass dafür.

Fortschritt auf dem Fehlerkanal
import { writeFileSync, createReadStream } from "node:fs";

writeFileSync("werte.txt", Array.from({ length: 5000 }, (_, i) => i + 1).join("\n"));

let summe = 0;
let gelesen = 0;

for await (const stueck of createReadStream("werte.txt", { encoding: "utf8", highWaterMark: 4096 })) {
  gelesen += 1;
  summe += stueck.length;
  console.error(`Stueck ${gelesen} verarbeitet`);
}

console.log(`Gesamtlaenge: ${summe}`);

Auf stdout landet hier genau eine Zeile, nämlich das Ergebnis. Die sechs Fortschrittsmeldungen gehen auf stderr, und im Terminal stehen sie deshalb in einem eigenen Block darunter, in Rot.

Dasselbe, alles auf stdout
import { writeFileSync, createReadStream } from "node:fs";

writeFileSync("werte.txt", Array.from({ length: 5000 }, (_, i) => i + 1).join("\n"));

let summe = 0;
let gelesen = 0;

for await (const stueck of createReadStream("werte.txt", { encoding: "utf8", highWaterMark: 4096 })) {
  gelesen += 1;
  summe += stueck.length;
  console.log(`Stueck ${gelesen} verarbeitet`);
}

console.log(`Gesamtlaenge: ${summe}`);

Jetzt sind es sieben Zeilen in einem Block, und rot ist nichts mehr. Auf einer Kommandozeile fiele selbst das nicht auf, dort laufen beide Kanäle unbeschriftet über denselben Bildschirm, und genau darin liegt die Falle. Der Unterschied zeigt sich erst, wenn jemand die Ausgabe weiterverwendet:

node auswertung.js > ergebnis.txt

Im ersten Fall steht in der Datei eine Zeile. Im zweiten stehen sieben, davon sechs überflüssige, und wer die Datei weiterverarbeitet, stolpert darüber. Bei einer Datei mit Millionen Zeilen sind es dann nicht sechs Zeilen zu viel, sondern tausende.

Die Fortschrittsmeldung ist außerdem für einen Menschen gedacht, der gerade zuschaut. Genau dafür ist stderr da: Es bleibt auf dem Bildschirm, auch wenn die eigentliche Ausgabe längst woandershin umgeleitet wurde.

Der Beweis, dass es sich lohnt

Bleibt die Frage, ob der ganze Aufwand mit den Stücken überhaupt etwas bringt. Das kann man messen.

Der Speicher wächst nicht mit der Datei
import { writeFileSync, createReadStream } from "node:fs";

writeFileSync("gross.txt", "x".repeat(20000000));

const vorher = process.memoryUsage().heapUsed;
let zeichen = 0;

for await (const stueck of createReadStream("gross.txt", { encoding: "utf8" })) {
  zeichen += stueck.length;
}

const zuwachs = process.memoryUsage().heapUsed - vorher;

console.log(`${zeichen} Zeichen gelesen`);
console.log(`Zuwachs unter 5 MB: ${zuwachs < 5000000}`);

Zwanzig Millionen Zeichen gelesen, und der Verbrauch auf dem Heap ist um weniger als fünf Megabyte gestiegen. Mit readFile wären es zwanzig gewesen, plus Reserve. Bei zwei Gigabyte wären es zwei Gigabyte, und da hört es dann auf.

Wir prüfen hier bewusst nur, ob der Zuwachs unter einer Grenze liegt, statt die Zahl selbst auszugeben: Der genaue Wert schwankt von Lauf zu Lauf, weil die Speicherbereinigung ihren eigenen Zeitplan hat. Die Aussage dahinter schwankt nicht. Der Verbrauch hängt an der Stückgröße, nicht an der Dateigröße.

Was in der Challenge zusammenkommt

Du liest eine Logdatei mit zwölftausend Zeilen, zählst nach Stufe, schreibst die Fehlerzeilen in eine eigene Datei und meldest zwischendurch den Fortschritt.

Das Zeilenlesen bekommst du diesmal fertig mitgeliefert, als kleine Hilfsfunktion. Sie enthält genau das Muster, das du in Lektion 7.4 selbst gebaut hast, nur einmal ordentlich verpackt: Sie nimmt einen Strom entgegen und gibt Zeilen aus. So sieht das aus, wenn man eine Technik einmal verstanden hat und sie danach nicht mehr jedes Mal von Hand hinschreibt.

Genau dieses Skript ist übrigens die Sorte Werkzeug, die man in echten Projekten regelmäßig braucht und die trotzdem nie in einer Bibliothek landet. Es ist zu klein dafür und zu speziell. Fünfzig Zeilen, und sie erledigen etwas, das mit readFile schlicht nicht ginge.

Zum Mitnehmen

Ein Lesestrom liefert, ein Emitter meldet, ein Schreibstrom nimmt entgegen. Und der Fortschritt gehört auf stderr, sonst steht er mitten im Ergebnis.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.