DenoでクソデカCSVを読む

Denoを使って巨大なCSVファイルを開く機会があったので、その方法の覚え書きです。

## DenoでCSVを読み書きする方法（通常ver.）

通常、DenoでCSVを読むには、標準ライブラリの`parse`関数を使います。

```ts
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`関数を使います。

```ts
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をパースする）ことで、ファイルを開くことができます。

コードは以下のようになります。

```ts
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](https://developer.mozilla.org/ja/docs/Web/API/ReadableStream)が入りますので、`.pipeThrough()`メソッドを呼ぶことでストリーミング処理を行うことができます。

https://developer.mozilla.org/en-US/docs/Web/API/Streams_API

`.pipeThrough()`メソッドは連鎖させることができるため、複数の変換処理を順番に実行できます。今回は`TextDecoderStream`でUint8Arrayから文字列への変換を、`CsvStream`で文字列からCSVへのパースを行っています。

CSVパース後のデータは、`for-await-of`文で1行分ずつ読み出すことができます。

:::note
##### Tips: 出力時に1行ずつ確認したい時は、promptを挟む

先ほどのコードでは、CSVの内容が一気に出力されてきます。
CSVの内容を確認したい場合、`console.log()`の直後に`prompt()`関数を挟むと、エンターキーを押したら次の行に進むようにすることができます。

```ts
for await (const line of csv) {
  console.log(line);
  prompt("次の行に進みますか？"); // ここでエンターキーを押すと次の行に行く
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/727c26ad-8c1b-5664-8fc0-525a1e304d2d.png)

なお、`prompt`の代わりに`confirm`を使うと、エンターキーではなく`y/n`の入力を求めるようになります。
:::

## まとめ

- 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()`のストリーミング版はまだ提供されていないようなので、自力でカンマで文字列結合するなどの対応が必要そうです。