Edited at

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


はじめに

データを扱うとき、相手が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の意味もあるそうです。

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

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