4.2.7 までは「インチキが効いた」、てことか…。
Tabulator のドキュメントが不完全かもしらんな。4.3.0 を堺に、どうも既に意図せず「continued jQuery support」が動作しなくなってる。4.4.3 までと以降とでエラーの種類は違えど、どちらにしても動かない。issue tracker はみてないけれど、きっと皆素直に「新スタイル」に書き換えてるのであろう。なので「マジメにマイグレート」しよう、と。ドキュメントに書かれてる通りに書き直せばいいということで:
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 inf.get("出身地", "-")))
190 result.sort()
191 with io.open("actor_basinf.html", "w", encoding="utf-8") as fo:
192 coln = [
193 "生誕年度", "活動開始年?", "生年月日",
194 "性別", "wikipedia", "名前",
195 "血液型",
196 "出生地", "出身地"
197 ]
198 print("""\
199 <html>
200 <head jang="ja">
201 <meta charset="UTF-8">
202 <link href="https://cdnjs.cloudflare.com/ajax/libs/tabulator/4.9.3/css/tabulator_site.min.css" rel="stylesheet">
203 <!--tabulator自身はjqueryに依存しなくなった。なお、今となっては蛇足だが、tabulator 4.2.3 と jquery 3.6.0 の組み合わせは問題なさげだった。
204 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
205 <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
206 jquery_wrapperからも卒業して走り出す。
207 <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/tabulator/4.9.3/js/jquery_wrapper.min.js"></script>
208 -->
209 <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/tabulator/4.9.3/js/tabulator.min.js"></script>
210 <!--
211 新スタイルに書き換えてしまえば、4.3.0 どころか 4.9.3 まで一気にアップグレード出来るようだ。
212 (無論機能をちょっとしか使ってないからこそ。やってることによっては小さな影響が出ることはあろう。)
213 -->
214
215 <!--
216 {}
217 -->
218 </head>""".format(__doc__), file=fo)
219 print("""\
220 <body>
221 <div id="actor_basinf"></div>
222 <script>
223 function _dt2int(dt) {
224 function _pad(n) {
225 let ns = "" + Math.abs(n);
226 if (ns.length === 1) {
227 ns = "0" + ns;
228 }
229 return ns;
230 }
231 return parseInt(dt.getFullYear() + _pad(dt.getMonth() + 1) + _pad(dt.getDate()));
232 }
233 var nowi = _dt2int(new Date());
234 function _calcage(cell, formatterParams) {
235 var v = cell.getValue().replace(new RegExp("\-", "g"), "");
236 if (v.startsWith("????")) {
237 return "";
238 }
239 v = parseInt(v);
240 return "" + parseInt((nowi - v) / 10000);
241 }
242
243
244 var actor_basinf_data = """, file=fo)
245 json.dump(
246 [dict(zip(coln, row)) for row in result],
247 fo, ensure_ascii=False, indent=4)
248 print("""
249 var table = new Tabulator("#actor_basinf", {
250 "height": "800px",
251 "tooltips": true,
252 "columns": [
253 {
254 "field": "生誕年度",
255 "title": "生誕年度",
256 "headerFilter": "input",
257 "headerFilterFunc": "regex",
258 },
259 {
260 "field": "活動開始年?",
261 "title": "活動開始年?",
262 "headerFilter": "input",
263 "headerFilterFunc": "regex",
264 },
265 {
266 "field": "生年月日",
267 "title": "生年月日",
268 },
269 {
270 "field": "生年月日",
271 "formatter": _calcage,
272 "headerTooltip": "存命の場合は年齢そのもの。亡くなっている方の場合は「生きていれば~歳」。 "
273 },
274 {
275 "field": "性別",
276 "title": "性別",
277 "headerFilter": "input",
278 },
279 {
280 "field": "名前",
281 "title": "名前",
282 "headerFilter": "input",
283 "headerFilterFunc": "regex",
284 },
285 {
286 "field": "wikipedia",
287 "title": "wikipedia",
288 "headerFilter": "input",
289 "headerFilterFunc": "regex",
290 "formatter": "html",
291 },
292 {
293 "field": "血液型",
294 "title": "血液型",
295 "headerFilter": "input",
296 "headerFilterFunc": "regex",
297 },
298 {
299 "field": "出生地",
300 "title": "出生地",
301 "headerFilter": "input",
302 "headerFilterFunc": "regex",
303 },
304 {
305 "field": "出身地",
306 "title": "出身地",
307 "headerFilter": "input",
308 "headerFilterFunc": "regex",
309 }
310 ],
311 "layout": "fitColumns",
312 "data": actor_basinf_data
313 });
314 </script>
315 </html>
316 """, file=fo)
初期データをコンストラクタオプションに渡す、というノリの都合、「旧スタイル」で書いてた順序と変えねばならんてのが、ソースコードの差異把握の邪魔にはなるけれど、別に修正作業は大変なことはない。まぁあんましサボろうとせずに、ちゃんとした方がいい、てことだわなぁ、きちんと腰さえ上げればこうして「なんだよ簡単じゃねーか」なんてことになるんだから。ともあれ、この新バージョンで出来たやつ:
実はさらに一気に 5.0.6 に置き換えることも割とすぐに出来るのだが、ワタシの単純なのでも「無傷」でなかったので、一個一個着実に、な。(ワタシのやつだと「tooltips」が非推奨警告が初出になる。)例によってページが重くなるんで、今回はこれだけのネタとしとく。