LTSVをCSVに変換する。

この文章は[DATUM STUDIO Advent Calendar 2016](http://qiita.com/advent-calendar/2016/datumstudio)の5日目です。

今日までの4日間、DATUM STUDIO社員ではない方々が熱いコンテンツを連続で投稿くださいました。
誠にありがとうございます。

その結果、弊社の社員の特に技術者系バックグラウンドを持っていない面々が萎縮してしまって誰も記事を書いてくれなかったので、軽い内容で埋めて「Advent Calendar怖くないよ！」を訴えて行きたいと思います。[^1]

[^1]: ここまでテンプレ。

# LTSVをCSVに変換する。
さて、世の中には[はてな](http://www.hatena.ne.jp/)の人達が提唱したと思われる[LTSV(Labeled Tab-separated Values)](http://ltsv.org/)というフォーマットのファイルがあります。

こちら、タブ区切りでカラムがあり、それぞれのカラムに`:`区切りでタイトルと値が入っているという形式で、エスケープシーケンスが無いなどパースし易い事が魅力のフォーマットとなっております。

これはこれで非常に柔軟で扱い安い形式なのですが、既存のツールやサービスなどの都合によりCSVに変換したいという事が時たま起こります。[^2]

[^2]: 某クラウドサービスに乗せたりとか。

ということで、変換処理を作ったので上げておきます。

## ソース
パースが簡単なファイルなので変換処理そのものも非常にシンプルです。
ただ、ファイルフォーマットの性質上1度最後まで読んでみないと項目数がわからないというのだけが難点です。

```python:ltsv2csv.py
#! /bin/sh
# -*- coding: utf-8 -*-
""":" .

exec python "$0" ${1+"$@}
"""

import sys
import errno

def key(col):
    i = col.find(':')
    if i < 0:
        return None
    else:
        return col[:i]

def value(col):
    i = col.find(':')
    if i < 0:
        return None
    else:
        return col[i + 1:]

def key_value(col):
    i = col.find(':')
    if i < 0:
        return (None, None)
    else:
        return (col[:i], col[i + 1:])

def keys(cleanline):
    return [key(x) for x in cleanline.split('\t')]

def cleanup(line):
    if line.endswith('\r\n'):
        return line[:-2]
    if line.endswith('\n') or line.endswith('\r'):
        return line[:-1]
    return line

def allkeys(path):
    file = open(path, 'r')
    try:
        line = file.readline()
        titles = []
        while line:
            ts = keys(cleanup(line))
            for t in ts:
                if t not in titles:
                    titles.append(t)
            line = file.readline()
        return titles
    finally:
        file.close()

def quote(col):
    return '"' + col.replace('"', '""') + '"'

def escape(col):
    if '"' in col:
        return quote(col)
    if ',' in col:
        return quote(col)
    return col

def list_to_line(col_list):
    escaped = [escape(x) for x in col_list]
    return ','.join(escaped)

def row_to_line(titles, row):
    res = []
    for title in titles:
        if title in row:
            res.append(row[title])
        else:
            res.append('')
    return list_to_line(res)

def line_to_row(line):
    cleanline = cleanup(line)
    res = {}
    for col in cleanline.split('\t'):
        k, v = key_value(col)
        if k is not None:
            v = value(col)
            res[k] = v
    return res

def to_csv(inpath):
    titles = allkeys(inpath)
    with open(inpath, 'r') as i:
        try:
            sys.stdout.write(list_to_line(titles) + '\n')
            line = i.readline()
            while line:
                row = line_to_row(line)
                out = row_to_line(titles, row)
                sys.stdout.write(out + '\n')
                line = i.readline()
        except IOError as e:
            if e.errno == errno.EPIPE:
                return
            raise

if __name__ == '__main__':
    args = sys.argv
    if len(args) != 2:
        sys.stderr.write("Usage: %s </path/to/ltsv/file>" % args[0])
        sys.exit(101)
    to_csv(args[1])
```

引数で受け取ったltsvファイルを2度走査して、CSVを標準出力に吐き出します。
適度にパイプかリダイレクトしてお使いください。

ライセンスはMITにしておきます。

```text
The MIT License (MIT)
Copyright (c) 2016 KznrMR

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

似たようなツールがすぐに見つかるかなーって思ってたのに、なかなか見つからなくて衝撃でした。
唯一見つかった[kor](https://github.com/ksss/kor)っていうツールも途中から増えた項目をうまく扱えなさそう？
(私が理解していないだけだと思われます。)

明日こそは弊社の誰かが軽い記事を書いてくれると信じて。
(もちろん社外の関係者の記事も大歓迎です。)

おつきあいいただきありがとうございました。