LTSVって何?
LTSVとはLabeled Tab-separated Valuesというテキストフォーマットです。
id:naoyaさんのLTSV FAQ - LTSV って何? どういうところが良いの? - naoyaのはてなダイアリーがわかりやすいです。
例えば、
url:http://qiita.com<TAB>title:some title<TAB>tab:LTSV
みたいな、
key1:value1<TAB>key2:value2<TAB>…
という形式で、httpdのログとかに便利だよね、と話題になっています。
LTSVの何が嬉しいの?
- 特別なパーサがなくても簡単にparseできる。Pythonだと
{ elem[0]:elem[1] for elem in [ f.split(":",2) for f in s.split("\t")]}
とか - 行指向なので、fulentdやMapReduceなどいろいろ扱いやすい
- ltsviewなんかを使うと人にも見やすくできる
- ltsv.orgでいろんな言語の実装もある
LTSVで可変長フィールドを扱いたいがどうすればいいんだ
可変長のフィールドがあるとき、どう表現すればいいか悩ましいのです。
例えば、qiitaのtagのようなものであったりとか、HTMLからテキスト取り出して文単位で並べたい場合とか。後者だと順序も保持したくなる。
今回は、TSVの時代にこういう形式で保存されているデータがあったとします。
url<TAB>date<TAB>sentence<TAB>sentence<TAB>…
具体的にはこんな感じ
http://qiita.com<TAB>05/Feb/2013:15:34:47 +0000<TAB>foo<TAB>bar
Idea 1) 可変長のフィールドを1フィールドとしてtabをエスケープする
YAMLで表現するとこうなる。
url: http://qiita.com
date: 05/Feb/2013:15:34:47 +0000
sentence: foo\\tbar
こういうLTSVで保持する。
url:http://qiita.com<TAB>date:05/Feb/2013:15:34:47 +0000<TAB>sentence:foo\\tbar
難点。エスケープが仕様で決まっておらず、parseも面倒になる。
エスケープはあえて仕様に入れていないそうです。
ただ、Rubyのltsv gemとかでもescapeしていたりするので、そのうちデファクトスタンダードができないかなーとも思います。
後、httpdのログにはタブは出ないそうなのですが、一般のテキストになるとタブ出てくる可能性が高いので、そこをどう割り切るかが必要です。
このアイデアの亜種として、CSVのようにダブルクォートでクォートするとかもありますが、これも面倒です。
Idea 2) 同じkeyが出てきたら、配列に入れる
YAMLで表現するとこういう表現に落とせるようにする。
url: http://qiita.com
date: 05/Feb/2013:15:34:47 +0000
sentence:
- foo
- bar
こういうLTSVで保持する。
url:http://qiita.com<TAB>date:05/Feb/2013:15:34:47 +0000<TAB>sentence:foo<TAB>sentence:bar
難点。parseが面倒くさくなる。
あと、なんか同じ行に同じラベルがあるのが気持ち悪い、かも。
Ideat 2') 同じkeyが出てきたら、Hashのvalueにタブ区切りで追加する
多分、これはltsv読み込み時にはエスケープされたタブしかないという前提になるんだろう。
入力ltsvはこうする。
label1:value1\tlabel2:value2-1\tlabel2:value2-2\tlabel3:value3
すると、これくらいでparseできる。
str="label1:value1\tlabel2:value2-1\tlabel2:value2-2\tlabel3:value3"
values = str.split("\t").map{|f| f.split(":", 2)}.inject({}){|h,k| h.has_key?(k[0]) ? h[k[0]] <<"\t" << k[1] : h[k[0]]=k[1]; h }
values["label2"] => "value2-1\tvalue2-2"
dumpするときもこれくらい。
values.inject([]){|s, k| s << (k[1].empty? ? k.join(":") : k[1].split("\t").map{|v2| "#{k[0]}:#{v2}" }) ; s }.join("\t")
多少面倒だけど、なんとかなるレベルか。
Idea 3) label名に番号を付加する
url:http://qiita.com<TAB>date:05/Feb/2013:15:34:47 +0000<TAB>sentence1:foo<TAB>sentence2:bar
これはイヤだ。。。
Idea 4) 可変長のフィールドの行を分ける
url:http://qiita.com<TAB>date:05/Feb/2013:15:34:47 +0000<TAB>sentence:foo<TAB>
url:http://qiita.com<TAB>date:05/Feb/2013:15:34:47 +0000<TAB>sentence:bar
オーバーヘッドが大きくなるから流石にこれはない。
で、どうするか
個人的には、Idea2の方向で拡張かIdea2'だと自分の用途には幸せになれそうという気がしています。
ただ、「特別なパーサがなくてもいいよね」という旨味が減ることが悩み。
この方向でRubyでいうところのネストしたHashやらArrayをどうするのか?とかに進めていくつもりはありません。
お前それjsonで、とか言われそうだけどjsonを1行にしてMapReduceとかね、、、