LoginSignup
38
25

More than 1 year has passed since last update.

DenoでクソデカCSVを読む

Last updated at Posted at 2022-12-29

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行分ずつ読み出すことができます。

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

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

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

image.png

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

まとめ

なお、CSV.stringify()のストリーミング版はまだ提供されていないようなので、自力でカンマで文字列結合するなどの対応が必要そうです。

38
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
25