「的」を消すつもりだった。
これの直接の続きで、「さぁ、ほんとの csv るぞ」と思ってた。
当然「むき身の csv ファイルそのものをごにょろう」と思うならば、html を箱に使う以上は input type=”file” と FileReader、もしくは ajax 的アプローチを採るかの二択になるわけだけれど、前者は「ファイルを選択」という行為をユーザに行わせる部分を省略出来ず、ワタシが使いたいものとはかけ離れてしまうし、後者は CORS 問題なんぞも絡んでくるのでそもそもがうっとうしい。
どうしたもんかと思ってたのだが、よくよく考えたら、「javascript や css なら直接取り込めるんだよな」と。つまりワタシのケースの場合なら「データセットアップ部分」だけ独立させて:
1 var actor_basinf_data_csv = `
2 0,1950年代,????????,,1,,"水島 晋, みずしま すすむ",,日本,,,,,,声優、俳優
3 0,1960年代,????0111,,1,,"高田 竜二, たかだ りゅうじ",,日本,,,,,,
4 ... (snip) ...
5 2005,2011,20050601,,2,,"稲葉菜月, いなば なつき",,日本,,,,,,テレビ・映画、声優、女優
6 2005,2013,20050427,,2,,"原涼子, はら すずこ",,神奈川県横須賀市,,,スペースクラフトジュニア,,,女優(子役)、CM、テレビドラマ、声優、テレビアニメ、映画
7 2005,2014,20051004,,2,,"遠藤璃菜, えんどう りな",,東京都,,148cm,テアトルアカデミー,,,CM、テレビドラマ、女優(元子役)、声優、テレビアニメ、映画
8 2012,2017,20120822,,2,,"中村優月, なかむら ゆづき",,東京都,,122cm,テアトルアカデミー,,,声優、子役`;
9 var actor_basinf_data = Array();
10 actor_basinf_data_csv.trim().split("\n").forEach(function (row, i) {
11 let r = jQuery.csv.toArray(row);
12 let bymd = r[2];
13 if (bymd) {
14 bymd = bymd.slice(0, 4) + "-" + bymd.slice(4, 6) + "-" + bymd.slice(6, 8);
15 }
16 let dymd = r[3];
17 if (dymd) {
18 dymd = dymd.slice(0, 4) + "-" + dymd.slice(4, 6) + "-" + dymd.slice(6, 8);
19 }
20 actor_basinf_data.push({
21 "by": r[0], /* 生誕年度 */
22 "as": r[1], /* 活動開始年? */
23 "bymd": bymd, /* 生年月日 */
24 "dymd": dymd, /* 没年月日 */
25 "gen": r[4], /* 性別 */
26 "wp": r[5], /* wikipedia */
27 "nm": r[6], /* 名前 */
28 "bld": r[7], /* 血液型 */
29 "bor": r[8], /* 出生地・出身地 */
30 "nn": r[9], /* 愛称 */
31 "tw": r[10], /* 身長/体重 */
32 "bel": r[11], /* 事務所・レーベル */
33 "fst": r[12], /* デビュー作 */
34 "tea": r[13], /* 共同作業者 */
35 "occ": r[14], /* 職業/職種/ジャンル */
36 })
37 })
それを取り込めばよいではないか、と:
1 <html>
2 <head jang="ja">
3 <meta charset="UTF-8">
4 <link href="https://cdnjs.cloudflare.com/ajax/libs/tabulator/5.0.6/css/tabulator_site.min.css" rel="stylesheet">
5 <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/tabulator/5.0.6/js/tabulator.min.js"></script>
6 <script type="text/javascript" src="https://oss.sheetjs.com/sheetjs/xlsx.full.min.js"></script>
7
8
9 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
10 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-csv/1.0.21/jquery.csv.js"></script>
11
12 <!-- なにも「csv ファイル」にこだわらなくてもいいじゃないか -->
13 <script type="text/javascript" src="data.js"></script>
14
15 <!-- ... -->
16 </head>
17 <body>
18 <!-- ... -->
19 <div id="actor_basinf"></div>
20 <!-- ... -->
21 <script>
22 /* ... */
23 var table = new Tabulator("#actor_basinf", {
24 /* ... */
25 "data": actor_basinf_data /* data.js でセットアップしたデータ */
26 });
27 </script>
28 </html>
絶対に csv ファイルでなければならないという理由はワタシにはないので、結局「的」のままで良いではないか、てことね。
そういうわけで、ほかにもこまごま改善しつつ:
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 なお、日本語版では不明となっているのに英語版には記載されていることがあり、この
37 スクリプトはそれも拾うが、これがアーティスト自身の公開非公開選択の方針とはより
38 ズレる可能性があることには一応注意。(日本語版では判明情報を本人方針尊重で隠して
39 いるのに、英語版ではそれが届いていない、のようなこと。)
40 """
41 import io
42 import os
43 import sys
44 import tempfile
45 import shutil
46 import ssl
47 import re
48 import csv
49 import urllib
50 import urllib.request
51 from urllib.request import urlretrieve as urllib_urlretrieve
52 from urllib.request import quote as urllib_quote
53 from urllib.request import unquote as urllib_unquote
54
55
56 import bs4 # require: beutifulsoup4
57
58
59 __MYNAME__, _ = os.path.splitext(
60 os.path.basename(sys.modules[__name__].__file__))
61 #
62 __USER_AGENT__ = "\
63 Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
64 AppleWebKit/537.36 (KHTML, like Gecko) \
65 Chrome/91.0.4472.124 Safari/537.36"
66 _htctxssl = ssl.create_default_context()
67 _htctxssl.check_hostname = False
68 _htctxssl.verify_mode = ssl.CERT_NONE
69 https_handler = urllib.request.HTTPSHandler(context=_htctxssl)
70 opener = urllib.request.build_opener(https_handler)
71 opener.addheaders = [('User-Agent', __USER_AGENT__)]
72 urllib.request.install_opener(opener)
73 #
74
75
76 def _str(s):
77 return s.encode("cp932", errors="xmlcharrefreplace").decode("cp932")
78
79
80 _urlretrieved = dict()
81
82
83 def _urlretrieve(url):
84 if url in _urlretrieved:
85 return _urlretrieved[url]
86
87 def _gettemppath(s):
88 tmptopdir = os.path.join(tempfile.gettempdir(), __MYNAME__)
89 if not os.path.exists(tmptopdir):
90 os.makedirs(tmptopdir)
91 import hashlib, base64
92 ep = base64.urlsafe_b64encode(
93 hashlib.md5(s.encode("utf-8")).digest()
94 ).partition(b"=")[0].decode()
95 flat = os.path.join(tmptopdir, ep)
96 p1, p2 = ep[0], ep[1:]
97 d1 = os.path.join(tmptopdir, p1)
98 if not os.path.exists(d1):
99 os.makedirs(d1)
100 d2 = os.path.join(tmptopdir, p1, p2)
101 if os.path.exists(flat) and not os.path.exists(d2):
102 os.rename(flat, d2)
103 return d2
104
105 cachefn = _gettemppath(url)
106 if os.path.exists(cachefn):
107 res = cachefn
108 else:
109 try:
110 res, _ = urllib_urlretrieve(url, filename=cachefn)
111 except Exception:
112 print(url, repr(urllib_unquote(url)))
113 raise
114 _urlretrieved[url] = res
115 return res
116
117
118 def _norm(s):
119 for f, r in (("(", "("), (")", ")"), ("・", "・"), (" ", " ")):
120 s = s.replace(f, r)
121 return s.strip()
122
123
124 def _try_get_actst(doc, actst, actst_rough):
125 # 活動開始をどうにか特定したい、ので、ヘディングで「1999年」としてるならまずはそれ、
126 # また、ヘディングが「出演」の配下にあるリスト項目の「~(1999年、…)」みたいな羅列
127 # を拾いたい、と思うわけだが、「html なのでこれは」なので(特に後者が)ひたすらに
128 # めんどいのよ。
129 validymin = 1900 # 日本の声優第一号の生誕から得るのがよかろうが、わからんのでてきとう
130 for stt, sta in (("dt", {}), ("span", {"class": "mw-headline"}), ("td", {}), ("b", {})):
131 # ヘディングから。<b>をヘディングにしてるのがいて迷惑…。
132 for dtt in [
133 dt.text
134 for dt in doc.find_all(stt, sta)
135 if not re.search(r"(生年月日|誕生日|生誕)", dt.parent.text)]:
136 m = re.match(r"(\d{4})年(代|\d+月(?:\d+日)?)?$", dtt.strip())
137 if not m:
138 continue
139 if not m.group(2) or m.group(2) != "代":
140 v = int(m.group(1))
141 if v > validymin:
142 actst = min(v, actst)
143 else:
144 actst_rough.append(m.group(0))
145 # if actst < 9999:
146 # print(re.sub(r"\s\-\s.*$", "", _str(doc.find("title").text)), actst, actst_rough)
147 if actst == 9999:
148 docs = str(doc)
149 m = re.search(r'<h2.* id="(?:主な)?(出演|活動歴).*</h2>', docs)
150 if m:
151 _, rngs = m.span()
152 m = re.search(r'<h2.*.*</h2>', docs[rngs:])
153 if not m or "案内メニュー" in m.group(0):
154 m = re.search(r'<div class="printfooter"', docs[rngs:])
155 if m:
156 rnge, _ = m.span()
157 sect = bs4.BeautifulSoup(docs[rngs:rngs + rnge], features="html.parser")
158 s = sect.text
159 for ys in list(
160 re.findall(r"\(.*?(\d{4}[年.]代?)", s)) + list(
161 re.findall(r"\(.*?(\d{4})[~-]\d{4}年\b", s)):
162 if ys[-1] == "代":
163 actst_rough.append(ys)
164 else:
165 v = int(re.sub(r"\D", "", ys))
166 if v > validymin:
167 actst = min(v, actst)
168 print(re.sub(r"\s\-\s.*$", "", _str(doc.find("title").text)), actst, actst_rough)
169 if actst == 9999:
170 # ほんとにこれは最後の最後の手段。出典の参照日や出典の発行日は当該人物が
171 # 世に知られるよりも前の日付であることがありえない、て考え。さすがにいくら
172 # 著名人でも
173 # 南野誰蔵は2015年3月17日に誕生予定である[1]2015年3月12日
174 # なんてのはなかろう。絶対とはいわんけど。(というか実在したとしてもそれは
175 # wikipedia の本来のポリシーとして出典としては認められないだろうよ、だから
176 # そのような記述がなされたとしてもいずれ消される。)
177 rl = doc.find_all("div", {"class": "reflist"})
178 if not rl:
179 rl = doc.find_all("ol", {"class": "references"})
180 if rl:
181 for rle in rl:
182 for ys in re.findall(r"(\d{4})年(?:\d{1,2}月)?(?:\d{1,2}日)?", rle.text):
183 v = int(ys)
184 if v > validymin:
185 actst = min(v, actst)
186 print(re.sub(r"\s\-\s.*$", "", _str(doc.find("title").text)), actst, actst_rough)
187 return actst, actst_rough
188
189
190 def _from_abspara(doc):
191 for p in doc.find_all("p"):
192 ptxt = re.sub(r"\[[^\[\]]+\]", "", p.text)
193 m = re.match(r"""[^。、]+\(([^。]+)\)は、?[^。、]+?の([^。]+)。""", ptxt)
194 if m:
195 return m.group(1), re.sub(r"である$", "", m.group(2))
196 return "", ""
197
198
199 def _citation_notice(doc):
200 for mts in doc.find_all("div", {"class": "mbox-text-span"}):
201 m = re.search(r"出典が([^。]+)。", mts.text)
202 if m:
203 if "全くありません" in m.group(1):
204 return -2
205 else:
206 return -1
207 return 0
208
209
210 def _redirect(doc):
211 e = doc.find("span", {"class": "mw-redirectedfrom"})
212 if e:
213 e = e.find("a", {"class": "mw-redirect"})
214 if e:
215 # 「~より転送」の「~」はワレが指定したページであり、
216 # 知りたいのは「左遷先」すなわち開いているページ名。
217 # つまりこれは違う: e.attrs["title"].strip()
218 e = doc.find("head").find("link", {"rel": "canonical"})
219 if e:
220 return urllib_unquote(
221 re.sub(r"^.*/([^/]+)$", r"\1", e.attrs["href"]))
222 e = doc.find("head").find("link", {"title": "編集"})
223 if e:
224 return urllib_unquote(
225 re.sub(r"^.*/index\.php\?title=(.*)&action=edit$", r"\1", e.attrs["href"]))
226 return ""
227
228
229 def _from_wp_jp(actorpagename):
230 baseurl = "https://ja.wikipedia.org/wiki/"
231 pn = urllib_quote(actorpagename, encoding="utf-8")
232 fn = _urlretrieve(baseurl + pn)
233
234 result = {"wikipedia": actorpagename, "名前": [actorpagename.partition("_")[0]]}
235 with io.open(fn, "r", encoding="utf-8") as fi:
236 soup = bs4.BeautifulSoup(_norm(fi.read()), features="html.parser")
237 bd_fromabs, occ_fromabs = _from_abspara(soup)
238 result["occ_fromabs"] = occ_fromabs
239 result["citation_notice"] = _citation_notice(soup)
240 result["redirect"] = _redirect(soup)
241 if result["redirect"]:
242 print(_str(actorpagename), _str(result["redirect"]), sep=" → ")
243
244 entr = soup.find("a", {"class": "interlanguage-link-target", "lang": "en"})
245 if entr:
246 result["wikipedia_en"] = entr.attrs["href"]
247 try:
248 categos = [
249 a.text
250 for a in soup.find("div", {"id": "mw-normal-catlinks"}).find_all("a")]
251 except Exception:
252 categos = []
253 trecords = []
254 try:
255 trecords = iter(
256 soup.find("table", {"class": "infobox"}).find("tbody").find_all("tr"))
257 except Exception:
258 pass
259 if trecords:
260 tr = next(trecords)
261 if tr.find("th"):
262 tr = bs4.BeautifulSoup(
263 re.sub(r"<br\s*/?>", r"\n", str(tr)), features="html.parser")
264 for sp in tr.find("th").find_all("span"):
265 s = re.sub(r"\[[^\[\]]+\]", "", sp.text.strip())
266 if "\n" in s:
267 result["名前"].extend(s.split("\n"))
268 else:
269 result["名前"].append(s)
270
271 actst = 9999
272 actst_rough = ["9999年代"]
273 for tr in trecords:
274 th, td = tr.find("th"), tr.find("td")
275 if not th or not td:
276 continue
277 k = th.text.replace("\n", "").replace(
278 "誕生日", "生年月日").replace("生誕", "生年月日")
279 k = re.sub("身長.*$", "身長/体重", k)
280 if k in ("事務所", "レーベル"):
281 k = "事務所・レーベル"
282 elif k in ("職業", "職種", "ジャンル",):
283 k = "occ"
284 v = ""
285 if td:
286 if k in ("出生地", "出身地"):
287 td = bs4.BeautifulSoup(
288 re.sub(r'<a [^<>]*title="日本(?:の旗)?">日本(?:の旗)?</a>[・\s,、]?', r"", str(td)),
289 features="html.parser")
290 if td.find("li"):
291 td = "\n".join([li.text for li in td.find_all("li")])
292 elif td.find("br"):
293 td = bs4.BeautifulSoup(
294 re.sub(r"<br\s*/?>", r"\n", str(td)), features="html.parser").text.strip()
295 else:
296 td = td.text
297 v = re.sub(
298 r"\[[^\[\]]+\]", "\n", td).strip()
299 if k == "活動期間":
300 m = re.search(r"(\d{4})年(代)?", v)
301 if m:
302 if not m.group(2):
303 actst = min(int(m.group(1)), actst)
304 else:
305 actst_rough.append(m.group(0))
306 continue
307 elif k in ("生年月日", "没年月日"):
308 m1 = re.search(r"\d+-\d+-\d+", v)
309 m2 = re.search(r"(\d+)年(\d+)月(\d+)日", v)
310 m3 = re.search(r"(\d+)月(\d+)日", v)
311 if m1:
312 v = m1.group(0)
313 elif m2:
314 v = "{:04d}-{:02d}-{:02d}".format(
315 *list(map(int, m2.group(1, 2, 3))))
316 elif m3:
317 v = "0000-{:02d}-{:02d}".format(
318 *list(map(int, m3.group(1, 2))))
319 else:
320 v = ""
321 v = re.sub(
322 r"(、\s*)+", "、",
323 "、".join(re.sub(r"\n+", r"\n", v).split("\n")))
324 if k in ("デビュー作", "事務所・レーベル", "共同作業者",):
325 v = "、".join(list(filter(None, [result.get(k), v])))
326 elif k == "occ":
327 v = "、".join(
328 list(
329 filter(
330 None,
331 re.split(r"[、,\n]", "、".join(
332 [result.get(k, ""), v])))))
333 elif k == "身長/体重":
334 v = re.sub(r"\s*、\s*cm", " cm", v) # なんで??
335 if not (k == "身長/体重" and ("cm" in result.get(k, "") or "kg" in result.get(k, ""))):
336 result[k] = v
337 actst_orig = actst
338 actst_rough_min_orig = list(sorted(actst_rough))[0]
339 armv_orig = int(re.match(r"(\d+)", actst_rough_min_orig).group(1))
340 actst, actst_rough = _try_get_actst(soup, actst, actst_rough)
341 actst_rough_min = list(sorted(actst_rough))[0]
342 armv = int(re.match(r"(\d+)", actst_rough_min).group(1))
343 if actst_orig < 9999 and armv_orig == 9999:
344 # 「活動期間」に「~年」で明記
345 result["actst"] = "{:04d}".format(actst_orig)
346 if actst // 10 < armv // 10 and actst < actst_orig:
347 result["actst"] = "{:04d}?".format(actst)
348 elif actst_orig // 10 > armv // 10:
349 result["actst"] = actst_rough_min + "?"
350 elif actst_orig == 9999 and armv_orig < 9999:
351 # 「活動期間」に「~年代」で明記
352 result["actst"] = actst_rough_min_orig
353 if actst // 10 < armv // 10 and actst // 10 < armv_orig // 10:
354 result["actst"] = "{:04d}?".format(actst)
355 elif armv < armv_orig:
356 result["actst"] = actst_rough_min + "?"
357 elif actst_orig < 9999 and armv_orig < 9999:
358 # 「活動期間」に「~年」「~年代」双方形式明記
359 # (声優活動が 2003 年、俳優活動が 1990年代、という具合)
360 if armv_orig // 10 <= actst_orig // 10:
361 # ex. 声優活動 2003 年、俳優活動が 1990年代 -> 1990年代を採る
362 result["actst"] = actst_rough_min_orig
363 if actst // 10 < armv // 10 and actst // 10 < armv_orig // 10:
364 result["actst"] = "{:04d}?".format(actst)
365 elif armv < armv_orig:
366 result["actst"] = actst_rough_min + "?"
367 else:
368 # ex. 声優活動 2001 年、俳優活動が 2010年代 -> 2001年を採る
369 result["actst"] = "{:04d}".format(actst_orig)
370 if actst // 10 < armv // 10 and actst < actst_orig:
371 result["actst"] = "{:04d}?".format(actst)
372 elif actst_orig // 10 > armv // 10:
373 result["actst"] = actst_rough_min + "?"
374 else: # 「活動期間」記載なし
375 if armv // 10 > actst // 10:
376 result["actst"] = "{:04d}?".format(actst)
377 elif armv < 9999:
378 result["actst"] = actst_rough_min + "?"
379 if "actst" not in result:
380 result["actst"] = "0000"
381 if not result.get("生年月日"):
382 result["生年月日"] = "0000-??-??"
383 if bd_fromabs:
384 # infobox なしなのに律儀に生年月日は書いてる、てのが結構ある。
385 m = re.search(
386 r"[^、。()\n]+、(\d{4}年)?(\d+月)?(\d+日)?[^()]*",
387 bd_fromabs)
388 if m:
389 yy, mm, dd = m.group(1, 2, 3)
390 if yy:
391 yy = int(yy[:-1])
392 else:
393 yy = 0
394 if mm and dd:
395 result["生年月日"] = "{:04d}-{:02d}-{:02d}".format(
396 yy, int(mm[:-1]), int(dd[:-1]))
397 elif mm:
398 result["生年月日"] = "{:04d}-{:02d}-??".format(
399 yy, int(mm[:-1]))
400 else:
401 result["生年月日"] = "{:04d}-??-??".format(yy)
402 if not result.get("性別"):
403 if any([("男性" in c or "男優" in c) for c in categos]):
404 result["性別"] = 1
405 elif any([("女性" in c or "女優" in c) for c in categos]):
406 result["性別"] = 2
407 else:
408 result["性別"] = 0
409 else:
410 result["性別"] = 1 if ("男" in result["性別"]) else 2
411 return result
412
413
414 def _from_wp_en(result):
415 # 基本的に日本語版を信じ、欠落のものだけ英語版に頼ることにする。
416 if "生年月日" in result and "0000" not in result["生年月日"]:
417 return result
418 try:
419 fn = _urlretrieve(result["wikipedia_en"])
420 except urllib.error.HTTPError:
421 return result
422 with io.open(fn, "r", encoding="utf-8") as fi:
423 soup = bs4.BeautifulSoup(fi.read(), features="html.parser")
424 try:
425 trecords = iter(
426 soup.find("table", {"class": "infobox"}).find("tbody").find_all("tr"))
427 except Exception:
428 return result
429 for tr in trecords:
430 th, td = tr.find("th"), tr.find("td")
431 if not th or not td:
432 continue
433 k_en = th.text.strip()
434 if k_en == "Born":
435 if "生年月日" not in result or "0000" in result["生年月日"]:
436 bd = td.find("span", {"class": "bday"})
437 if bd:
438 bd = re.sub(r"\[[^\[\]]+\]", "", bd.text)
439 result["生年月日"] = bd
440 return result
441
442
443 def _from_wp(actorpagename):
444 result = _from_wp_jp(actorpagename)
445 if "wikipedia_en" in result:
446 _from_wp_en(result)
447 return result
448
449
450 if __name__ == '__main__':
451 def _yrgrp(ymd, actst):
452 y = 0
453 if ymd:
454 y, _, md = ymd.partition("-")
455 y = int(y)
456 if y and md < "04-02":
457 y -= 1
458 return [
459 y,
460 actst.replace("0000", "????"),
461 ymd.replace("0000", "????")]
462 def _names(inf):
463 # inf["名前"]
464 # 通常は
465 # 0: wikipediaページ名からあいまい回避サフィクスを取り除いた名前
466 # 1: 2のよみ
467 # 2: 姓+スペース+名
468 # ページによってこれがバラつく。0しか取れないものは結構ある。外国人名は「姓+スペース+名」
469 # に従わない。通常ケースでは 2、1 をとって 0 を捨てるのが使いやすい。
470 nms = inf["名前"]
471 if len(nms) == 3:
472 if nms[0] == re.sub(r"\s+", "", nms[2]) or nms[0] == nms[1]:
473 return ", ".join([nms[2], nms[1]]), nms[2]
474 return ", ".join([nms[0], nms[2], nms[1]]), nms[0]
475 elif len(nms) == 1:
476 return nms[0], nms[0]
477 else: # ふりがながないケース、と思う。
478 if nms[0] != re.sub(r"\s+", "", nms[1]):
479 return ", ".join([nms[0], nms[1]]), nms[0]
480 return nms[1], nms[1]
481
482 actorpages = list(filter(None, list(set(
483 map(lambda s: s.strip(),
484 io.open("wppagenames.txt", encoding="utf-8").read().strip().split("\n"))))))
485 result = []
486 for i, a in enumerate(actorpages):
487 inf = _from_wp(a)
488 if not inf:
489 continue
490 g = _yrgrp(inf.get("生年月日", ""), inf.get("actst", "0000"))
491 nms_disp, n0 = _names(inf)
492 wp = inf["wikipedia"]
493 if n0.replace(" ", "") == wp:
494 wp = ""
495 result.append((
496 g[0], g[1], g[2].replace("-", ""),
497 inf.get("没年月日", "").replace("-", ""),
498 inf.get("性別", 0),
499 wp,
500 inf["redirect"],
501 nms_disp,
502 re.sub(r"\s*型", "", inf.get("血液型", "")),
503 "、".join(list(filter(None, [inf.get("出生地", ""), inf.get("出身地", "")]))),
504 re.sub(r"、\s*([((])", r"\1", inf.get("愛称", "")),
505 re.sub(r"(\d)\s+([a-z])", r"\1\2", inf.get("身長/体重", "")),
506 re.sub(r"、\s*([((])", r"\1", inf.get("事務所・レーベル", "")),
507 re.sub(r"、\s*([((])", r"\1", inf.get("デビュー作", "")),
508 inf.get("共同作業者", ""),
509 inf.get("occ", ""),
510 inf.get("occ_fromabs", ""),
511 inf["citation_notice"],
512 ))
513 print("{}/{}".format(i + 1, len(actorpages)))
514 result.sort()
515
516 # 出力は「データセットアップするだけの js」+ html。csv ファイルとして独立させることを最初
517 # 考えたが、よくよく考えたら「js を link」が最も手っ取いよね。
518 with io.open("actor_basinf_data.js", "w", encoding="utf-8") as fo: # データ部
519 print("""\
520 var actor_basinf_data_csv = `""", file=fo)
521 def _esc(c):
522 return re.sub(r'''([$`])''', r"\\\1", str(c))
523 tmpobj = io.StringIO()
524 writer = csv.writer(tmpobj)
525 writer.writerows(((_esc(col) for col in row) for row in result))
526 print(tmpobj.getvalue().strip().replace("\r", ""), file=fo, end="")
527 print("""`;
528 var actor_basinf_data = Array();
529 actor_basinf_data_csv.trim().split("\\n").forEach(function (row, i) {
530 let r = jQuery.csv.toArray(row);
531 let bymd = r[2];
532 if (bymd) {
533 bymd = bymd.slice(0, 4) + "-" + bymd.slice(4, 6) + "-" + bymd.slice(6, 8);
534 }
535 let dymd = r[3];
536 if (dymd) {
537 dymd = dymd.slice(0, 4) + "-" + dymd.slice(4, 6) + "-" + dymd.slice(6, 8);
538 }
539 actor_basinf_data.push({
540 "by": r[0], /* 生誕年度 */
541 "as": r[1], /* 活動開始年? */
542 "bymd": bymd, /* 生年月日 */
543 "dymd": dymd, /* 没年月日 */
544 "gen": r[4], /* 性別 */
545 "wp": r[5], /* wikipedia */
546 "redi": r[6], /* リダイレクト先 */
547 "nm": r[7], /* 名前 */
548 "bld": r[8], /* 血液型 */
549 "bor": r[9], /* 出生地・出身地 */
550 "nn": r[10], /* 愛称 */
551 "tw": r[11], /* 身長/体重 */
552 "bel": r[12], /* 事務所・レーベル */
553 "fst": r[13], /* デビュー作 */
554 "tea": r[14], /* 共同作業者 */
555 "occ": r[15], /* 職業/職種/ジャンル */
556 "occ2": r[16], /* 本文最初のセンテンスの要約に書かれている職種 */
557 "cin": r[17], /* 「出典」の過不足: 0:警告なし, -1:「不足」, -2:「皆無」 */
558 })
559 })""", file=fo)
560 #
561 with io.open("actor_basinf.html", "w", encoding="utf-8") as fo: # html 本体
562 print("""\
563 <html>
564 <head jang="ja">
565 <meta charset="UTF-8">
566 <link href="https://cdnjs.cloudflare.com/ajax/libs/tabulator/5.0.6/css/tabulator_site.min.css" rel="stylesheet">
567 <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/tabulator/5.0.6/js/tabulator.min.js"></script>
568 <script type="text/javascript" src="https://oss.sheetjs.com/sheetjs/xlsx.full.min.js"></script>
569
570
571 <!-- Tabulator ではなく csv parser として jquery-csv を使いたくて、のための jquery -->
572 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
573 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-csv/1.0.21/jquery.csv.js"></script>
574
575 <script type="text/javascript" src="actor_basinf_data.js"></script>
576
577 <!--
578 {}
579 -->
580 </head>""".format(__doc__), file=fo)
581 print("""\
582 <body>
583 <div style="margin: 1em 0em">
584 <select id="filter-gender">
585 <option value=""></option>
586 <option value="1">男性のみ</option>
587 <option value="2">女性のみ</option>
588 </select>
589 <select id="filter-alive">
590 <option></option>
591 <option>存命者のみ</option>
592 <option>死没者のみ</option>
593 </select>
594 <select id="filter-occ2">
595 <option></option>
596 <option>wikipedia本文冒頭要約が声優に言及</option>
597 <option>wikipedia本文冒頭要約が声優に言及していない</option>
598 </select>
599 <select id="filter-citenote">
600 <option></option>
601 <option>「出典皆無」を除外</option>
602 <option>「出典皆無」と「出典不足」を除外</option>
603 </select>
604 </div>
605 <div id="actor_basinf"></div>
606 <div style="margin: 1em 0em">
607 <button id="download-csv">Download CSV</button>
608 <button id="download-json">Download JSON</button>
609 <button id="download-xlsx">Download XLSX</button>
610 <button id="download-html">Download HTML</button>
611 </div>
612 <script>
613 var filter_gender = document.getElementById("filter-gender");
614 function _update_genderfilter() {
615 let i = filter_gender.selectedIndex;
616 let v = filter_gender.options[i].value;
617 let eflts = table.getFilters(true);
618 for (let efi in eflts) {
619 let f = eflts[efi];
620 if (f["field"] == "gen") {
621 table.removeFilter(f["field"], f["type"], f["value"]);
622 break;
623 }
624 }
625 if (v) {
626 table.addFilter("gen", "regex", v);
627 }
628 }
629 filter_gender.addEventListener("change", _update_genderfilter);
630 var filter_alive = document.getElementById("filter-alive");
631 function _update_alivefilter() {
632 let i = filter_alive.selectedIndex;
633 let eflts = table.getFilters(true);
634 for (let efi in eflts) {
635 let f = eflts[efi];
636 if (f["field"] == "dymd") {
637 table.removeFilter(f["field"], f["type"], f["value"]);
638 break;
639 }
640 }
641 if (i == 1) {
642 table.addFilter("dymd", "=", "");
643 } else if (i == 2) {
644 table.addFilter("dymd", "!=", "");
645 }
646 }
647 filter_alive.addEventListener("change", _update_alivefilter);
648 var filter_occ2 = document.getElementById("filter-occ2");
649 function _occ2filter(data, para) {
650 if (para == 1) {
651 return data["occ2"].includes("声優");
652 } else {
653 return !data["occ2"].includes("声優");
654 }
655 }
656 var last_filter_occ2_selectedIndex = 0;
657 function _update_occ2filter() {
658 let i = filter_occ2.selectedIndex;
659 let eflts = table.getFilters();
660 for (let efi in eflts) {
661 let f = eflts[efi];
662 if (f["field"] === _occ2filter) {
663 table.removeFilter(_occ2filter, last_filter_occ2_selectedIndex);
664 break;
665 }
666 }
667 if (i > 0) {
668 table.addFilter(_occ2filter, i);
669 }
670 last_filter_occ2_selectedIndex = i;
671 }
672 filter_occ2.addEventListener("change", _update_occ2filter);
673 var filter_citenote = document.getElementById("filter-citenote");
674 function _citenotefilter(data, para) {
675 if (para == 1) {
676 return data["cin"] > -2;
677 } else {
678 return data["cin"] > -1;
679 }
680 }
681 var last_filter_citenote_selectedIndex = 0;
682 function _update_citenotefilter() {
683 let i = filter_citenote.selectedIndex;
684 let eflts = table.getFilters();
685 for (let efi in eflts) {
686 let f = eflts[efi];
687 if (f["field"] === _citenotefilter) {
688 table.removeFilter(_citenotefilter, last_filter_citenote_selectedIndex);
689 break;
690 }
691 }
692 if (i > 0) {
693 table.addFilter(_citenotefilter, i);
694 }
695 last_filter_citenote_selectedIndex = i;
696 }
697 filter_citenote.addEventListener("change", _update_citenotefilter);
698
699 document.getElementById("download-csv").addEventListener("click", function() {
700 table.download("csv", "data.csv");
701 });
702 document.getElementById("download-json").addEventListener("click", function() {
703 table.download("json", "data.json");
704 });
705 document.getElementById("download-xlsx").addEventListener("click", function() {
706 table.download("xlsx", "data.xlsx", {sheetName:"My Data"});
707 });
708 document.getElementById("download-html").addEventListener("click", function() {
709 table.download("html", "data.html", {style: true});
710 });
711
712 function _dt2int(dt) {
713 function _pad(n) {
714 let ns = "" + Math.abs(n);
715 if (ns.length === 1) {
716 ns = "0" + ns;
717 }
718 return ns;
719 }
720 return parseInt(dt.getFullYear() + _pad(dt.getMonth() + 1) + _pad(dt.getDate()));
721 }
722 var nowi = _dt2int(new Date());
723 function _calcage(cell, formatterParams) {
724 var v = cell.getValue().replace(new RegExp("\-", "g"), "");
725 if (v.startsWith("????")) {
726 return "";
727 }
728 v = parseInt(v.replace(/\?/g, "0"));
729 var result = "" + parseInt((nowi - v) / 10000);
730 result += "歳";
731 let unk = cell.getValue().includes("-??");
732 if (unk) {
733 result += "?";
734 }
735 var d = cell.getRow().getCell("dymd").getValue().replace(
736 "?", "0").replace(new RegExp("\-", "g"), "");
737 if (d) {
738 result += " (";
739 result += parseInt((parseInt(d) - v) / 10000);
740 result += "歳";
741 if (unk) {
742 result += "?";
743 }
744 result += "没)";
745 }
746 return result;
747 }
748
749 var table = new Tabulator("#actor_basinf", {
750 "height": "800px",
751 "columnDefaults": {
752 /* 注意: 「tooltips」ではない。「tooltip」である。 */
753 "tooltip": true,
754
755 /* これはいつから使えたのかな? かつては「columnDefaults」の外にいたやつ。 */
756 "headerSortTristate": true,
757 },
758 "columns": [
759 {
760 "field": "by",
761 "title": "生誕年度",
762 "headerFilter": "input",
763 "headerFilterFunc": "regex",
764 "formatter": function (cell, formatterParams, onRendered) {
765 let v = parseInt(cell.getValue());
766 return !v ? "????" : v;
767 },
768 },
769 {
770 "field": "as",
771 "title": "活動開始年?",
772 "headerFilter": "input",
773 "headerFilterFunc": "regex",
774 "headerTooltip": "「最低でもこの年には存命」としての用途を想定。wikipedia で断定しているもののほか、ページ内に現れる年の最小値も拾っている。"
775 },
776 {
777 "field": "bymd",
778 "title": "生年月日",
779 "headerFilter": "input",
780 "headerFilterFunc": "regex",
781 },
782 {
783 "field": "bymd",
784 "formatter": _calcage,
785 "headerTooltip": "存命の場合は年齢そのもの。亡くなっている方の場合は「生きていれば~歳」。 "
786 },
787 {
788 "field": "dymd",
789 "title": "没年月日",
790 },
791 {
792 "field": "gen",
793 "title": "性別",
794 "formatter": function (cell, formatterParams, onRendered) {
795 let g = cell.getValue();
796 if (g == 1) {
797 return "男";
798 } else if (g == 2) {
799 return "女";
800 }
801 return "";
802 },
803 },
804 {
805 "field": "nm",
806 "title": "名前",
807 "headerFilter": "input",
808 "headerFilterFunc": "regex",
809 },
810 {
811 "field": "wp",
812 "title": "wikipedia",
813 "formatter": function (cell, formatterParams, onRendered) {
814 let pn = cell.getValue();
815 if (!pn) {
816 let n0 = cell.getRow().getCell("nm").getValue();
817 n0 = n0.replace(new RegExp(", .*$"), "");
818 pn = n0.replace(new RegExp("\\\\s+", "g"), "");
819 }
820 return "<a href='https://ja.wikipedia.org/wiki/" +
821 pn + "' target=_blank>" + pn + "</a>";
822 },
823 },
824 {
825 "field": "redi",
826 "headerFilter": "input",
827 "headerFilterFunc": "regex",
828 "formatter": function (cell, formatterParams, onRendered) {
829 let pn = cell.getValue();
830 return "<a href='https://ja.wikipedia.org/wiki/" +
831 pn + "' target=_blank>" + pn + "</a>";
832 },
833 "headerTooltip": "転送された場合、転送先。"
834 },
835 {
836 "field": "bld",
837 "title": "血液型",
838 "headerFilter": "input",
839 "headerFilterFunc": "regex",
840 },
841 {
842 "field": "bor",
843 "title": "出生地・出身地",
844 "headerFilter": "input",
845 "headerFilterFunc": "regex",
846 },
847 {
848 "field": "nn",
849 "title": "愛称",
850 "headerFilter": "input",
851 "headerFilterFunc": "regex",
852 },
853 {
854 "field": "tw",
855 "title": "身長/体重",
856 "headerFilter": "input",
857 "headerFilterFunc": "regex",
858 },
859 {
860 "field": "bel",
861 "title": "事務所・レーベル",
862 "headerFilter": "input",
863 "headerFilterFunc": "regex",
864 },
865 {
866 "field": "fst",
867 "title": "デビュー作",
868 "headerFilter": "input",
869 "headerFilterFunc": "regex",
870 },
871 {
872 "field": "tea",
873 "title": "共同作業者",
874 "headerFilter": "input",
875 "headerFilterFunc": "regex",
876 },
877 {
878 "field": "occ",
879 "title": "職業/職種/ジャンル",
880 "headerFilter": "input",
881 "headerFilterFunc": "regex",
882 },
883 {
884 "field": "occ2",
885 "title": "wikipediaページ本文冒頭での要約に書かれている職業",
886 "headerFilter": "input",
887 "headerFilterFunc": "regex",
888 },
889 ],
890 "layout": "fitColumns",
891 "data": actor_basinf_data
892 });
893 </script>
894 </html>
895 """, file=fo)
今回の出力結果は actor_basinf.html と actor_basinf_data.js のふたつになり、data-uri の形で html 内に埋め込むと actor_basinf_data.js をワタシのロリポップサーバーにアップロード(して管理)する手間があってワタシがダルいので、今回は zip でまとめたものを:
展開して html をブラウザで開きなはれ。
あ、スクリプトも同梱したけれど、もし動かしたいなら、処理時間に無頓着に作ってるんで注意。html 取得部分のキャッシュだけはやってるけど、ページの解析をして情報収集する部分は毎度処理してるんで、今の 3800人くらいを処理するのに結構かかる。(コンビニ行って買い物して帰ってくるまでには終わらない、というくらい。)