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

4.2.7 までは「インチキが効いた」、てことか…。

Tabulator のドキュメントが不完全かもしらんな。4.3.0 を堺に、どうも既に意図せず「continued jQuery support」が動作しなくなってる。4.4.3 までと以降とでエラーの種類は違えど、どちらにしても動かない。issue tracker はみてないけれど、きっと皆素直に「新スタイル」に書き換えてるのであろう。なので「マジメにマイグレート」しよう、と。ドキュメントに書かれてる通りに書き直せばいいということで:

ついでにやや拡張も加えた ver 3
  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」が非推奨警告が初出になる。)例によってページが重くなるんで、今回はこれだけのネタとしとく。



Related Posts