DenoでCSVを処理する

Deno Advent Calendar 2020 4日目の記事です🦕

今日は、DenoでCSVを処理する方法について説明します。

## はじめに

この記事では、Denoの標準モジュールを使ってCSVを処理する方法について解説します。
Deno及び各ライブラリは以下のバージョンを想定しています。

|項目|バージョン|
|:---:|:---:|
|**deno**|v1.5.4|
|**deno_std**|v0.79.0|

## 簡単な例

試しに次のようなCSVファイルを処理してみます。

```csv:data.csv
name,age
hoge,20
piyo,30
fuga,40
```

Denoの標準モジュールには、CSVを処理するためのモジュール(`std/encoding/csv`)が存在するので、これを使ってみます。

`std/encoding/csv`で提供されている`parse`関数を使うことで、CSVデータを解析することができます。

```typescript:sample.ts
import { parse } from "https://deno.land/std@0.79.0/encoding/csv.ts";
import { BufReader } from "https://deno.land/std@0.79.0/io/bufio.ts";

const file = await Deno.open("data.csv");
try {
  const buf = BufReader.create(file);
  const result = await parse(buf);
  console.log(result);
} finally {
  file.close();
}
```

試しにこのファイル(`sample.ts`)を実行してみましょう。CSVファイルを読み込むため、`--allow-read`オプションを指定する必要があります。

```shell
$ deno run --allow-read sample.ts
```

すると、次のように出力されます。

```javascript
[ [ "name", "age" ], [ "hoge", "20" ], [ "piyo"
, "30" ], [ "fuga", "40" ] ]
```

### 解説

#### CSVファイルを開く

まず、`parse`関数によってCSVファイルを処理するためには、そのファイルを開く必要があります。Denoでファイルを開くときは`Deno.open`関数を使います。

```typescript
const file = await Deno.open("data.csv");
```

#### `BufReader`を作成する

`parse`関数は入力元のCSVデータを`string`または`BufReader`(`std/io/bufio`で提供されています)のいずれかの形式で受け取ります。
今回のケースでは、ファイルに格納されたCSVデータを処理したいため、`BufReader`の方を使いましょう。

`BufReader`を作成する際は、`BufReader.create`関数に`Reader`interfaceを実装したオブジェクトを渡す必要があります。
`Deno.open`で返却される`File`オブジェクトは`Reader`interfaceを実装しているため、そのまま渡すことができます。

```typescript
  const buf = BufReader.create(file);
```

#### `parse`関数を使ってCSVを解析する

あとは、作成した`BufReader`を`parse`関数に渡すことで、CSVデータを解析できます。

```typescript
  const result = await parse(buf);
```

`parse`関数は、デフォルトでは、次のような入れ子の配列を返却します。

```javascript
[ [ "name", "age" ], [ "hoge", "20" ], [ "piyo"
, "30" ], [ "fuga", "40" ] ]
```

そのため、`for-of`ループなどを使って解析結果を処理できます。

```typescript
  const result = await parse(buf) as [string, number][];
  for (const [name, age] of result) {
    console.log(name, age);
  }
```

## 応用編

### CSVの最初の行をスキップしたい

次のように、CSVの1行目がヘッダーとして定義されていたとします。

```csv
name,age
hoge,20
piyo,30
fuga,40
```

このような場合、`parse`関数の第2引数で`skipFirstRow`に`true`を指定します。
すると、CSVの最初の行がスキップされます。

```typescript
  const result = await parse(buf, { skipFirstRow: true });
```

また、`skipFirstRow`に`true`を設定した際は、`parse`関数は**オブジェクトの配列を返却します。**

```javascript
[
  { name: "hoge", age: "20" },
  { name: "piyo", age: "30" },
  { name: "fuga", age: "40" }
]
```

### ヘッダーが定義されていないCSVデータを処理したい

例えば、次のようにヘッダーが存在しないCSVデータがあったとします。
(一列目がID、二列目が生徒名、三列目がテストの得点というようなデータを想定しています)

```csv
1,taro,100
2,jiro,68
3,hanako,45
```

このようなヘッダーが存在しないデータを処理することを想定し、`parse`関数は`columns`オプションを提供しています。

例えば、次のように`columns`オプションを指定して実行してみます。

```typescript
  const result = await parse(buf, {
    columns: [
      { name: "id", parse: Number }, 
      { name: "student" },
      {
        name: "rank",
        parse: _score => {
          const score = Number(_score);
          if (score === 100) {
            return "A";
          } else if (score >= 80) {
            return "B";
          } else if (score >= 60) {
            return "C";
          } else if (score >= 40) {
            return "D";
          } else {
            return "E";
          }
        }
      },
    ]
  });
```

すると、`parse`関数は、次のようなオブジェクトの配列を返却します。

```javascript
[
  { id: 1, student: "taro", rank: "A" },
  { id: 2, student: "jiro", rank: "C" },
  { id: 3, student: "hanako", rank: "D" }
]
```

### TSV形式のデータを処理したい

次のように、カンマではなくタブで区切られたデータがあったとします。

```tsv
id	name	age
1	hoge	15
2	piyo	24
3	fuga	32
```

このような場合は、`separator`オプションを指定すると、適切に処理できます。

```typescript
  const result = await parse(buf, {
    separator: "\t",
    skipFirstRow: true,
  });
  // [
  //   { id: "1", name: "hoge", age: "15" },
  //   { id: "2", name: "piyo", age: "24" },
  //   { id: "3", name: "fuga", age: "32" }
  // ]
```

## 終わりに

この記事では、DenoでCSVを処理する方法について解説しました。

最近、`std/encoding/csv`で`stringify`関数が実装されました。(おそらく、Denoのv1.6.0のリリースに合わせて、提供されると思います)
これにより、将来的には、JavaScriptのオブジェクトをCSV形式でファイルに書き出すことも容易になります。

興味があれば、ぜひ使ってみてください!

## 参考

* [APIドキュメント](https://doc.deno.land/https/deno.land/std/encoding/csv.ts)