Node.js (NestJS) で csv ファイルを読み込む（４つのライブラリ比較）

## はじめに

Node.js (NestJS) で csv を読み込むには、使用できるライブラリが４つほどあります。
備忘と比較のため、ここではそれぞれのコードを記載していきます。

|項番|名称|参考サイト|
|---|---|---|
|1|csv-parse|[npmjs - csv-parse](https://www.npmjs.com/package/csv-parse), [CSV for Node.js](https://csv.js.org/)|
|2|csv-parser|[npmjs - csv-parser](https://www.npmjs.com/package/csv-parser), [OpenBase - csv-parser](https://openbase.com/js/csv-parser/documentation)|
|3|csv|[npmjs - csv](https://www.npmjs.com/package/csv), [初心者のためのCSV読み込み・JSON変換まとめ！](https://www.sejuku.net/blog/85650)|
|4|fast-csv|[npmjs - fast-csv](https://www.npmjs.com/package/fast-csv), [Fast-CSV - Methods](https://c2fo.github.io/fast-csv/docs/parsing/methods)|

＜**どのライブラリを使用するか**＞
一番メジャーな「① csv-parse」を使用するのが無難そうですが、Stream を使用する場合の記述が長くなります（私の確認した範囲で）。

[比較サイト](https://npmcompare.com/compare/csv,csv-parse,csv-parser,fast-csv)を見ると、どのライブラリも一定数ダウンロードされており、更新も頻繁にされていることから、使いやすいものを使用すれば良いと思います。

＜**実行環境**＞
フレームワークは NeatJS を使用します（そのため TypeScript で記述します）。

|名称|バージョン|
|---|---|
|Node.js|14.17.6|
|NestJS|8.2.0|
|TypeScript|4.5.4|

＜**使用する CSV ファイル**＞
以下のファイルを共通して使用します（フォルダは自由に指定してください）。

```src/assets/csv/test.csv
品名,区分,単価,購入数,購入日
にんじん,野菜,70,3,2020/10/20
りんご,果物,150,2,2020/10/22
みかん,果物,40,10,2020/10/30
キャベツ,野菜,180,1,2020/11/1
じゃがいも,野菜,50,5,2020/11/5
バナナ,果物,300,2,2020/11/6
メロン,果物,1500,1,2020/11/8
```

## 1. csv-parse

[csv-parse](https://www.npmjs.com/package/csv-parse) は、一番ダウンロードされているライブラリです。

### 1-1. インストール

```sh
$ npm install --save csv-parse
```

### 1-2. 同期処理で取得

まず、同期処理の例です。
シンプルな形式ですが、ファイルを一旦全部読み込むため、ファイルサイズ分だけメモリを使用することになります。

#### 1-2-1. オプション指定なし

```sample.ts
import * as fs from 'fs';
import { parse } from 'csv-parse/sync';

const data = fs.readFileSync('src/assets/csv/test.csv');
const records = parse(data);
for (const record of records) {
  console.log(record);
}
```

＜参考サイト＞
・[import * as fs from 'fs' の記述について](https://stackoverflow.com/questions/71865726/typeerror-0-dayjs-1-default-is-not-a-function)

＜**出力結果**＞
オプションの指定をしない場合の取得結果は、以下のように配列形式となります。

```sh:出力結果
[ '品名', '区分', '単価', '購入数', '購入日' ]
[ 'にんじん', '野菜', '70', '3', '2020/10/20' ]
[ 'りんご', '果物', '150', '2', '2020/10/22' ]
[ 'みかん', '果物', '40', '10', '2020/10/30' ]
[ 'キャベツ', '野菜', '180', '1', '2020/11/1' ]
[ 'じゃがいも', '野菜', '50', '5', '2020/11/5' ]
[ 'バナナ', '果物', '300', '2', '2020/11/6' ]
[ 'メロン', '果物', '1500', '1', '2020/11/8' ]
```

#### 1-2-2. オプション指定

オプション指定は、parse() メソッドの引数に指定します。
以下は、CSV をオブジェクト形式で取得するようにして、区切り文字をタブ文字と指定する場合です。

```sample.ts
const records = parse(data, { columns: true, delimiter: '\t' });
```

|キー|内容|
|---|---|
|columns|先頭行をカラム名としてオブジェクト表示|
|delimiter|区切り文字を指定|

※ 上記以外にもオプションはありますので、公式サイトなどを確認してみてください。

出力結果は、以下のようにオブジェクト形式となります。

```sh:出力結果
{
  '品名': 'にんじん',
  '区分': '野菜',
  '単価': '70',
  '購入数': '3',
  '購入日': '2020/10/20'
}
{
  '品名': 'りんご',
  '区分': '果物',
  '単価': '150',
  '購入数': '2',
  '購入日': '2020/10/22'
}
# 以下略
```

<details><summary><font color="blue">コード全体を表示</font></summary><div>

＜**テキストファイル**＞
使用するファイルの区切り文字はタブ（`\t`）にしています。
タブ区切りのため拡張子は .tsv としていますが、.csv でも何でも構いません。

```src/assets/csv/testTab.tsv
品名	区分	単価	購入数	購入日
にんじん	野菜	70	3	2020/10/20
りんご	果物	150	2	2020/10/22
みかん	果物	40	10	2020/10/30
キャベツ	野菜	180	1	2020/11/1
じゃがいも	野菜	50	5	2020/11/5
バナナ	果物	300	2	2020/11/6
メロン	果物	1500	1	2020/11/8
```

＜**ソースコード**＞

```sample.ts
import * as fs from 'fs';
import { parse } from 'csv-parse/sync';

const data = fs.readFileSync('src/assets/csv/testTab.tsv');
const records = parse(data, { columns: true, delimiter: '\t' });
for (const record of records) {
  console.log(record);
}
```

＜**出力結果**＞

```sh:出力結果
{
  '品名': 'にんじん',
  '区分': '野菜',
  '単価': '70',
  '購入数': '3',
  '購入日': '2020/10/20'
}
{
  '品名': 'りんご',
  '区分': '果物',
  '単価': '150',
  '購入数': '2',
  '購入日': '2020/10/22'
}
{
  '品名': 'みかん',
  '区分': '果物',
  '単価': '40',
  '購入数': '10',
  '購入日': '2020/10/30'
}
{
  '品名': 'キャベツ',
  '区分': '野菜',
  '単価': '180',
  '購入数': '1',
  '購入日': '2020/11/1'
}
{
  '品名': 'じゃがいも',
  '区分': '野菜',
  '単価': '50',
  '購入数': '5',
  '購入日': '2020/11/5'
}
{
  '品名': 'バナナ',
  '区分': '果物',
  '単価': '300',
  '購入数': '2',
  '購入日': '2020/11/6'
}
{
  '品名': 'メロン',
  '区分': '果物',
  '単価': '1500',
  '購入数': '1',
  '購入日': '2020/11/8'
}
```
</div></details>

### 1-3. Stream で取得

Stream で取得する場合は次のようになります。
コードは少し長くなってしまいます。

#### 1-3-1. オプション指定なし

```sample.ts
import * as fs from 'fs';
import { parse } from 'csv-parse';

const records = [];
const parser = parse();
parser.on('readable', () => {
  let record;
  while ((record = parser.read()) !== null) {
    records.push(record);
  }
});
parser.on('error', (e) => console.log(e.message));
parser.on('end', () => console.log(records));
// parser を使用して実行
fs.createReadStream('src/assets/csv/test.csv').pipe(parser);
```

＜**出力結果**＞

```sh:出力結果
[
  [ '品名', '区分', '単価', '購入数', '購入日' ],
  [ 'にんじん', '野菜', '70', '3', '2020/10/20' ],
  [ 'りんご', '果物', '150', '2', '2020/10/22' ],
  [ 'みかん', '果物', '40', '10', '2020/10/30' ],
  [ 'キャベツ', '野菜', '180', '1', '2020/11/1' ],
  [ 'じゃがいも', '野菜', '50', '5', '2020/11/5' ],
  [ 'バナナ', '果物', '300', '2', '2020/11/6' ],
  [ 'メロン', '果物', '1500', '1', '2020/11/8' ]
]
```

#### 1-3-2. オプション指定

parse の引数にオプションを指定します。
指定できる内容は、同期処理の場合と同じです。

```sample.ts
const parser = parse({ delimiter: '\t', columns: true });
```

|キー|内容|
|---|---|
|columns|先頭行をカラム名としてオブジェクト表示|
|delimiter|区切り文字を指定|

※ 上記以外にもオプションはありますので、公式サイトなどを確認してみてください。

<details><summary><font color="blue">コード全体を表示</font></summary><div>

＜**テキストファイル**＞
使用するファイルの区切り文字はタブ（`\t`）にしています。
タブ区切りのため拡張子は .tsv としていますが、.csv でも何でも構いません。

```src/assets/csv/testTab.tsv
品名	区分	単価	購入数	購入日
にんじん	野菜	70	3	2020/10/20
りんご	果物	150	2	2020/10/22
みかん	果物	40	10	2020/10/30
キャベツ	野菜	180	1	2020/11/1
じゃがいも	野菜	50	5	2020/11/5
バナナ	果物	300	2	2020/11/6
メロン	果物	1500	1	2020/11/8
```

＜**ソースコード**＞

```sample.ts
import * as fs from 'fs';
import { parse } from 'csv-parse';

const records = [];
const parser = parse({ delimiter: '\t', columns: true });
parser.on('readable', () => {
  let record;
  while ((record = parser.read()) !== null) {
    records.push(record);
  }
});
parser.on('error', (e) => console.log(e.message));
parser.on('end', () => console.log(records));
// parser を使用して実行
fs.createReadStream('src/assets/csv/testTab.tsv').pipe(parser);
```

＜**出力結果**＞

```sh:出力結果
[
  {
    '品名': 'にんじん',
    '区分': '野菜',
    '単価': '70',
    '購入数': '3',
    '購入日': '2020/10/20'
  },
  {
    '品名': 'りんご',
    '区分': '果物',
    '単価': '150',
    '購入数': '2',
    '購入日': '2020/10/22'
  },
  {
    '品名': 'みかん',
    '区分': '果物',
    '単価': '40',
    '購入数': '10',
    '購入日': '2020/10/30'
  },
  {
    '品名': 'キャベツ',
    '区分': '野菜',
    '単価': '180',
    '購入数': '1',
    '購入日': '2020/11/1'
  },
  {
    '品名': 'じゃがいも',
    '区分': '野菜',
    '単価': '50',
    '購入数': '5',
    '購入日': '2020/11/5'
  },
  {
    '品名': 'バナナ',
    '区分': '果物',
    '単価': '300',
    '購入数': '2',
    '購入日': '2020/11/6'
  },
  {
    '品名': 'メロン',
    '区分': '果物',
    '単価': '1500',
    '購入数': '1',
    '購入日': '2020/11/8'
  }
]
```
</div></details>

## 2. csv-parser

次は、[csv-parser](https://www.npmjs.com/package/csv-parser) です。
前述の csv-parse　と１文字違いなので混同しそうですが、末尾に `r` が付いており、異なるライブラリとなります。

### 2-1. インストール

```sh
$ npm install --save csv-parser
```

### 2-2. サンプルコード

Stream を使用してファイルを読み込みます。

#### 2-2-1. オプション指定なし

```sample.ts
import * as fs from 'fs';
import * as csv from 'csv-parser';

const results = [];
fs.createReadStream('src/assets/csv/test.csv')
  .pipe(csv())
  .on('data', (data) => results.push(data))
  .on('end', () => console.log(results));
```

＜**出力結果**＞
オプションの指定をしない場合の取得結果は、以下のようにオブジェクト形式となります。

```sh:出力結果
[
  {
    '品名': 'にんじん',
    '区分': '野菜',
    '単価': '70',
    '購入数': '3',
    '購入日': '2020/10/20'
  },
  {
    '品名': 'りんご',
    '区分': '果物',
    '単価': '150',
    '購入数': '2',
    '購入日': '2020/10/22'
  },
  # 中略
]
```

#### 2-2-2. オプション指定

オプション指定は、csv() メソッドの引数に指定します。
以下は、先頭行もレコードとして取得し（`headers: false`）、区切り文字をタブ文字と指定する場合です（CSV の代わりにタブ区切りのファイルを読み込みます）。

```sample.ts
import * as fs from 'fs';
import * as csv from 'csv-parser';

const results = [];
fs.createReadStream('src/assets/csv/testTab.tsv')
  .pipe(csv({ separator: '\t', headers: false }))
  .on('data', (data) => results.push(data))
  .on('end', () => console.log(results));
```

|キー|内容|
|---|---|
|headers|先頭行をカラム名としてオブジェクト表示（デフォルト: true）|
|separator|区切り文字を指定|

※ 上記以外にもオプションはありますので、公式サイトなどを確認してみてください。

出力結果は、以下のようになります。

```sh:出力結果
[
  { '0': '品名', '1': '区分', '2': '単価', '3': '購入数', '4': '購入日' },
  { '0': 'にんじん', '1': '野菜', '2': '70', '3': '3', '4': '2020/10/20' },
  { '0': 'りんご', '1': '果物', '2': '150', '3': '2', '4': '2020/10/22' },
  { '0': 'みかん', '1': '果物', '2': '40', '3': '10', '4': '2020/10/30' },
  { '0': 'キャベツ', '1': '野菜', '2': '180', '3': '1', '4': '2020/11/1' },
  { '0': 'じゃがいも', '1': '野菜', '2': '50', '3': '5', '4': '2020/11/5' },
  { '0': 'バナナ', '1': '果物', '2': '300', '3': '2', '4': '2020/11/6' },
  { '0': 'メロン', '1': '果物', '2': '1500', '3': '1', '4': '2020/11/8' }
]
```

## 3. csv

次は、[csv](https://www.npmjs.com/package/csv) というライブラリです。
コードが一番シンプルになります。

### 3-1. インストール

```sh
$ npm install --save csv
```

### 3-2. サンプルコード

Stream を使用してファイルを読み込みます。

#### 3-2-1. オプション指定なし

```sample.ts
import * as fs from 'fs';
import * as csv from 'csv';

fs.createReadStream('src/assets/csv/test.csv').pipe(
  csv.parse((err, data) => console.log(data)),
);
```

＜**出力結果**＞
オプションの指定をしない場合の取得結果は、以下のように配列形式となります。

```sh:出力結果
[
  [ '品名', '区分', '単価', '購入数', '購入日' ],
  [ 'にんじん', '野菜', '70', '3', '2020/10/20' ],
  [ 'りんご', '果物', '150', '2', '2020/10/22' ],
  [ 'みかん', '果物', '40', '10', '2020/10/30' ],
  [ 'キャベツ', '野菜', '180', '1', '2020/11/1' ],
  [ 'じゃがいも', '野菜', '50', '5', '2020/11/5' ],
  [ 'バナナ', '果物', '300', '2', '2020/11/6' ],
  [ 'メロン', '果物', '1500', '1', '2020/11/8' ]
]
```

#### 3-2-2. オプション指定

オプション指定は、次のように csv.parse() メソッドの引数に指定します。
以下は、CSV をオブジェクト形式で取得するようにして、区切り文字をタブ文字と指定する場合です（CSV の代わりにタブ区切りのファイルを読み込みます）。

```sample.ts
import * as fs from 'fs';
import * as csv from 'csv';

fs.createReadStream('src/assets/csv/testTab.tsv').pipe(
  csv.parse({ columns: true, delimiter: '\t' }, (err, data) =>
    console.log(data),
  ),
);
```

|キー|内容|
|---|---|
|columns|先頭行をカラム名としてオブジェクト表示（デフォルト: false）|
|delimiter|区切り文字を指定|

※ 上記以外にもオプションはありますので、公式サイトなどを確認してみてください。

出力結果は、以下のようになります。

```sh:出力結果
[
  {
    '品名': 'にんじん',
    '区分': '野菜',
    '単価': '70',
    '購入数': '3',
    '購入日': '2020/10/20'
  },
  {
    '品名': 'りんご',
    '区分': '果物',
    '単価': '150',
    '購入数': '2',
    '購入日': '2020/10/22'
  },
  # 中略
]
```

## 4. fast-csv

最後に [fast-csv](https://www.npmjs.com/package/fast-csv) です。

### 4-1. インストール

```sh
$ npm install --save fast-csv
```

### 4-2. サンプルコード

Stream を使用してファイルを読み込みます。

#### 4-2-1. オプション指定なし

```sample.ts
import * as fs from 'fs';
import { parse } from '@fast-csv/parse';

const rows = [];
fs.createReadStream('src/assets/csv/test.csv')
  .pipe(parse())
  .on('error', (error) => console.error(error))
  .on('data', (row) => rows.push(row))
  .on('end', () => console.log(rows));
```

＜**出力結果**＞
オプションの指定をしない場合の取得結果は、以下のように配列形式となります。

```sh:出力結果
[
  [ '品名', '区分', '単価', '購入数', '購入日' ],
  [ 'にんじん', '野菜', '70', '3', '2020/10/20' ],
  [ 'りんご', '果物', '150', '2', '2020/10/22' ],
  [ 'みかん', '果物', '40', '10', '2020/10/30' ],
  [ 'キャベツ', '野菜', '180', '1', '2020/11/1' ],
  [ 'じゃがいも', '野菜', '50', '5', '2020/11/5' ],
  [ 'バナナ', '果物', '300', '2', '2020/11/6' ],
  [ 'メロン', '果物', '1500', '1', '2020/11/8' ]
]
```

#### 4-2-2. オプション指定

オプション指定は、次のように parse() メソッドの引数に指定します。
以下は、CSV をオブジェクト形式で取得するようにして、区切り文字をタブ文字と指定する場合です（CSV の代わりにタブ区切りのファイルを読み込みます）。

```sample.ts
import * as fs from 'fs';
import { parse } from '@fast-csv/parse';

const rows = [];
fs.createReadStream('src/assets/csv/testTab.tsv')
  .pipe(parse({ delimiter: '\t', headers: true }))
  .on('error', (error) => console.error(error))
  .on('data', (row) => rows.push(row))
  .on('end', () => console.log(rows));
```

|キー|内容|
|---|---|
|headers|先頭行をカラム名としてオブジェクト表示（デフォルト: false）|
|delimiter|区切り文字を指定|

※ 上記以外にもオプションはありますので、公式サイトなどを確認してみてください。

出力結果は、以下のようになります。

```sh:出力結果
[
  {
    '品名': 'にんじん',
    '区分': '野菜',
    '単価': '70',
    '購入数': '3',
    '購入日': '2020/10/20'
  },
  {
    '品名': 'りんご',
    '区分': '果物',
    '単価': '150',
    '購入数': '2',
    '購入日': '2020/10/22'
  },
  # 中略
]
```

## さいごに

以上です。
私もどれを使うか迷いましたが、コードが一番簡略になる [csv](https://www.npmjs.com/package/csv) ライブラリを使おうかと思います。