tsvファイルの入出力を簡単にする技

# はじめに

データを扱うとき、相手がExcelだったりすること多いですよね。
Excelとの読み書きをするときには、tsv形式（タブ区切り）にすると便利です。
ただし、Excelはデフォルトでは文字コードがcp932なので気をつけましょう。

というわけで、各言語でのtsvファイルの扱いについて

# 方針

ヘッダを読み込み、それをキーにしてハッシュのリストにして扱うと簡単です。

出力時も、出力する項目をリストにしておけば、簡単です。

仕様変更にも柔軟に対応できます。

## Python

### ファイル読み込み

```python
with open(filepath, encoding='cp932') as f:
    header = next(f)[:-1].split('\t')
    
    data = [dict(zip(header, l[:-1].split('\t'))) for l in f]
```

filepathは読み込みファイルのパスです。
dataにキーがヘッダで辞書形式の配列で格納されます。

### ファイル書き込み

```python
header = '出力したい項目/区切り'.split('/')
with open(filepath, 'w', encoding='cp932') as wf:
    wf.write('\t'.join(header) + '\n')
    for d in data:
        wf.write('\t'.join(str(d[k]) for k in header) + '\n')
```

filepathは読み込みファイルのパスです。

### ちょっと便利な使い方

pythonの場合はformatが強力なので、こんな風に使うと便利です。

```python
for d in data:
    print('{品目}:{単価}円'.format(**d))
```
説明なくても何をしているのか伝わるかと思います。

データを加工する際にも、加工した結果を辞書に追加していけばわかりやすいかと思います。

## Javascript(nodejs v10)

ファイル1本にまとめてしまいます。
cp932を読み込むのは結構大変で本質的ではないので省略。
v10の追加機能として、fs.promisesを利用しています。
pythonと比較して、zipとかdictとかの便利な関数がないため、読みにくいかと思いますが。

```js
const fs = require('fs');

// 読み込み
async function readData(filepath) {
    const text = await fs.promises.readFile(filepath, 'utf8');
    const lines = text.split('\n').map(l => l.split('\t'));
    const header = lines.shift();
    
    return lines.map(
        l => header.reduce(
            (a, c, i, s) => Object.assign(a, {[c]:l[i]}), {})
    );
}

// 書き込み
const writeData = (header, data, path) => fs.promises.writeFile(
    path, 
    header.join('\t') + '\n'
        + data.map(d => header.map(h => d[h]).join('\t')).join('\n') + '\n'
);

(async () => {
    const data = await readData('assets.tsv');

    var d = data[0];
    console.log(d);
    console.log(`${d.id}:${d.name}`);
    
    await writeData('id/name/route'.split('/'), data, 'assets_js.tsv');
})();
```

## go編

ファイル1本にまとめてしまいます。
これもcp932を処理するのにはパッケージ追加が必要なので省略。
map系がないため、全部forで書いています。長いですね。

```go
package main

import (
    "encoding/csv"
    "fmt"
    "io"
    "os"
    "strings"
)

func readData(path string) []map[string]string {
    fp, err := os.Open(path)
    if err != nil {
        panic(err)
    }
    defer fp.Close()
    reader := csv.NewReader(fp)
    reader.Comma = '\t'
    reader.LazyQuotes = true
    header, err := reader.Read()
    if err != nil {
        panic(err)
    }
    data := []map[string]string{}
    for {
        record, err := reader.Read()
        if err == io.EOF {
            break
        } else if err != nil {
            panic(err)
        }
        d := map[string]string{}
        for i, h := range header {
            d[h] = record[i]
        }
        data = append(data, d)
    }
    
    return data
}

func writeData(header []string, data []map[string]string, path string) {
    fp, err := os.Create(path)
    if err != nil {
        panic(err)
    }
    defer fp.Close()
    fp.Write(([]byte)(strings.Join(header, "\t") + "\n"))
    for _, d := range data {
        outdata := []string{}
        for _, h := range header {
            outdata = append(outdata, d[h])
        }
        fp.Write(([]byte)(strings.Join(outdata, "\t") + "\n"))
    }
}

func main() {
    data := readData("assets.tsv")
    fmt.Println(data[0])
    writeData(strings.Split("id/name/route", "/"), data, "assets_go.tsv")
}
```

## 余談その１

javascriptとgoの読み込み用ファイルassets.tsvはjqで作成しました。
jqはコマンドラインでjsonを扱える便利なツールです。
元データは、仮想通貨のapiから作成しました。

```bash
curl https://api.cryptowat.ch/assets | jq -r  '.result|((.[0]|keys_unsorted),map([.[]])[]|@tsv)' > assets.tsv
```

## 余談その２

tsv形式って言い方はあんまり一般的ではないみたいです。
Excelでも、タブ区切り出力は.txtになります。

csv形式も、カンマ区切りと思っていましたが、character-separated valuesの意味もあるそうです。

https://www.msng.info/archives/2015/12/tsv-or-csv.php

まあ、伝わる人には、区切り記号がはっきりしやすいように、「tsv形式です！」というほうがいいと思います。