Tabulator で csv 「的」データを入力にしたい、兼「声優世代表のおとも」(5) – 「データの正規化」のはなしの少し続き

Tabulator のネタって、読み取り専用・オフラインに限るとそんなに書くことないのよね。なのでここ数日やってた一連に関しては、もう Tabulator ネタでめぼしいのは尽きた。

Tabulator そのものについてなんかオモロイことはないかなぁと思ったのだけれど、「ワタシにはお初だけど別に 4.2 時点でも使えた」という2つを辛うじて見つけられただけかな。ひとつは List Lookup Formatterで、もう一つが tooltip に function を使う用法ね。後者は「Column Defaults」の持ち物なので、「4.x → 5.0」で修正が必要なヤツね。

そういうわけで、「Tabulator ネタとしてオモロイこと」はもうないんだけれど、「Tabulator で csv 「的」データを入力にしたい、兼「声優世代表のおとも」(4) – 「データの正規化」のはなし」の話について、もうちょっと話を進めようかと。

先に注意しておかければならない。「(4) – 「データの正規化」のはなし」」の後半の話からと、今から続ける話というのはこれは、「html + js としてのテキストとしてのサイズを小さくする」ことだけを徹底する、という話であって、あまり可搬性のある話ではない。徹底的にやってみれば得るものがあると思ってやってみたら、そういうのが少しはあったので、書いておこう、とは思ったけれど、少なくともそうして紹介するテクニックは、「そういうアイディアもある」として頭の片隅におくに留めて欲しい。「保守性」をかなり犠牲にしていたりと、どちらかといえば、というか、かなりバッドノウハウとも言えるような、まぁ強引なアプローチだ。少なくとも大喜びで無批判に飛びつくのだけはやめて欲しい。

そもそも「javascript とか Tabulator の学習」目当てでは話を続けられるネタではあるけれど、「(4) – 「データの正規化」のはなし」」における当初目的の「Wordpress に data-uri の形にして貼り付ける際の WordPress データベース列サイズ制限にひっかからないように小さくする」に関し、既にもう「zip はとっくに何してもダメ、bzip2 では列も増やしつつ 4000レコード超えたあたりで最高圧縮でもダメになり、既に(5100レコードくらいで)7zip の最高圧縮でしか措置できない」ところまで来てて、7zip の最高圧縮に頼るんだったらテキストサイズを小さくすることにはあんまし意味がなくなってる。ので、ワタシ的にももはや、単なる興味の範疇。いや、「偏執狂」というべきかもな。

では、おさらいから。まずは「C/S アプローチをとらないオフラインで、PC ローカルなデータファイルの取り込み」を考えるに当たり、「csvファイルみたいな形式にこだわらんでええやん」:

1 <script type="text/javascript" src="actor_basinf_data.js"></script>

から話が始まる。この割り切りが「スタティックなデータにこだわる必要がなく、javascript による処理を含んで良い」ということになり、なので actor_basinf_data.js は「データを用意する処理」を書く場所となった:

1 var actor_basinf_data_csv = /* ... */;

今ワタシが目標としているのが「actor_basinf_data.js のファイルサイズを小さくする」ことになるので、「…」をどう書くのか、というのが、今回の話の主題。「(4) – 「データの正規化」のはなし」」の後半で、ここを「Template Literals の仕組みに頼った正規化」によって小さくする、という話までした:

1 let V = [
2 "女性声優",
3 "男性声優",
4 ]
5 
6 var actor_basinf_data_csv = `
7 0,1940?,????0620,,1,,,"瀬水 暁,せみ あける",,64,あける。,,,,,声優,${V[1]},0,,,,,
8 `;

これ以上どうしようか、というところからの話ね。

真っ先に出来る簡単なことは、「スタティックなデータにこだわる必要がなく、javascript による処理を含んで良い」を地で行くこれ:

1 let V = `女性声優
2 男性声優`.split("\n");
3 
4 var actor_basinf_data_csv = `
5 0,1940?,????0620,,1,,,"瀬水 暁,せみ あける",,64,あける。,,,,,声優,${V[1]},0,,,,,
6 `;

「javascript データ型としてリストの表現としてコーディングする」のではなくて、「「行志向のテキスト」を分解してリストにする、という処理を書く」わけだね。これは「一個余分な処理が増えるので性能劣化になる」のだよ、そこは当たり前。だけれども「この js ファイルを小さくする」という点にだけ着目するなら、最低でもおよそ「レコード数 x 3バイト」(引用符2つとカンマ)の削減になる。(今ワタシが手持ちのデータだと、V に相当する変数が 220 くらいあって、なので 220 * 3 = 660バイトの削減になっている。)

こういうのを「やってみる」ことで何かを得るのは大事で、何が得られるかって、「この性能劣化は許容出来るかどうか?」をちゃんと体感出来る、てことね。こういうトレードオフがあることって、やってみないとわからんこともあるしね。(最後に実際に動くやつを貼り付けるので、性能についてはご自身で体感してみて欲しい。)

今回の話は一貫してそうなんだけれど、「「”V[0]”」より「”V[100]”」の表現の方がかさばる」というのはこれは、あくまでもテキスト表現としての話であって、「数値としての 0 と 100 ではメモリを使う量が違う」というのは「あるときは真、あるときは偽」。プログラミングにおいては 0 と 100 でのメモリ使用量が同じであるケースの方が多くお目にかかるであろう。いわゆる C 系の言語における「32 bit integer」で表現しているならば、0 だろうが 100 だろうがこれは 32bit、4バイトであって、同じである。一方で、4bit で数値を扱おうとするならこれは 0 は 4bit で済むが、100 は 4bit に収まらない、つまり 0 と 100 はメモリ使用量が違う。そして「テキスト表現で」も同じく 0 と 100 のメモリ使用量が違う。

ともあれ、「「”V[0]”」より「”V[100]”」の表現の方がかさばる」ということは、上でやったようなルックアップテーブルは「より多く使われるもの順」にするほどテキストとしてのサイズが小さいということになる。つまりこれ:

1 var text = `
2 ${V[19920]},${V[33]}
3 ${V[19920]},${V[12]}
4 ${V[19920]},${V[555]}
5 ${V[19920]},${V[981]}
6 ${V[19920]},${V[66]}
7 `;

よりも

1 var text = `
2 ${V[0]},${V[33]}
3 ${V[0]},${V[12]}
4 ${V[0]},${V[555]}
5 ${V[0]},${V[981]}
6 ${V[0]},${V[66]}
7 `;

のように参照が多いものほど添字が小さくなるようにしたい、ということだ。

ワタシのケースでは「html + js を Python を使ってオフラインで生成する」というアプローチなので、こうした手心は Python 処理に表現することになる。だいたいこんな感じ:

 1 from collections import Counter
 2 
 3 vers = Counter()
 4 # v はスクレイプ対象の wikipedia ページから得たなんらかテキスト
 5 for b in set(re.split(r"\s*[、→/()]\s*", v)):
 6     vers[b] += 1  # 参照数を数える。
 7 
 8 # 参照数が多い順のリストに変換する
 9 vers = [v for v, c in sorted(vers.items(), key=lambda it: -it[1])]
10 
11 # ルックアップテーブルの出力
12 print("""\
13 let V = `{}`.split("\\n");""".format("\n".join(vers))
14 
15 # 目的の csv データの出力
16 def _replace(col, i):
17     # vers アイテムに合致する部分文字列を、「${V[0]}」のようなテキストに置換
18     return col
19 print("""\
20 var actor_basinf_data_csv = `""")
21 print([_replace(col, i) for i, col in enumerate(row)) for row in result])

「「${V[0]}」のようなテキストに置換」部分も処理順序が繊細な結構大事な処理だけど省略。ここでの本題は「参照順に並び替える」てとこ。

ワタシが今直接扱ってる 5100 人くらいのデータでこれをすると、ルックアップテーブルのレコード数も3桁に及ぶので、参照数が非常に多い(つまりテキスト中に何度も登場する)ものの添字が3桁の場合と1桁の場合とでは、最終的な js ファイルのサイズが結構違う。例えば平均1000回現れるものが100あるとして、その添字が3桁だと 300K、1桁なら 100K になるので、200K の削減になる。

大事なのでもう一度繰り返すけれど、これは「テキスト表現だから」だよ。

次の話は「変数に追いやる方がおいしいのか逆効果になるのか」という話。ワタシのケースではこの目的の html + js を Python で生成しているので、実現そのものは Python でやっているが、ここでしたい話は「考え方」の話。個人的に結構考えづらくて苦労したもんで。

つまり、「変数にして追い出す前」がこうだとして:

1 var text = `
2 aaaaaaa
3 aaaaaaa
4 bbbbbbb`;

これは圧縮できたのかどうか?:

1 let V = `aaaaaaa
2 `.split("\n");
3 var text = `
4 ${V[0]}
5 ${V[0]}
6 bbbbbbb`;

無論答えは「否」で、「let V = `」部分と「`.split(“\n”);」部分を無視したとしてもなお「元の 1.5 倍」に膨張している。この計算よ、追い出してはいけない判定のための。簡単なはずなのに、全然出てこなくて参った。上で書いた Python スクリプトの延長で、だいたいこんな感じ:

変数に追い出すかどうかの判断に関係する部分
 1     def _arrange_vers(dest):
 2         dest = list(
 3             sorted(((v, c, len(v.encode("utf-8")))
 4                     for v, c in dest.items()),
 5                    key=lambda t: (-t[1], t[0])))
 6         dest = [
 7             v for i, (v, c, l) in enumerate(dest)
 8             # 文字列として「${V[?]}」より小さかったり参照数が少ないのは意味ない。
 9             if (l - len("${V[%d]}" % i)) * c > l]
10         return dest
11     vers = _arrange_vers(vers)

これを捻出するのにワタシはえれぇ時間がかかっちまったが、出来上がったこれを理解するのはそんなに難しくはなかろう。「バイト列として」のために文字列長を encode したもので測ってるのでそのぶん煩雑にはなってるが、「ルックアップテーブルに追加することになるデータのサイズと、参照のための「${V[num]}」という文字列が登場数ぶん、のトータルサイズが、変数参照する前のサイズを下回らないと意味ない」という計算ね。

これを正しく計算することそのものは「小さくしようと思ったのにかえって大きくなってる」のを防ぐためのものであって、「小さくしようと試みる範囲を限定する」ということ。なのだけれど、扱っているデータ量が少ない場合は結構な効果でも、よほど極端な性質のデータでない限りは「平均的には変数に追い出して小さくなる方が多い(かえって大きくなるケースが埋もれる)」ので、この改善が削減出来るサイズは、おもったほどは多くない。まぁでも、「気分は良い」のだけどね、「悪影響がないようにする」ということだからさ。誤差でも「逆効果がになってるのが一定数ある」のはあんまり気持ちがよくない、てことよ。

もうひとつ。たとえば「女性声優・ナレーター・アナウンサー・司会者・歌手」というみたいなのがあり、このなかの「女性声優・ナレーター」というまとまりで頻出、みたいなケースをどう考えるかの問題について考えてみる。

「・」をセパレータと考えて「女性声優」「ナレーター」「アナウンサー」「司会者」「歌手」に分解するのが逆効果なのは既にワタシは把握している。メカニズムはともかく体験として。けれども「女性声優・ナレーター・アナウンサー・司会者・歌手」というまとまりがそうそう現れないことだって、これは誰しもがわかること。「女性声優・ナレーター」という切り出しは出来ないか、と。

処理に落とし込むのが結構めんどいんだよねぇ:

変数に追い出す分解単位のバリエーション
 1 # シンプルな re.split とともに以下で分解したものも使う:
 2 def _split(seprgx, s, minlen=1):
 3     rx = re.compile(seprgx)
 4     result = []
 5     pos, st = 0, 0
 6     for m in rx.finditer(s):
 7         e = m.span()[0]
 8         ns = s[st:e]
 9         if len(ns.encode("utf-8")) >= minlen:
10             result.append(ns)
11             st = m.span()[1]
12         pos = m.span()[1]
13     if st < len(s):
14         result.append(s[st:])
15     return result
16 
17 # vers は上で説明した Counter。
18 sep1 = r"\s*[、→/()]+\s*"
19 sep2 = r"\s*[、→/()・]+\s*"
20 for b in set(filter(None, re.split(sep1, v) + _split(sep2, v, 8))):
21     vers[b] += 1

「女性声優・ナレーター・アナウンサー・司会者・歌手」を前から順に拾い上げていくので、必ずしも期待のものとなるとは限らない(たとえば「ナレーター・アナウンサー」は切り出さない)が、ワタシのケースだとこの単純な考え方だけでも結構効くみたい。まぁ「声優と思しき人物だけをかき集めたい」として収集してるページ群相手なので、フリーテキストだとはいっても、そこそこ皆同じワードが集中するんだよね、文字列長がかなり長いものですら。

まぁ今後どこかに使えるアプローチかと言うと至極自信はないけれど、とにかく目下の「「オレの js」のサイズ削減」の足しには、かなりなる。ので採用しとく。

なお、「ナレーター・アナウンサー」も切り出すようにすること自体は上の _split でやってることの応用問題ではあるのだけれど、これは組合せ爆発問題との格闘になり、想像するよりは難しいと思う。まぁそこまではやらなくていいかな、きっと。

最後に、さらに過激なことも考えてみる。といっても要は最初の「javascript としてのリストをコーディングするのではなく文字列を分解してリストにする処理を書く」というのと思想は同じくするんだけれど、何が極端かと言うと、「${V[1000]}」という参照の手段の冗長性に目をつける、て点。つまり「オレオレテンプレート」としてたとえば「{V1000}」の状態のテキストにして、これを「${V[1000]}」に置換してから改めて「${V[1000]}」を変数置換として解釈させる、という二段階にしたい、ってわけだ。

ワタシはまだまだ javascript/ECMAScript に対する全貌把握が欠けているんだけれど、さっとみた感じだと、「Template Literal を eval」するしか思い当たらない。こんな感じ:

1 var actor_basinf_data_csv = `
2 0,1940?,????0620,,1,,,"瀬水 暁,せみ あける",,64,あける。,,,,,声優,{V1},0,,,,,`;
3 
4 actor_basinf_data_csv = eval(
5     "`" + actor_basinf_data_csv.replace(/\{([A-Za-z]+)(\d+)\}/g, "$${$1[$2]}") + "`");

当たり前だがこれによる js サイズの削減量は結構なもので、「わーい、へったへったぁ」と割と大喜び出来る。

もちろんこういうのは「表現の衝突」の問題はつきまとう(つまり「{V[100]}というテキストがほんとに置換前に存在してたらどうすんだ問題」)。ちゃんとしなきゃダメ、ではあるんだけれど、扱うデータによってはかなりぞんざいにやっても問題にならないこともある。ワタシのケースがまさにそうで、「声優事務所」だのが入力テキストであるときに「{V100}って事務所があったらどうしよう?」と考えるのはまぁ結構考えすぎだ。

最後の極端なやつは、考えて実際に html + js を直接テキストエディタで編集してみて削減量を知る、というところまではやったけれど、「声優世代表のおとも」には取り込んでいない。そもそも最初に言ったように、ここに貼り付けるためにはそれによる削減量でもまだダメで「7zip に頼るしかない」ことは確定してて、であれば無理に取り込む理由はないのよね。その上で「保守性」を考えて、取り込みを躊躇してしまったの。

まぁ「オレが使えればいい」という雑なスクリプト相手に「保守性もクソもなかろう」といえばその通りなんだけれど、いくら「オレが使えればいい」だろうと「やりながら整理していこう」とは考えるわけで。

てなわけで、声優世代表のおとも ver 17:

スクリプトはかなりムゴいことになってて、自分でも制御しにくくなってるが、まぁやりながら整理していくわ。html + javascript + Tabulator にしか興味がない人は、html と js だけ見てもらえればいいよ、Python の方は、読むの疲れるかもしれんし。(動かすのも時間かかるしよ。)

ちなみに「actor_basinf_data.js がデータそのものではない」がゆえに「人間が読みにくい」のだが、「ブラウザで開いてみないとわからない」ことが不都合なのであれば、node.js に頼るといい。console.log を末尾に追加した状態で node に渡せば、展開した状態の「データ」を読むことが出来る。



Related Posts