「Tabulator 3.3.2 → 3.5.3 → 4.0.5 → …」乗り換え、兼「声優世代表のおとも」

いつもながらの「どっちが本題?」ネタである。

「ワークシートでやりたいようなデータ集計的作業」が、結構な割合で「結果は html で」が一番楽、てのも、もうちょっと選択肢の幅が広がらんかなぁ、とは思う。たとえば「さっさと始める」のには、MS Excel や LibreOffice、Google の Sheets がね、早いことは早いんだ、だけれどもさ。入り口が「データの収集」からである場合、つまり、「自由利用可能なオープンデータがダウンロード可能」という状況でないケースだと、「どっちみちスクレイパから始めねばならんので python みたいな強力なツールを使いたくて、そうであるならば、出力は Excel / LibreOffice 的なのにこだわる必要はなく、であれば、めっさ使いやすい Tabulator な html がよくね?」てなる、てこと。

そんなわけで。どんなわけだ?

wikipedia 的まとめの見解だと、ここ二年以内での「声優ブーム」に特に名前は付いていないようなのだが、誰もが感じている通り、今絶賛進行中なのは「アフター鬼滅」の声優ブームである。鬼滅ブームの一部要因はコロナ禍だろうけど、それでもコンテンツそのものに爆発力があったのは間違いなく、そして結果として今「札幌の名を冠すればラーメンが売れる」がごとく、地上波テレビのラテ欄には「声優」の文字が、まぁ並ぶ並ぶ。声優が出てなかろうが、鬼滅の刃では重要な役どころでないのに「鬼滅声優」と謳えば「視聴率うなぎのぼる」んであろうか、この言葉を見ない日がないのではないかというくらい、頻繁に登場するワードになった。これってなんだかなぁ、と…、このムーブメントを苦々しくみているか、というと、ワタシは別にそうでもなくて、むしろ声優が積極的に顔出ししてくれるのを楽しんでいる。

露骨なフジテレビとは違って、テレビ朝日は今の「アフター鬼滅」ブームとは無関係に声優バラエティをやってた(主にお願いランキングで)ので、きっとワタシのようなタイプでなくてもそんなに悪い印象は持ってないだろう。その流れの「声優パーク建設計画VR部」が面白い。その中でもさらに「声優世代表を作ろう」のコーナーが楽しい。こういうのと、いろんなアニメのラジオとかオーディオコメンタリーとかとセットで楽しむとなお良い。

このコーナーで作っている「声優世代表」自体がまだまだ小さいてこともあるし、そもそも「世代表」は年功序列でないので、「30歳年上の後輩」みたいな関係性の面白さについては、個々の出演者発言があれば面白いが、それがなかった場合はなかなか気付かない。そういう関係性について一瞥出来る表が欲しいぞと:


例によってだが「Open Frame」で画面いっぱい使って遊んでほしい。

最初に言ったように、「python で html を吐き出すスクリプト」として作った:

ver 1
  1 # -*- coding: utf-8 -*-
  2 # require: python 3, bs4
  3 """
  4 「wikipedia声優ページ一覧」(など)を入力として、およそ世代関係を把握しやすい
  5 html テーブルを生成する。
  6 
  7 該当アーティストが情報を公開しているかどうかと wikipedia 調べがすべて一致する
  8 かどうかは誰も保証出来ない。正しい公開情報が真実である保証もない。という、元
  9 データそのものについての注意はあらかじめしておく。そして、wikipedia における
 10 ペンディング扱い(「要出典」など)やその他補足情報はこのスクリプトは無視している
 11 ので、「確実かそうでないか」がこの道具の結果だけでは判別出来ないことにも注意。
 12 (wikipedia 本体を読んでいる限りは、正しくない可能性がまだ残っている場合、
 13 そうであるとわかることが多い。)
 14 
 15 ワタシの目下の目的が「声優世代表のおとも」なので、「生年月日非公開/不明」が困る。
 16 なので、wikipedia が管理しようとしている「活動期間」を補助的に使おうと考えた。
 17 これが活用出来る場合は「生誕年=活動開始-20年くらい」みたいな推測に使える。
 18 
 19 この情報の精度には、当たり前ながらかなりのバラつきがあるし、要約の仕方も統一感
 20 がない。たとえば「田中ちえ美」の声優活動は最低でも「サクラクエスト」の2017年に
 21 始まっているが、当該ページ執筆者が「声優アーティスト活動開始」の定義に基いて
 22 「音楽活動の活動期間は2021年-」と記述してしまっていて、かつ、声優活動としての
 23 開始を記述していない。ゆえにこの情報だけを拾うと「田中ちえ美の活動期間は2021年-」
 24 であると誤って判断してしまいかねない。そもそもが「サクラクエスト」にてキャラク
 25 ターソングを出しているので、定義次第では「音楽活動の活動期間は2021年-」も誤って
 26 いる。また、「2011年」のように年を特定出来ずに「2010年代」と要約しているページ
 27 も多く、これも結構使いにくい。
 28 
 29 ので、「テレビアニメ」などの配下の「2012年」みたいな年ごとまとめ見出しの最小値を、
 30 活動期間の情報として補助的に拾ってる。
 31 
 32 活動期間は年齢の推測にも使えるけれど、「声優世代表のおとも」として考える場合は、
 33 生年月日と活動期間の両方が既知でこその面白さがある。たとえば黒沢ともよ、宮本侑芽、
 34 諸星すみれ、浪川大輔などの子役出身者の例。あるいは逆に「遅咲き」と言われる役者。
 35 """
 36 import io
 37 import os
 38 import sys
 39 import tempfile
 40 import shutil
 41 import ssl
 42 import re
 43 import json
 44 import urllib.request
 45 from urllib.request import urlretrieve as urllib_urlretrieve
 46 from urllib.request import quote as urllib_quote
 47 
 48 
 49 import bs4  # require: beutifulsoup4
 50 
 51 
 52 __MYNAME__, _ = os.path.splitext(
 53     os.path.basename(sys.modules[__name__].__file__))
 54 #
 55 __USER_AGENT__ = "\
 56 Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
 57 AppleWebKit/537.36 (KHTML, like Gecko) \
 58 Chrome/91.0.4472.124 Safari/537.36"
 59 _htctxssl = ssl.create_default_context()
 60 _htctxssl.check_hostname = False
 61 _htctxssl.verify_mode = ssl.CERT_NONE
 62 https_handler = urllib.request.HTTPSHandler(context=_htctxssl)
 63 opener = urllib.request.build_opener(https_handler)
 64 opener.addheaders = [('User-Agent', __USER_AGENT__)]
 65 urllib.request.install_opener(opener)
 66 #
 67 
 68 
 69 _urlretrieved = dict()
 70 
 71 
 72 def _urlretrieve(url):
 73     if url in _urlretrieved:
 74         return _urlretrieved[url]
 75 
 76     def _gettemppath(s):
 77         tmptopdir = os.path.join(tempfile.gettempdir(), __MYNAME__)
 78         if not os.path.exists(tmptopdir):
 79             os.makedirs(tmptopdir)
 80         import hashlib, base64
 81         ep = base64.urlsafe_b64encode(
 82             hashlib.md5(s.encode("utf-8")).digest()
 83         ).partition(b"=")[0].decode()
 84         return os.path.join(tmptopdir, ep)
 85 
 86     cachefn = _gettemppath(url)
 87     if os.path.exists(cachefn):
 88         res = cachefn
 89     else:
 90         res, _ = urllib_urlretrieve(url, filename=cachefn)
 91     _urlretrieved[url] = res
 92     return res
 93 
 94 
 95 def _from_wp(actorpagename):
 96     baseurl = "https://ja.wikipedia.org/wiki/"
 97     pn = urllib_quote(actorpagename, encoding="utf-8")
 98     fn = _urlretrieve(baseurl + pn)
 99 
100     result = {"wikipedia": actorpagename, "名前": [actorpagename.partition("_")[0]]}
101     with io.open(fn, "r", encoding="utf-8") as fi:
102         soup = bs4.BeautifulSoup(fi.read(), features="html.parser")
103         try:
104             categos = [
105                 a.text
106                 for a in soup.find("div", {"id": "mw-normal-catlinks"}).find_all("a")]
107         except Exception:
108             return result
109         try:
110             trecords = iter(
111                 soup.find("table", {"class": "infobox"}).find("tbody").find_all("tr"))
112         except Exception:
113             return result
114         tr = next(trecords)
115         result["名前"] += [
116             re.sub(r"\[[^\[\]]+\]", "", sp.text)
117             for sp in tr.find("th").find_all("span")]
118 
119         actst = float("inf")
120         for tr in trecords:
121             th, td = tr.find("th"), tr.find("td")
122             if not th or not td:
123                 continue
124             k = th.text.replace("\n", "").replace(
125                 "誕生日", "生年月日").replace("生誕", "生年月日")
126             v = ""
127             if td:
128                 v = re.sub(
129                     r"\[[^\[\]]+\]", "", td.text.replace("\n", "")).strip()
130                 if k == "活動期間":
131                     m = re.search(r"(\d+)年\s*\-", v)
132                     if m:
133                         actst = min(float(m.group(1)), actst)
134                     continue
135                 elif k == "生年月日":
136                     m1 = re.search(r"\d+-\d+-\d+", v)
137                     m2 = re.search(r"(\d+)月(\d+)日", v)
138                     if m1:
139                         v = m1.group(0)
140                     elif m2:
141                         v = "0000-{:02d}-{:02d}".format(
142                             *list(map(int, m2.group(1, 2))))
143                     else:
144                         v = ""
145             result[k] = v
146         for dtt in [
147                 dt.text
148                 for dt in soup.find_all("dt") if re.match(r"\d+年", dt.text)]:
149             actst = min(float(re.search(r"(\d+)年", dtt).group(1)), actst)
150         try:
151             result["actst"] = "{:04d}".format(int(actst))
152         except OverflowError:
153             result["actst"] = "0000"
154         if not result.get("性別"):
155             if any([("男性" in c or "男優" in c) for c in categos]):
156                 result["性別"] = "男性"
157             elif any([("女性" in c or "女優" in c) for c in categos]):
158                 result["性別"] = "女性"
159     return result
160 
161 
162 if __name__ == '__main__':
163     def _yrgrp(ymd, actst):
164         y = 0
165         if ymd:
166             y, _, md = ymd.partition("-")
167             y = int(y)
168             if y and md < "04-02":
169                 y -= 1
170         return list(
171             map(lambda s: s.replace("0000", "????"),
172                 ["{:04d}".format(y), actst, ymd]))
173 
174     actorpages = list(
175         map(lambda s: s.strip(),
176             io.open("wppagenames.txt", encoding="utf-8").read().strip().split("\n")))
177     result = []
178     for a in actorpages:
179         inf = _from_wp(a)
180         g = _yrgrp(inf.get("生年月日", ""), inf.get("actst"))
181         result.append((
182             g[0], g[1], g[2],
183             inf.get("性別", ""),
184             ("<a href='https://ja.wikipedia.org/wiki/{pn}' target=_blank>{pn}</a>".format(
185                 pn=inf["wikipedia"])),
186             ", ".join(inf["名前"]),
187             inf.get("出生地", "-"),
188             inf.get("出身地", "-")))
189     result.sort()
190     with io.open("actor_basinf.html", "w", encoding="utf-8") as fo:
191         coln = [
192             "生誕年度", "活動開始年?", "生年月日",
193             "性別", "wikipedia", "名前",
194             "出生地", "出身地"
195             ]
196         print("""\
197 <html>
198 <head jang="ja">
199 <meta charset="UTF-8">
200 <link href="https://cdnjs.cloudflare.com/ajax/libs/tabulator/3.5.3/css/tabulator_site.min.css" rel="stylesheet">
201 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
202 <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
203 <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/tabulator/3.5.3/js/tabulator.min.js"></script>
204 <!--
205 {}
206   -->
207 </head>""".format(__doc__), file=fo)
208         print("""\
209 <body>
210 <div id="actor_basinf"></div>
211 <script>
212 function _dt2int(dt) {
213     function _pad(n) {
214         let ns = "" + Math.abs(n);
215         if (ns.length === 1) {
216             ns = "0" + ns;
217         }
218         return ns;
219     }
220     return parseInt(dt.getFullYear() + _pad(dt.getMonth() + 1) + _pad(dt.getDate()));
221 }
222 var nowi = _dt2int(new Date());
223 function _calcage(cell, formatterParams) {
224     var v = cell.getValue().replace(new RegExp("\-", "g"), "");
225     if (v.startsWith("????")) {
226         return "";
227     }
228     v = parseInt(v);
229     return "" + parseInt((nowi - v) / 10000);
230 }
231 
232 
233 $("#actor_basinf").tabulator({
234     "height": "800px",
235     "tooltips": true,
236     "columns": [
237         {
238             "field": "生誕年度",
239             "title": "生誕年度",
240             "headerFilter": "input",
241         },
242         {
243             "field": "活動開始年?",
244             "title": "活動開始年?",
245             "headerFilter": "input",
246         },
247         {
248             "field": "生年月日",
249             "title": "生年月日",
250         },
251         {
252             "field": "生年月日",
253             "formatter": _calcage,
254             "headerTooltip": "存命の場合は年齢そのもの。亡くなっている方の場合は「生きていれば~歳」。 "
255         },
256         {
257             "field": "性別",
258             "title": "性別",
259             "headerFilter": "input",
260         },
261         {
262             "field": "名前",
263             "title": "名前",
264             "headerFilter": "input",
265         },
266         {
267             "field": "wikipedia",
268             "title": "wikipedia",
269             "headerFilter": "input",
270             "formatter": "html",
271         },
272         {
273             "field": "出生地",
274             "title": "出生地",
275         },
276         {
277             "field": "出身地",
278             "title": "出身地",
279         }
280 
281   ], 
282   "layout": "fitColumns"
283 });
284 actor_basinf_data = """, file=fo)
285         json.dump(
286             [dict(zip(coln, row)) for row in result],
287             fo, ensure_ascii=False, indent=4)
288         print("""
289 $("#actor_basinf").tabulator("setData", actor_basinf_data);
290 </script>
291 </html>
292 """, file=fo)

スクリプトの入力にしている「wppagenames.txt」は wikipedia のページ名リストのテキストファイルで、たとえば:

 1 KENN
 2 Machico
 3 M・A・O
 4 Pile
 5 TARAKO
 6 くまいもとこ
 7 たかはし智秋
 8 たてかべ和也
 9 てらそままさき
10 キートン山田
11 チョー_(俳優)
12 ファイルーズあい
13 七海ひろき
14 七瀬彩夏
15 三ツ矢雄二
16 三上枝織
17 三宅健太
18 三木眞一郎
19 三森すずこ
20 三瓶由布子
21 三石琴乃
22 上坂すみれ
23 上村祐翔
24 上田燿司
25 上田瞳
26 上田麗奈
27 下地紫野
28 下山吉光
29 下崎紘史
30 下野紘
31 中井和哉
32 中原麻衣
33 中尾隆聖
34 中島唯_(声優)
35 中島由貴_(声優)
36 中村悠一
37 中村繪里子
38 中田譲治

てな具合。ワタシの目的が声優についてなのでそうしてるけれど、様式が似てるものなら何相手でも通用する。

かなり昔にワタシのサイトで Tabulator をはじめて紹介した際に使ってたのは 3.3.2 付近のバージョンだったようだ。cdnjs にあげられてるものから、ひとまず「以前の知識のままどうやら使えそう」な 3.5.3 でまずは書いてみた、てこと。

これでオールオッケーならばそれだけなんだけれど、最新のドキュメントでちら見えてしまった「regex フィルタ」がね、これ使えればハッピーなのでは、と思うわけだ。上に貼り付けたバージョンではたとえば「2000年度生まれ」とか「2000年代度生まれ」のフィルタは(likeなので)問題なく出来るのだけれど、「1998~2002年度生まれ」みたいなフィルタリングは出来ない。カスタムフィルタの自作は出来るとはいえ、新しいバージョンにならあるものを再発明するのもやだなぁ、と。ので「最新バージョンを使って regex フィルタでハッピーになりたい」、のだけれども。

どうやら 3.x から 4.x が大きなバージョンアップのようで、regex が導入されたのが 4.0 だからと「3.5.3」を「4.0.5」に置き換えるだけだと致命的なエラーによって、動作しなくなる。が、まぁなんというか「Tabulator を選んで良かった」と思うよほんと。懇切丁寧。これを読んでもなお手も足も出ない、てことは、きっとなかろう。

とりあえず、python が 2 から 3 になる際のインパクトに相当するような変更が「Removal of jQuery」に関係するもので、清く正しくにはこれに則った「新流儀」に乗り換えなければならない。このノリ、openlayers と同じ流れだな…。ただ、「昔流儀を踏襲したい」のための逃げ道が用意されてて、それが「continued jQuery Support」。これを使えば「変更は最小限で済む」ということらしい。ステキだ。(それでも100%無傷、てわけではないようだけれども。)そういうわけで、この jQuery wrapper を使いつつ「regex 使うぜ」バージョン:

ver 2
  1 # -*- coding: utf-8 -*-
  2 # require: python 3, bs4
  3 """
  4 「wikipedia声優ページ一覧」(など)を入力として、およそ世代関係を把握しやすい
  5 html テーブルを生成する。
  6 
  7 該当アーティストが情報を公開しているかどうかと wikipedia 調べがすべて一致する
  8 かどうかは誰も保証出来ない。正しい公開情報が真実である保証もない。という、元
  9 データそのものについての注意はあらかじめしておく。そして、wikipedia における
 10 ペンディング扱い(「要出典」など)やその他補足情報はこのスクリプトは無視している
 11 ので、「確実かそうでないか」がこの道具の結果だけでは判別出来ないことにも注意。
 12 (wikipedia 本体を読んでいる限りは、正しくない可能性がまだ残っている場合、
 13 そうであるとわかることが多い。)
 14 
 15 ワタシの目下の目的が「声優世代表のおとも」なので、「生年月日非公開/不明」が困る。
 16 なので、wikipedia が管理しようとしている「活動期間」を補助的に使おうと考えた。
 17 これが活用出来る場合は「生誕年=活動開始-20年くらい」みたいな推測に使える。
 18 
 19 この情報の精度には、当たり前ながらかなりのバラつきがあるし、要約の仕方も統一感
 20 がない。たとえば「田中ちえ美」の声優活動は最低でも「サクラクエスト」の2017年に
 21 始まっているが、当該ページ執筆者が「声優アーティスト活動開始」の定義に基いて
 22 「音楽活動の活動期間は2021年-」と記述してしまっていて、かつ、声優活動としての
 23 開始を記述していない。ゆえにこの情報だけを拾うと「田中ちえ美の活動期間は2021年-」
 24 であると誤って判断してしまいかねない。そもそもが「サクラクエスト」にてキャラク
 25 ターソングを出しているので、定義次第では「音楽活動の活動期間は2021年-」も誤って
 26 いる。また、「2011年」のように年を特定出来ずに「2010年代」と要約しているページ
 27 も多く、これも結構使いにくい。
 28 
 29 ので、「テレビアニメ」などの配下の「2012年」みたいな年ごとまとめ見出しの最小値を、
 30 活動期間の情報として補助的に拾ってる。
 31 
 32 活動期間は年齢の推測にも使えるけれど、「声優世代表のおとも」として考える場合は、
 33 生年月日と活動期間の両方が既知でこその面白さがある。たとえば黒沢ともよ、宮本侑芽、
 34 諸星すみれ、浪川大輔などの子役出身者の例。あるいは逆に「遅咲き」と言われる役者。
 35 """
 36 import io
 37 import os
 38 import sys
 39 import tempfile
 40 import shutil
 41 import ssl
 42 import re
 43 import json
 44 import urllib.request
 45 from urllib.request import urlretrieve as urllib_urlretrieve
 46 from urllib.request import quote as urllib_quote
 47 
 48 
 49 import bs4  # require: beutifulsoup4
 50 
 51 
 52 __MYNAME__, _ = os.path.splitext(
 53     os.path.basename(sys.modules[__name__].__file__))
 54 #
 55 __USER_AGENT__ = "\
 56 Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
 57 AppleWebKit/537.36 (KHTML, like Gecko) \
 58 Chrome/91.0.4472.124 Safari/537.36"
 59 _htctxssl = ssl.create_default_context()
 60 _htctxssl.check_hostname = False
 61 _htctxssl.verify_mode = ssl.CERT_NONE
 62 https_handler = urllib.request.HTTPSHandler(context=_htctxssl)
 63 opener = urllib.request.build_opener(https_handler)
 64 opener.addheaders = [('User-Agent', __USER_AGENT__)]
 65 urllib.request.install_opener(opener)
 66 #
 67 
 68 
 69 _urlretrieved = dict()
 70 
 71 
 72 def _urlretrieve(url):
 73     if url in _urlretrieved:
 74         return _urlretrieved[url]
 75 
 76     def _gettemppath(s):
 77         tmptopdir = os.path.join(tempfile.gettempdir(), __MYNAME__)
 78         if not os.path.exists(tmptopdir):
 79             os.makedirs(tmptopdir)
 80         import hashlib, base64
 81         ep = base64.urlsafe_b64encode(
 82             hashlib.md5(s.encode("utf-8")).digest()
 83         ).partition(b"=")[0].decode()
 84         return os.path.join(tmptopdir, ep)
 85 
 86     cachefn = _gettemppath(url)
 87     if os.path.exists(cachefn):
 88         res = cachefn
 89     else:
 90         res, _ = urllib_urlretrieve(url, filename=cachefn)
 91     _urlretrieved[url] = res
 92     return res
 93 
 94 
 95 def _from_wp(actorpagename):
 96     baseurl = "https://ja.wikipedia.org/wiki/"
 97     pn = urllib_quote(actorpagename, encoding="utf-8")
 98     fn = _urlretrieve(baseurl + pn)
 99 
100     result = {"wikipedia": actorpagename, "名前": [actorpagename.partition("_")[0]]}
101     with io.open(fn, "r", encoding="utf-8") as fi:
102         soup = bs4.BeautifulSoup(fi.read(), features="html.parser")
103         try:
104             categos = [
105                 a.text
106                 for a in soup.find("div", {"id": "mw-normal-catlinks"}).find_all("a")]
107         except Exception:
108             return result
109         try:
110             trecords = iter(
111                 soup.find("table", {"class": "infobox"}).find("tbody").find_all("tr"))
112         except Exception:
113             return result
114         tr = next(trecords)
115         result["名前"] += [
116             re.sub(r"\[[^\[\]]+\]", "", sp.text)
117             for sp in tr.find("th").find_all("span")]
118 
119         actst = float("inf")
120         for tr in trecords:
121             th, td = tr.find("th"), tr.find("td")
122             if not th or not td:
123                 continue
124             k = th.text.replace("\n", "").replace(
125                 "誕生日", "生年月日").replace("生誕", "生年月日")
126             v = ""
127             if td:
128                 v = re.sub(
129                     r"\[[^\[\]]+\]", "", td.text.replace("\n", "")).strip()
130                 if k == "活動期間":
131                     m = re.search(r"(\d+)年\s*\-", v)
132                     if m:
133                         actst = min(float(m.group(1)), actst)
134                     continue
135                 elif k == "生年月日":
136                     m1 = re.search(r"\d+-\d+-\d+", v)
137                     m2 = re.search(r"(\d+)月(\d+)日", v)
138                     if m1:
139                         v = m1.group(0)
140                     elif m2:
141                         v = "0000-{:02d}-{:02d}".format(
142                             *list(map(int, m2.group(1, 2))))
143                     else:
144                         v = ""
145             result[k] = v
146         for dtt in [
147                 dt.text
148                 for dt in soup.find_all("dt") if re.match(r"\d+年", dt.text)]:
149             actst = min(float(re.search(r"(\d+)年", dtt).group(1)), actst)
150         try:
151             result["actst"] = "{:04d}".format(int(actst))
152         except OverflowError:
153             result["actst"] = "0000"
154         if not result.get("性別"):
155             if any([("男性" in c or "男優" in c) for c in categos]):
156                 result["性別"] = "男性"
157             elif any([("女性" in c or "女優" in c) for c in categos]):
158                 result["性別"] = "女性"
159     return result
160 
161 
162 if __name__ == '__main__':
163     def _yrgrp(ymd, actst):
164         y = 0
165         if ymd:
166             y, _, md = ymd.partition("-")
167             y = int(y)
168             if y and md < "04-02":
169                 y -= 1
170         return list(
171             map(lambda s: s.replace("0000", "????"),
172                 ["{:04d}".format(y), actst, ymd]))
173 
174     actorpages = list(
175         map(lambda s: s.strip(),
176             io.open("wppagenames.txt", encoding="utf-8").read().strip().split("\n")))
177     result = []
178     for a in actorpages:
179         inf = _from_wp(a)
180         g = _yrgrp(inf.get("生年月日", ""), inf.get("actst"))
181         result.append((
182             g[0], g[1], g[2],
183             inf.get("性別", ""),
184             ("<a href='https://ja.wikipedia.org/wiki/{pn}' target=_blank>{pn}</a>".format(
185                 pn=inf["wikipedia"])),
186             ", ".join(inf["名前"]),
187             inf.get("出生地", "-"),
188             inf.get("出身地", "-")))
189     result.sort()
190     with io.open("actor_basinf.html", "w", encoding="utf-8") as fo:
191         coln = [
192             "生誕年度", "活動開始年?", "生年月日",
193             "性別", "wikipedia", "名前",
194             "出生地", "出身地"
195             ]
196         print("""\
197 <html>
198 <head jang="ja">
199 <meta charset="UTF-8">
200 <link href="https://cdnjs.cloudflare.com/ajax/libs/tabulator/4.0.5/css/tabulator_site.min.css" rel="stylesheet">
201 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
202 <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
203 <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/tabulator/4.0.5/js/jquery_wrapper.min.js"></script>
204 <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/tabulator/4.0.5/js/tabulator.min.js"></script>
205 <!--
206 {}
207   -->
208 </head>""".format(__doc__), file=fo)
209         print("""\
210 <body>
211 <div id="actor_basinf"></div>
212 <script>
213 function _dt2int(dt) {
214     function _pad(n) {
215         let ns = "" + Math.abs(n);
216         if (ns.length === 1) {
217             ns = "0" + ns;
218         }
219         return ns;
220     }
221     return parseInt(dt.getFullYear() + _pad(dt.getMonth() + 1) + _pad(dt.getDate()));
222 }
223 var nowi = _dt2int(new Date());
224 function _calcage(cell, formatterParams) {
225     var v = cell.getValue().replace(new RegExp("\-", "g"), "");
226     if (v.startsWith("????")) {
227         return "";
228     }
229     v = parseInt(v);
230     return "" + parseInt((nowi - v) / 10000);
231 }
232 
233 
234 $("#actor_basinf").tabulator({
235     "height": "800px",
236     "tooltips": true,
237     "columns": [
238         {
239             "field": "生誕年度",
240             "title": "生誕年度",
241             "headerFilter": "input",
242             "headerFilterFunc": "regex",
243         },
244         {
245             "field": "活動開始年?",
246             "title": "活動開始年?",
247             "headerFilter": "input",
248             "headerFilterFunc": "regex",
249         },
250         {
251             "field": "生年月日",
252             "title": "生年月日",
253         },
254         {
255             "field": "生年月日",
256             "formatter": _calcage,
257             "headerTooltip": "存命の場合は年齢そのもの。亡くなっている方の場合は「生きていれば~歳」。 "
258         },
259         {
260             "field": "性別",
261             "title": "性別",
262             "headerFilter": "input",
263         },
264         {
265             "field": "名前",
266             "title": "名前",
267             "headerFilter": "input",
268         },
269         {
270             "field": "wikipedia",
271             "title": "wikipedia",
272             "headerFilter": "input",
273             "formatter": "html",
274         },
275         {
276             "field": "出生地",
277             "title": "出生地",
278         },
279         {
280             "field": "出身地",
281             "title": "出身地",
282         }
283 
284   ], 
285   "layout": "fitColumns"
286 });
287 actor_basinf_data = """, file=fo)
288         json.dump(
289             [dict(zip(coln, row)) for row in result],
290             fo, ensure_ascii=False, indent=4)
291         print("""
292 $("#actor_basinf").tabulator("setData", actor_basinf_data);
293 </script>
294 </html>
295 """, file=fo)

これで作ったのがこれ:


regex フィルタにより、たとえば「活動開始が 1999年か2000年」でフィルタしたい場合「1999|2000」とか書けばいい。

技術的なネタとしてこれを考える場合、このあと「jQuery wrapper を使わないで新流儀に乗り換える」話と、あとはもちろん「もう 5.0 が出てるんのぞ」な話が続くべきなんだけれど、一気に書くとこのページが重くなりすぎる、ので、それは(あくまでもヤル気が続けばだけど)後日。(とりあえず、無傷なままの置き換えが 4.2.7 まで可能なのは確認した。)

あ、一応注意してほしいのは、これは「スクレイパ」なので、非常に重いタスクである、てことね。間違ったやり方でやるとインターネット世界に迷惑もかけうるし、そもそもあなたの PC 時間も結構奪う。曲がりなりにも http get な取得はキャッシュするのでその部分はそう心配しなくてもいいけれど、もしもあなたが「もっとくれぃ」と色々拡張したくなったり、作り上げた道具または結果の「公開」を考える場合は、そこらへんもっとちゃんとしてね。

技術ネタではなくて声優ネタとしては、「声優世代表」の中では浪川大輔がとても面白い扱いになってるし、諸星すみれ出演回では諸星すみれも「大御所」にされかけた。同じ流れになりそうなのがスクリプトのコメントに書いた「黒沢ともよ、宮本侑芽」なのだが、久野美咲もかなりのもので、なおかつ「黒沢ともよ、久野美咲、沢城みゆき、花澤香菜」の繋がりが非常に面白いことにはじめて気付いた。気付いたきっかけは、今放送中の「終末のワルキューレ」のオーディオコメンタリー。最近増えたね、オンエア時オーコメ。黒沢ともよ中学生、沢城みゆき大学生のときに出会った、と。これが何なのかと探してみたら、これはブラック★ロックシューター、久野美咲と花澤香菜の関係もここにあった。「黒沢ともよと久野美咲」って、「ひそねとまそたん」が初めましてではなかったんだね…。

ほかにもいろんなことがわかるんで、好きな人はじっくり眺めてニヤニヤしてくれ。



Related Posts