Denoを使って巨大なCSVファイルを開く機会があったので、その方法の覚え書きです。
DenoでCSVを読み書きする方法(通常ver.)
通常、DenoでCSVを読むには、標準ライブラリのparse
関数を使います。
import * as CSV from "https://deno.land/std@0.170.0/encoding/csv.ts";
// ↓直接pathに文字列を指定するとカレントディレクトリからの相対パス。import.meta.resolveを使用するとこのファイルからの相対パス。
const path = new URL(import.meta.resolve("./path/to/file"));
const text = await Deno.readTextFile(path); // ファイルを文字列として読み取り
const data = CSV.parse(text); // 文字列をCSVとしてパース
console.log(data);
(※検索するとBufReader
を使うやり方も出てきますが、これは古いやり方で現在はあまり推奨されていないようです。)
また、CSVをファイルに書き込むには以下のようにstringify
関数を使います。
import * as CSV from "https://deno.land/std@0.170.0/encoding/csv.ts";
const path = new URL(import.meta.resolve("./path/to/file"));
const data = [["hello", "world"]];
const text = CSV.stringify(data); // データをCSV文字列に変換
await Deno.writeTextFile(path, text); // 文字列データをファイルに書き込み
このあたりはJSON.parse
/JSON.stringify
を使ってJSONファイルを読み書きする時と全く同じ使い方なので、特に問題はないと思います。
CSVのサイズが大きすぎてメモリに乗らない時のやり方
ところが、CSVのファイルが大きすぎると、そもそもDeno.readTextFile()
でファイルを開けない可能性があります。
筆者は8.9GBのCSVファイルを読もうとして以下のようなエラーに引っかかりました。
error: Uncaught (in promise) TypeError: Cannot allocate String: buffer exceeds maximum length.
const text = await Deno.readTextFile("path/to/file");
^
at async Object.readTextFile (deno:runtime/js/40_read_file.js:56:20)
at async file:///C:/Users/ayame/work/deno/test/tmptp.ts:6:14
このエラーはファイルサイズが大きすぎて開けないことを示しています。
こういう場合は、Web Stream APIと標準ライブラリのcsv/stream.ts
を使ってストリーミング処理する(=細切れでデータを読み出しながらCSVをパースする)ことで、ファイルを開くことができます。
コードは以下のようになります。
import { CsvStream } from "https://deno.land/std@0.170.0/encoding/csv/stream.ts";
const path = new URL(import.meta.resolve("./path/to/file"));
// 変数 readable には ReadableStream(Web Stream API)が入っている
const { readable } = await Deno.open(path);
// ストリーミング処理
const data = readable
.pipeThrough(new TextDecoderStream()) // utf8のバイト列をstringに変換
.pipeThrough(new CsvStream()); // 文字列をCSVとしてパース
for await (const line of data) {
console.log(line); // 1行ずつ読み出し
}
ここでは先ほど使ったDeno.readTextFile
の代わりにDeno.open()
を使用しています。これを使うとファイルをストリーミングすることができます。
上記のコードのreadable
という変数にReadableStreamが入りますので、.pipeThrough()
メソッドを呼ぶことでストリーミング処理を行うことができます。
.pipeThrough()
メソッドは連鎖させることができるため、複数の変換処理を順番に実行できます。今回はTextDecoderStream
でUint8Arrayから文字列への変換を、CsvStream
で文字列からCSVへのパースを行っています。
CSVパース後のデータは、for-await-of
文で1行分ずつ読み出すことができます。
まとめ
- CSVファイルを読み書きする方法(通常ver.)
→ https://deno.land/std@0.170.0/encoding/csv.ts のCSV.parse()
やCSV.stringify()
を使う。 - ファイルサイズが大きいCSVファイルを読む方法
→ https://deno.land/std@0.170.0/encoding/csv/stream.ts のCsvStream
を使う。
なお、CSV.stringify()
のストリーミング版はまだ提供されていないようなので、自力でカンマで文字列結合するなどの対応が必要そうです。