いつもながらの「どっちが本題?」ネタである。
「ワークシートでやりたいようなデータ集計的作業」が、結構な割合で「結果は html で」が一番楽、てのも、もうちょっと選択肢の幅が広がらんかなぁ、とは思う。たとえば「さっさと始める」のには、MS Excel や LibreOffice、Google の Sheets がね、早いことは早いんだ、だけれどもさ。入り口が「データの収集」からである場合、つまり、「自由利用可能なオープンデータがダウンロード可能」という状況でないケースだと、「どっちみちスクレイパから始めねばならんので python みたいな強力なツールを使いたくて、そうであるならば、出力は Excel / LibreOffice 的なのにこだわる必要はなく、であれば、めっさ使いやすい Tabulator な html がよくね?」てなる、てこと。
そんなわけで。どんなわけだ?
wikipedia 的まとめの見解だと、ここ二年以内での「声優ブーム」に特に名前は付いていないようなのだが、誰もが感じている通り、今絶賛進行中なのは「アフター鬼滅」の声優ブームである。鬼滅ブームの一部要因はコロナ禍だろうけど、それでもコンテンツそのものに爆発力があったのは間違いなく、そして結果として今「札幌の名を冠すればラーメンが売れる」がごとく、地上波テレビのラテ欄には「声優」の文字が、まぁ並ぶ並ぶ。声優が出てなかろうが、鬼滅の刃では重要な役どころでないのに「鬼滅声優」と謳えば「視聴率うなぎのぼる」んであろうか、この言葉を見ない日がないのではないかというくらい、頻繁に登場するワードになった。これってなんだかなぁ、と…、このムーブメントを苦々しくみているか、というと、ワタシは別にそうでもなくて、むしろ声優が積極的に顔出ししてくれるのを楽しんでいる。
露骨なフジテレビとは違って、テレビ朝日は今の「アフター鬼滅」ブームとは無関係に声優バラエティをやってた(主にお願いランキングで)ので、きっとワタシのようなタイプでなくてもそんなに悪い印象は持ってないだろう。その流れの「声優パーク建設計画VR部」が面白い。その中でもさらに「声優世代表を作ろう」のコーナーが楽しい。こういうのと、いろんなアニメのラジオとかオーディオコメンタリーとかとセットで楽しむとなお良い。
このコーナーで作っている「声優世代表」自体がまだまだ小さいてこともあるし、そもそも「世代表」は年功序列でないので、「30歳年上の後輩」みたいな関係性の面白さについては、個々の出演者発言があれば面白いが、それがなかった場合はなかなか気付かない。そういう関係性について一瞥出来る表が欲しいぞと:
例によってだが「Open Frame」で画面いっぱい使って遊んでほしい。
最初に言ったように、「python で html を吐き出すスクリプト」として作った:
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 使うぜ」バージョン:
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 な取得はキャッシュするのでその部分はそう心配しなくてもいいけれど、もしもあなたが「もっとくれぃ」と色々拡張したくなったり、作り上げた道具または結果の「公開」を考える場合は、そこらへんもっとちゃんとしてね。
技術ネタではなくて声優ネタとしては、「声優世代表」の中では浪川大輔がとても面白い扱いになってるし、諸星すみれ出演回では諸星すみれも「大御所」にされかけた。同じ流れになりそうなのがスクリプトのコメントに書いた「黒沢ともよ、宮本侑芽」なのだが、久野美咲もかなりのもので、なおかつ「黒沢ともよ、久野美咲、沢城みゆき、花澤香菜」の繋がりが非常に面白いことにはじめて気付いた。気付いたきっかけは、今放送中の「終末のワルキューレ」のオーディオコメンタリー。最近増えたね、オンエア時オーコメ。黒沢ともよ中学生、沢城みゆき大学生のときに出会った、と。これが何なのかと探してみたら、これはブラック★ロックシューター、久野美咲と花澤香菜の関係もここにあった。「黒沢ともよと久野美咲」って、「ひそねとまそたん」が初めましてではなかったんだね…。
ほかにもいろんなことがわかるんで、好きな人はじっくり眺めてニヤニヤしてくれ。