ブラウザ上でCSVファイルをパースする


# 背景

CSVファイルのアップロード機能を考えます。
一番シンプルな方法は、`form`タグに`input type="file"`タグを入れて、指定したファイルをPOSTします。

例えば、[HTMLタグ/フォームタグ/ファイルの送信欄を作る - TAG index Webサイト](http://www.tagindex.com/html_tag/form/input_file.html) です。

# 問題
CSVファイルのバリデーションにサーバーへの送信が必須です。
POST前にチェックできれば、ユーザが意図しないCSVファイルを指定した場合に、迅速なフィードバックを返せます。
便利なユーザーインタフェースが作れます。

また、CSVファイルの仕様は

- エスケープ
- 区切り文字
- 改行文字
- ファイルのエンコーディング

を考慮する必要があります。面倒です。
自分では、パーサーを書きたくありません。

# 解決策
[npm](https://www.npmjs.com/)パッケージとして公開されているCSVパーサーを利用します。
今回は、[csv-parser](https://github.com/mafintosh/csv-parser)を使って、ブラウザ上でCSVファイルをパースします。

csv-parserは

```csv
id,first_name,last_name,email,gender,ip_address
1,Paul,Hill,phill0@yahoo.com,Male,213.190.9.236
```

のようなCSVを

```json
{
  "id": "1",
  "first_name": "Paul",
  "last_name": "Hill",
  "email": "phill0@yahoo.com",
  "gender": "Male",
  "ip_address": "213.190.9.236"
}
```

JavaScriptオブジェクトに変換します。

[browserify](http://browserify.org/)を使うと、npmパッケージをブラウザで動くJavaScriptに変換できます。

# 実装

## ユーザが指定したファイルを取得

`input type="file"`は指定ファイルが変わると`change`イベントを早出します。
`input`の`files`属性から[File](https://developer.mozilla.org/ja/docs/Web/API/File)オブジェクトが取得できます。

```js
document.querySelector('input')
  .addEventListener('change', (e) => {
    const file = e.target.files[0] // ファイルが取れます。
  })
```

イベントオブジェクトのtarget属性には、`input`要素（イベントが発生した要素）が入っています。



## ファイルの中身を読み込む
File APIの[readAsText](https://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsText)関数を使って読み込みます。

readAsText関数は引数にFileオブジェクトを指定できます。

```js
const reader = new FileReader
reader.readAsText(file)
```

このとき第二引数でエンコードを指定できます。
WindowsのExeclで作ったCSVファイルを読み込むときは`'shift-jis'`を指定すると、文字化けを回避できます。

ファイルの読み込みは非同期で行われます。読み込みが終わると`onload`ハンドラーが呼ばれます。
読み込んだ結果は`input`の`result`プロパティから取得できます。

```js
const reader = new FileReader
reader.onload = (e) => {
  const content = e.target.result // ファイルの中身が取れます。
}
```

## CSVのパース
csv-parserを使います。
csv-parserは[Stream API](http://nodejs.jp/nodejs.org_ja/api/stream.html)を実装しています。

### データ入力
入力データを`write`関数で渡します。

文字列か[Buffer](http://nodejs.jp/nodejs.org_ja/api/buffer.html)オブジェクトが指定できます。
readAsTextの読み取り結果は文字列です。そのまま指定できます。

```js
const csv = require('csv-parser')
const stream = csv()

stream.write(e.target.result)
```

[readAsArrayBuffer](https://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsArrayBuffer)を利用してArrayBufferオブジェクトを指定した場合はBuffuerに変換する必要があります。

```js
const csv = require('csv-parser')
const stream = csv()

stream.write(new Buffer(e.target.result))
```

### データ取得
変換結果は`data`イベントハンドラーで取得します。

```js
const csv = require('csv-parser')
const stream = csv()

stream.on('data',(data) => {
  const object = data // objectにパースされた値が取得できます。
})
```


## ソースコード


### index.js
```index.js
const csv = require('csv-parser')
const stream = csv()

stream.on('data', function(data) {
  console.log(data)
})

document.querySelector('input')
  .addEventListener('change', (e) => {
    const reader = new FileReader
    reader.onload = (e) => {
      stream.write(e.target.result)
    }

    if (e.target.files[0]) {
      const file = e.target.files[0]
      reader.readAsText(file)
    }
  })
```

### index.html
```index.html
<input type="file">
<script src="bundle.js" charset="utf-8"></script>
```

## 実行
```
npm init -y
npm i -D browserify csv-parser
node_modules/browserify/bin/cmd.js index.js bundle.js
```

index.htmlを開きます。

## 動作イメージ
![スクリーンショット 2016-06-24 14.05.04.png](https://qiita-image-store.s3.amazonaws.com/0/15964/e8b0adf5-3cfd-18f8-9989-16e285a3921a.png "スクリーンショット 2016-06-24 14.05.04.png")


# その他
テスト用のCSVファイルは[Mockaroo - Random Data Generator | CSV / JSON / SQL / Excel](https://www.mockaroo.com/)で作りました。


# 参考
- [JavaScript でのローカル ファイルの読み込み - HTML5 Rocks](http://www.html5rocks.com/ja/tutorials/file/dndfiles/)

- [JavaScriptでShift_JISのcsvを読み込む（文字化け対策） | #GUNMAGISGEEK](http://shimz.me/blog/javascript/4525)

- [【JavaScript & HTML5 】文字コードに関係なくテキストファイルを正しく読み込む - のんびり猫プログラマの日常](http://catprogram.hatenablog.com/entry/2015/04/23/171157)