Denoには主に2種類のストリームが実装されている。Go言語をモデルにした入出力インターフェースと、Web標準のストリームである。
これらに加えてイテレータもストリームのように扱える。これら3つは相互に変換可能である。
大事なのは、これらはNode.jsのストリームとは全く異なるものだということ。混乱の元なので気を付けたい。
この記事はDeno1.19のリリースを受けて大幅に改稿し、
改稿時には
・Deno.FileからDeno.FsFileへの改名
・stdin/stderr/stdoutやDeno.FsFileのWebストリーム対応
・std/ioからstd/streamsへの再編
についての情報を反映した。
この記事ではまずWebストリームについて解説し、次にGo由来のストリームについて解説し、最後に2つの相互変換について解説する。
Webストリーム
Webストリームはブラウザ互換のストリーム。
の3種のインターフェースからなる。現在Denoに実装されているAPIのうち、
-
Deno.FsFile
:ファイルシステム(doc) -
Deno.stdin.readable
:標準入出力(doc) -
Deno.stdout.writable
(doc) -
Deno.stderr.writable
(doc) -
Deno.Process.stdin.writable
:サブプロセス入出力(doc) -
Deno.Process.stdout.readable
(doc) -
Deno.Process.stderr.readable
(doc) -
TextDecoderStream
API:Uint8Arrayと文字列の変換(MDN) -
TextEncoderStream
API(MDN) -
CompressionStream
API:gzip圧縮の変換(MDN) -
DecompressionStream
API(MDN) -
Blob.stream()
API(MDN) -
fetch
API:body等に渡せる(MDN) -
WebSocketStream
API(doc)
が対応済み。
ReadableStream
から取得したデータをTransformStream
で変換し、WritableStream
へ流し込むことができる。
https://developer.mozilla.org/ja/docs/Web/API/Streams_API/Concepts#pipe_chains
Webストリームの操作方法
Webストリームの操作方法については、Web標準ということもあり他の記事でも開設されているため、詳しくは取り上げない。
ReadableStreamの読み取り
ReadableStreamはWritableStreamやTransformStreamへパイプするのが基本であるが、for-await文でも読み出すことができる。
for await (const chunk of Deno.stdin.readable) {
console.log(chunk); //=> 細切れになったデータがUint8Arrayで流れてくる
}
ReadableStreamからWritableStreamへのパイプ
ReadableStreamから読み取ったデータをWritableStreamに流すには、ReadableStream.pipeTo()
を使用する。
ReadableStream
は、Deno.stdin.readable
やfile.readable
のように取得する。
WritableStream
は、Deno.stdout.writable
やfile.writable
のように取得する。
// Deno.openの返り値はDeno.FsFile
const file = await Deno.open("./foo.txt");
file.readable.pipeTo(Deno.stdout.writable); // fileから読み取ったデータをstdoutにパイプ
// Deno.openの返り値はDeno.FsFile
const file = await Deno.open("./foo.txt");
Deno.stdin.readable.pipeTo(file.writable); // stdinから読み取ったデータをfileにパイプ
1行ずつの読み込み
Webストリームの変換にはTransformStream
とReadableStream.pipeThrough()
関数を使う。
ここでは、
- Uint8Arrayのストリームを文字列のストリームに変換するTextDecoderStream(Web API)
- 文字列のストリームを1行ずつの形に変換するTextLineStream(Deno標準ライブラリ)
の2種類のTransformStreamを使用する例を挙げる。
import { TextLineStream } from "https://deno.land/std@0.136.0/streams/mod.ts";
// Deno.openの返り値はDeno.FsFile
const file = await Deno.open("./foo.txt");
const lineStream = file.readable // fileからReadableStreamで読み出し
.pipeThrough(new TextDecoderStream()) // Uint8Arrayをstringに変換
.pipeThrough(new TextLineStream()) // 1行ずつに変換
for await (const line of lineStream) {
console.log(line); //=> 1行ずつ表示
}
Deno標準ライブラリのstreamsモジュールでは、他にもいくつかのTransformStream
が提供されており、同様に扱うことができる。
その他のストリーム処理
- ストリームのマージ(順序を考慮する):zipReadableStreams関数
- ストリームのマージ(順序を考慮しない):mergeReadableStreams関数
- ストリームの分岐:ReadableStream.teeメソッド
- 改行で分割するTransformStream:TextLineStreamクラス
- 区切り文字で分割するTransformStream:TextDelimiterStreamクラス
- バイト列で分割するTransformStream:DelimiterStreamクラス
- 指定したバイト数の分だけ読み出すTransformStream:LimitedTransformStreamクラス
- 指定した数の分だけchunkを読み出すTransformStream:LimitedBytesTransformStreamクラス
- CSV形式のテキストを配列のストリームに変換するTransformStream:CSVStreamクラス
例えばTextLineStreamとLimitedBytesTransformStreamを組み合わせると、「先頭からn行読み込み」のような事ができる。
Go由来の入出力インターフェース
ここから先は、Webストリームが導入される前に使われていた、Go由来の入出力インターフェースについて解説する。
Go由来の入出力インターフェースは以下。
Deno.Reader
Deno.ReaderSync
Deno.Writer
Deno.WriterSync
Deno.Seeker
Deno.SeekerSync
Deno.Closer
これらは、Denoの入出力APIである
Deno.FsFile
Deno.conn
Deno.stdin
Deno.stdout
Deno.stderr
Deno.Process.stdin
Deno.Process.stdout
Deno.Process.stderr
に実装されている。
(注:ここでは、インターフェースというのはTypeScriptのinterfaceのこと。)
それぞれ、どのクラスにどのインターフェースが実装されているものかを一覧にしたものが下表。
Deno.Reader |
Deno.ReaderSync |
Deno.Writer |
Deno.WriterSync |
Deno.Seeker |
Deno.SeekerSync |
Deno.Closer |
|
---|---|---|---|---|---|---|---|
Deno.FsFile ※ファイル読み込み |
○ | ○ | ○ | ○ | ○ | ○ | ○ |
Deno.conn ※Http接続 |
○ | ○ | ○ | ||||
Deno.stdin |
○ | ○ | ○ | ||||
Deno.stdout |
○ | ○ | ○ | ||||
Deno.stderr |
○ | ○ | ○ | ||||
Deno.Process.stdin ※サブプロセスのstdin |
○ | ○ | |||||
Deno.Process.stdout ※サブプロセスのstdout |
○ | ○ | |||||
Deno.Process.stderr ※サブプロセスのstderr |
○ | ○ |
これらのインターフェースと標準ライブラリを使うことで、「標準入力からファイルへ」「ファイルからサブプロセスへ」等、それぞれ相互にストリームをパイプすることができる。
Go由来の入出力インターフェースの操作方法
標準ライブラリのstream
を用いて操作する。以下は一例。詳しくは公式ドキュメント(https://doc.deno.land/https://deno.land/std/streams/mod.ts )を参照。
なお、これらの関数はかつてはDeno.xxx
に存在したが、標準ライブラリに移動されたという経緯がある(例えばDeno.copy
は標準ライブラリのio/copy
を経てstreams/copy
に移動した)。古い記事ではこれらの変更が反映されていないことがあるので注意。(参考)
データの読み書き
基本となるデータの読み書きはreadAll
関数(読み取り)とwriteAll
関数(書き込み)を使う。
import { readAll, writeAll } from "https://deno.land/std@0.125.0/streams/mod.ts";
{
// Deno.openの返り値はReaderかつWriter
const file = await Deno.open("my_file.txt", {read: true});
const content = new TextDecoder.decode(await readAll(file));
file.close();
console.log(content);
}
{
const content = "Hello World";
// Deno.openの返り値はReaderかつWriter
const file = await Deno.open("my_file.txt", {read: true});
await writeAll(file, new TextEncoder().encode(content));
file.close();
}
ReaderからWriterへのパイプ
copy
関数を使うとReaderからWriterへストリームをパイプすることができる。
import { copy } from "https://deno.land/std@0.125.0/streams/mod.ts";
// Deno.openの返り値であるDeno.FsFileはReaderかつWriter
const reader = await Deno.open("my_file.txt");
const writer = await Deno.open("my_file.txt");
// readerのデータをwriterへコピー
await copy(reader, writer);
reader.close();
writer.close();
1行ずつの読みこみ
標準ライブラリのioモジュールから、readLines
関数を使う。(streamsモジュールではないので注意)
ioモジュールには他にも、指定した区切り文字で分割するreadDelim
関数など、reader操作系の関数が揃っている。
import { readLines } from "https://deno.land/std@0.125.0/io/mod.ts";
// Deno.openの返り値であるDeno.FsFileはReaderかつWriter
const reader = await Deno.open("my_file.txt");
// 1行ずつ読み込み
for await (const line of readLines(reader)) {
console.log(line)
}
reader.close();
その他
encoding/binaryモジュールにもDeno.Readerを操作する関数がある。
WebストリームとGo由来のストリームの相互変換
これまで紹介したストリーム2種と、イテレータ(Iterable)は相互に変換できる。
Webストリーム⇔Go由来ストリーム
-
writerFromStreamWriter(
WritableStream
→Deno.writer
) -
writableStreamFromWriter(
Deno.writer
→WritableStream
) -
readerFromStreamReader(
ReadableStream
→Deno.reader
) -
readableStreamFromReader(
Deno.reader
→ReadableStream
)
import {
writerFromStreamWriter,
writableStreamFromWriter,
readerFromStreamReader,
readableStreamFromReader,
} from "https://deno.land/std@0.125.0/streams/mod.ts";
const writer = writerFromStreamWriter(writable);
const writable = writableStreamFromWriter(writer);
const reader = readerFromStreamReader(readable);
const readable = readableStreamFromReader(reader);
Go由来ストリーム⇔イテレータ
-
readerFromIterable(イテレータ→
Deno.reader
) -
iterateReader(
Deno.reader
→イテレータ)
import { delay } from "https://deno.land/std@0.125.0/async/delay.ts";
import { iterateReader, readerFromIterable } from "https://deno.land/std@0.125.0/streams/mod.ts";
async function* asyncIterable() {
while (true) {
await delay(1000);
yield new TextEncoder().encode("hey^^");
}
}
// イテレータ→`Deno.reader`
const reader = readerFromIterable(asyncIterable());
// `Deno.reader`→イテレータ
for await (const chunk of iterateReader(reader)) {
console.log(chunk);
}
Webストリーム⇔イテレータ
-
readableStreamFromIterable(イテレータ→
ReadableStream
) - ReadableStream[Symbol.asyncIterator]
※ReadableStreamにはSymbol.asyncIterator
が生えているため、そのままfor-await-of
文で回すことができる。
import { delay } from "https://deno.land/std@0.125.0/async/delay.ts";
import { readableStreamFromIterable } from "https://deno.land/std@0.125.0/streams/mod.ts";
async function* asyncIterable() {
while (true) {
await delay(1000);
yield new TextEncoder().encode("hey^^");
}
}
// イテレータ→`ReadableStream`
const readableStream = readableStreamFromIterable(asyncIterable());
// `ReadableStream`→イテレータ
// ReadableStreamはそのまま`for-await-of`文で回すことができる
for await (const chunk of readableStream) {
console.log(chunk);
}
Node.jsストリームのポリフィル
Deno標準ライブラリには、Node.jsストリームのポリフィルが存在する。
主にNode.js互換モードやDeno対応npmレジストリで使われるものであり、自分の手で使用する機会は少ないと思われる。
ストリームの代わりに使えるもの
特に標準入出力やファイル等は、ストリームを扱わなくても使える便利関数が用意されている。
- 標準入出力系
- ファイル読み書き系
ストリームを使う必要が無い場合は、Deno.readTextFile等、上記のAPIを使ったほうが簡単に書ける(こともある)。
まとめ
- DenoにはGo言語由来の入出力インターフェースと、Web標準のストリームの、2種類のストリームがある
- これらは相互に変換可能である
- Node.jsのストリームとは全く別物
なお、従来DenoにはGo言語由来の入出力インターフェースのみ存在していたが、後々Web APIが拡充されるに従ってWebストリームに対応するAPIが増えており、将来的にはWebストリームに一本化される予定である。