קריאה וכתיבה לקבצים עם Streams ב Node.JS

עבודה עם מידע בינארי דרך Streams היא חלק מהתקן של JavaScript ונתמכת בצורה מלאה גם ב Node וגם ב Deno, ואלה חדשות טובות כי תמיד כיף שדברים עובדים בכל מקום. בואו נדבר על הקוד.

1. קצת תיאוריה

ברמה הבסיסית Streams באים בשני טעמים יש את ה ReadableStream שאיתו אנחנו קוראים מידע ואת ה WritableStream שאיתו כותבים מידע. ב node.js המודול stream יודע להפוך Streams של Node ל Streams של Web API.

הדברים הטובים שאפשר לעשות עם Streams הם:

  1. אפשר לחבר אותם אחד לשני. אם מחברים Stream לקריאה ל Stream של כתיבה אז מקבלים העתקה.

  2. אפשר להוסיף טרנספורמציות על Stream וככה לקבל Stream חדש. לדוגמה אם לוקחים Stream בינארי של מידע שמגיע לקובץ ומוסיפים טרנספורמציה של פיענוח הביטים לטקסט מקבלים Stream של מחרוזות.

דוגמאות? בשמחה.

2. קריאת ביטים מקובץ והדפסתם ב Chunk-ים

נתחיל עם קריאת קובץ בינארי לפי בלוקים דרך Stream לקריאה. הפונקציה fs.createReadStream של Node יוצרת Stream לקריאה של Node, ובעזרת stream.toWeb נהפוך אותו ל Stream סטנדרטי. הקריאה מזרם לקריאה היא פשוט איטרציה אסינכרונית ולכן נוכל לכתוב:

import fs from 'node:fs';
import stream from 'node:stream';

const sin = stream.Readable.toWeb(fs.createReadStream('text.md'));
for await (const chunk of sin) {
  console.log(chunk);
}

והפלט:

Uint8Array(15143) [
   35,  32, 215, 156, 215, 162, 215, 169, 215, 149, 215, 170,
   32, 215, 144, 215, 170,  32, 215, 148, 215, 144, 215, 153,
  215, 158, 215, 153, 215, 153, 215, 156,  32, 215, 160, 215,
  164, 215, 156, 215, 144,  32, 215, 169, 215, 149, 215, 145,
   10, 215, 144, 215, 153, 215, 158, 215, 153, 215, 153, 215,
  156,  32, 215, 148, 215, 149, 215, 144,  32, 215, 155, 215,
  160, 215, 168, 215, 144, 215, 148,  32, 215, 144, 215, 151,
  215, 147,  32, 215, 148, 215, 158, 215, 167, 215, 149, 215,
  158, 215, 149, 215,
  ... 15043 more items
]

3. קריאת טקסט מקובץ באמצעות טרנספורמציה

הקריאה מקובץ דרך Stream עבדה ואיפשרה לקרוא את הקובץ בבלוקים. לקבצים בינאריים אפשר לעצור כאן, אבל אם הביטים שבקובץ מכילים טקסט אולי נרצה גם לפענח אותו ולראות מחרוזות על המסך. בשביל זה בדיוק התקן של Web Streams הוסיף מחלקה בשם TextDecoder. השימוש בה מאוד פשוט, אנחנו יוצרים מפענח, בבנאי אפשר להעביר לו את הקידוד (ברירת המחדל היא utf8), ואז מפעילים את הפונקציה decode עם בלוק של ביטים בשביל לפענח אותם לטקסט. ואל תדאגו הוא יודע לטפל כמו שצריך ב Multi Byte Strings אפילו בין Chunk-ים.

בשביל שהתוכנית הקודמת תדפיס טקסטים במקום ביטים צריך רק להעביר את הביטים שלנו דרך כזה Decoder. למזלנו ה API של Streams מספק דרך קלה לעשות את זה בדמות מחלקה בשם TextDecoderStream:

import fs from 'node:fs';
import stream from 'node:stream';

const sin = stream
  .Readable
  .toWeb(fs.createReadStream('/etc/shells'))
  .pipeThrough(new TextDecoderStream());

for await (const chunk of sin) {
  console.log(chunk);
}

4. כתיבה לקובץ עם WritableStream

בכתיבה לקובץ יש כמה נקודות שצריך להכיר אבל בעיקרון אין הפתעות גדולות:

  1. פותחים WritableStream ומפעילים את הפונקציה getWriter שלו.

  2. מפעילים את פונקציית write של ה Writer שקיבלנו בשביל לכתוב.

  3. בסוף קוראים ל Close שסוגר את ה Writer ואת ה Stream.

הקוד הבא כותב ביטים לקובץ:

import fs from 'node:fs';
import stream from 'node:stream';

const sout = stream
  .Writable
  .toWeb(fs.createWriteStream('demo.bin'));

const writer = sout.getWriter();
const data = new Uint8Array(10).fill(0).map((_, i) => i);
await writer.write(data);
writer.close();

5. העתקה עם Pipe

הטריק האחרון להיום עם Streams הוא העתקת קבצים, והמשחק הוא כזה - במקום להריץ לולאה שתקרא בצורה מפורשת בלוקים ותכתוב אותם לזרם השני אפשר פשוט לחבר זרם לקריאה לזרם לכתיבה והכל מסתדר. זה נראה ככה:

import fs from 'node:fs';
import stream from 'node:stream';

const sin = stream
  .Readable
  .toWeb(fs.createReadStream('/etc/shells'));

const sout = stream
  .Writable
  .toWeb(fs.createWriteStream('shells.txt'));

sin.pipeTo(sout);

בהעתקת קבצים אני מעדיף לוותר על ה Text Decoding כי בצורה כזאת אני מעתיק את הביטים כמו שהם מקובץ המקור בלי קשר אם הם מייצגים טקסט או לא או מה הקידוד שלהם.

למידע נוסף על Streams וכל מה שאפשר לעשות איתם ב Node.JS שווה להעיף מבט בתיעוד:

https://nodejs.org/api/webstreams.html

כל הדוגמאות בפוסט הזה נבדקו ועובדות ב Node גירסה 21.7 אחרי ששמרתי את הקוד בקובץ בשם a.mjs.