はじめに
データを扱うとき、相手がExcelだったりすること多いですよね。
Excelとの読み書きをするときには、tsv形式(タブ区切り)にすると便利です。
ただし、Excelはデフォルトでは文字コードがcp932なので気をつけましょう。
というわけで、各言語でのtsvファイルの扱いについて
方針
ヘッダを読み込み、それをキーにしてハッシュのリストにして扱うと簡単です。
出力時も、出力する項目をリストにしておけば、簡単です。
仕様変更にも柔軟に対応できます。
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にキーがヘッダで辞書形式の配列で格納されます。
ファイル書き込み
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が強力なので、こんな風に使うと便利です。
for d in data:
print('{品目}:{単価}円'.format(**d))
説明なくても何をしているのか伝わるかと思います。
データを加工する際にも、加工した結果を辞書に追加していけばわかりやすいかと思います。
Javascript(nodejs v10)
ファイル1本にまとめてしまいます。
cp932を読み込むのは結構大変で本質的ではないので省略。
v10の追加機能として、fs.promisesを利用しています。
pythonと比較して、zipとかdictとかの便利な関数がないため、読みにくいかと思いますが。
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で書いています。長いですね。
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")
}
余談その1
javascriptとgoの読み込み用ファイルassets.tsvはjqで作成しました。
jqはコマンドラインでjsonを扱える便利なツールです。
元データは、仮想通貨のapiから作成しました。
curl https://api.cryptowat.ch/assets | jq -r '.result|((.[0]|keys_unsorted),map([.[]])[]|@tsv)' > assets.tsv
余談その2
tsv形式って言い方はあんまり一般的ではないみたいです。
Excelでも、タブ区切り出力は.txtになります。
csv形式も、カンマ区切りと思っていましたが、character-separated valuesの意味もあるそうです。
まあ、伝わる人には、区切り記号がはっきりしやすいように、「tsv形式です!」というほうがいいと思います。