一つ前の続き。
ひとつ前で言っていた「csv 的」と言っていたのは、まだ単に「非 key-value なリスト」の意味だった。
今回も「データと html ソースは一体」を維持するが、「csv 的」については一歩進めてみる。つまり「カンマセパレーティッド形式の表現」を入力とする話。前回言ったとおり Tabulator からのサポートはどうやらなさげなので、本質的な部分は Tabulator 関係ない。
javascript の言語から離れて汎用データテキスト表現を使うことは、デメリットもあるがメリットもある。フィールドの「型」を失うかわりに「引用符が必須でなくなる」:
1 var actor_basinf_data_csv =
2 [
3 [
4 0,
5 "1950年代",
6 "????????",
7 "",
8 1,
9 "",
10 "水島 晋, みずしま すすむ",
11 "",
12 "日本",
13 "",
14 "",
15 "",
16 "",
17 "",
18 "俳優、声優"
19 ],
20 [
21 0,
22 "1960年代",
23 "????0111",
24 "",
25 1,
26 "",
27 "高田 竜二, たかだ りゅうじ",
28 "",
29 "日本",
30 "",
31 "",
32 "",
33 "",
34 "",
35 ""
36 ]
37 ]
1 0,1950年代,????????,,1,,"水島 晋, みずしま すすむ",,日本,,,,,,俳優、声優
2 0,1960年代,????0111,,1,,"高田 竜二, たかだ りゅうじ",,日本,,,,,,
サイズを小さくしたい、とい目的にまさに合致するわけね。
「データと html ソースは一体」というのはつまり「独立した .csv ファイルをロードしてうんぬん」とするのではなく「ヒアドキュメント的に文字列として csv を貼り付ける」ということ。ひとまず今回はそこは維持するが、csv パースを今回解決出来るはずなので、その後 csv ファイルとして独立させるのもきっと簡単であろう、と。
いわゆる Unix シェルの「ヒアドキュメント」に完全に等価な機能はどうも javascript にはなさげよね。メジャーな Unix シェルで「ヒアドキュメント」といえば、引用符の違いにより変数展開あり/なしの両方を使える。ES6 の Template literals は、どうも変数展開しないものはなさげ:
1 let s = `
2 a,b,3,"abc,def"
3 d,d,g,"fjj,ggg"
4 x,y,\`wow!\`,ppp
5 x,y,\${val},"fjj,ggg"
6 `.trim()
エスケープが必要なのはバックティックそのものとダラーだけでいいかな? たぶんそうよね。
じゃぁこうして取り込んだ(埋め込んだ)csv 表現をパースするには?
javascript 本体、ECMAScript 本体にはないのかしらね。さっと調べてみるも皆 jquery-csv (cdnjs)をお薦めてる。Tabulator が jquery とのカップリングを切ったのにまた jquery か、とも少し思うが、まぁ「オレが jquery を使うかどうか」は Tabulator のポリシーとは無関係なので、気にせず使うことにしようか。
「Convert a multi-line CSV string to a 2D array」と multi-line でないのとの API の違いは「目を凝らさないとわからない」:
1 <html>
2 <head jang="ja">
3 <meta charset="UTF-8">
4
5 <!-- ... -->
6
7 <!-- Tabulator ではなく csv parser として jquery-csv を使いたくて、のための jquery -->
8 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
9 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-csv/1.0.21/jquery.csv.js"></script>
10
11 <!-- ... -->
12 <script>
13 /* ... */
14 let csvtext = `
15 a,b,3,"abc,def"
16 d,d,g,"fjj,ggg"
17 x,y,\`wow!\`,ppp
18 x,y,\${val},"fjj,ggg"
19 `;
20
21 console.log(jQuery.csv.toArrays(csvtext));
22 csvtext.trim().split("\n").forEach(function (row) {
23 console.log(jQuery.csv.toArray(row));
24 });
25 /* ... */
26 </script>
27 <!-- ... -->
そういうわけで:
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
54
55 import bs4 # require: beutifulsoup4
56
57
58 __MYNAME__, _ = os.path.splitext(
59 os.path.basename(sys.modules[__name__].__file__))
60 #
61 __USER_AGENT__ = "\
62 Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
63 AppleWebKit/537.36 (KHTML, like Gecko) \
64 Chrome/91.0.4472.124 Safari/537.36"
65 _htctxssl = ssl.create_default_context()
66 _htctxssl.check_hostname = False
67 _htctxssl.verify_mode = ssl.CERT_NONE
68 https_handler = urllib.request.HTTPSHandler(context=_htctxssl)
69 opener = urllib.request.build_opener(https_handler)
70 opener.addheaders = [('User-Agent', __USER_AGENT__)]
71 urllib.request.install_opener(opener)
72 #
73
74
75 _urlretrieved = dict()
76
77
78 def _urlretrieve(url):
79 if url in _urlretrieved:
80 return _urlretrieved[url]
81
82 def _gettemppath(s):
83 tmptopdir = os.path.join(tempfile.gettempdir(), __MYNAME__)
84 if not os.path.exists(tmptopdir):
85 os.makedirs(tmptopdir)
86 import hashlib, base64
87 ep = base64.urlsafe_b64encode(
88 hashlib.md5(s.encode("utf-8")).digest()
89 ).partition(b"=")[0].decode()
90 flat = os.path.join(tmptopdir, ep)
91 p1, p2 = ep[0], ep[1:]
92 d1 = os.path.join(tmptopdir, p1)
93 if not os.path.exists(d1):
94 os.makedirs(d1)
95 d2 = os.path.join(tmptopdir, p1, p2)
96 if os.path.exists(flat) and not os.path.exists(d2):
97 os.rename(flat, d2)
98 return d2
99
100 cachefn = _gettemppath(url)
101 if os.path.exists(cachefn):
102 res = cachefn
103 else:
104 try:
105 res, _ = urllib_urlretrieve(url, filename=cachefn)
106 except Exception:
107 from urllib.request import unquote as urllib_unquote
108 print(url, repr(urllib_unquote(url)))
109 raise
110 _urlretrieved[url] = res
111 return res
112
113
114 def _norm(s):
115 for f, r in (("(", "("), (")", ")"), ("・", "・")):
116 s = s.replace(f, r)
117 return s
118
119
120 def _try_get_actst(doc, actst, actst_rough):
121 # 活動開始をどうにか特定したい、ので、ヘディングで「1999年」としてるならまずはそれ、
122 # また、ヘディングが「出演」の配下にあるリスト項目の「~(1999年、…)」みたいな羅列
123 # を拾いたい、と思うわけだが、「html なのでこれは」なので(特に後者が)ひたすらに
124 # めんどいのよ。
125 validymin = 1900 # 日本の声優第一号の生誕から得るのがよかろうが、わからんのでてきとう
126 for stt, sta in (("dt", {}), ("b", {})):
127 # ヘディングから。<b>をヘディングにしてるのがいて迷惑…。
128 for dtt in [
129 dt.text
130 for dt in doc.find_all(stt, sta) if re.match(r"\d{4}年代?$", dt.text)]:
131 m = re.match(r"(\d+)年(代)?", dtt)
132 if not m.group(2):
133 v = int(m.group(1))
134 if v > validymin:
135 actst = min(v, actst)
136 else:
137 actst_rough.append(m.group(0))
138 if actst == 9999:
139 docs = str(doc)
140 m = re.search(r'<h2.* id="出演.*</h2>', docs)
141 if m:
142 _, rngs = m.span()
143 m = re.search(r'<h2.*.*</h2>', docs[rngs:])
144 if m:
145 rnge, _ = m.span()
146 sect = bs4.BeautifulSoup(docs[rngs:rngs + rnge], features="html.parser")
147 s = sect.text
148 for ys in re.findall(r"\((\d{4}[年.]代?)", s):
149 if ys[-1] == "代":
150 actst_rough.append(ys)
151 else:
152 v = int(ys[:-1])
153 if v > validymin:
154 actst = min(v, actst)
155 return actst, actst_rough
156
157
158 def _from_wp_jp(actorpagename):
159 baseurl = "https://ja.wikipedia.org/wiki/"
160 pn = urllib_quote(actorpagename, encoding="utf-8")
161 fn = _urlretrieve(baseurl + pn)
162
163 result = {"wikipedia": actorpagename, "名前": [actorpagename.partition("_")[0]]}
164 with io.open(fn, "r", encoding="utf-8") as fi:
165 soup = bs4.BeautifulSoup(_norm(fi.read()), features="html.parser")
166 entr = soup.find("a", {"class": "interlanguage-link-target", "lang": "en"})
167 if entr:
168 result["wikipedia_en"] = entr.attrs["href"]
169 try:
170 categos = [
171 a.text
172 for a in soup.find("div", {"id": "mw-normal-catlinks"}).find_all("a")]
173 except Exception:
174 categos = []
175 trecords = []
176 try:
177 trecords = iter(
178 soup.find("table", {"class": "infobox"}).find("tbody").find_all("tr"))
179 except Exception:
180 pass
181 if trecords:
182 tr = next(trecords)
183 if tr.find("th"):
184 tr = bs4.BeautifulSoup(
185 re.sub(r"<br\s*/?>", r"\n", str(tr)), features="html.parser")
186 for sp in tr.find("th").find_all("span"):
187 s = re.sub(r"\[[^\[\]]+\]", "", sp.text)
188 if "\n" in s:
189 result["名前"].extend(s.split("\n"))
190 else:
191 result["名前"].append(s)
192
193 actst = 9999
194 actst_rough = ["9999年代"]
195 for tr in trecords:
196 th, td = tr.find("th"), tr.find("td")
197 if not th or not td:
198 continue
199 k = th.text.replace("\n", "").replace(
200 "誕生日", "生年月日").replace("生誕", "生年月日")
201 k = re.sub("身長.*$", "身長/体重", k)
202 if k in ("事務所", "レーベル"):
203 k = "事務所・レーベル"
204 elif k in ("職業", "職種", "ジャンル",):
205 k = "occ"
206 v = ""
207 if td:
208 if td.find("li"):
209 td = "\n".join([li.text for li in td.find_all("li")])
210 elif td.find("br"):
211 td = bs4.BeautifulSoup(
212 re.sub(r"<br\s*/?>", r"\n", str(td)), features="html.parser").text.strip()
213 else:
214 td = td.text
215 v = re.sub(
216 r"\[[^\[\]]+\]", "\n", td).strip()
217 if k == "活動期間":
218 m = re.match(r"(\d+)年(代)?", v)
219 if m:
220 if not m.group(2):
221 actst = min(int(m.group(1)), actst)
222 else:
223 actst_rough.append(m.group(0))
224 continue
225 elif k in ("生年月日", "没年月日"):
226 m1 = re.search(r"\d+-\d+-\d+", v)
227 m2 = re.search(r"(\d+)年(\d+)月(\d+)日", v)
228 m3 = re.search(r"(\d+)月(\d+)日", v)
229 if m1:
230 v = m1.group(0)
231 elif m2:
232 v = "{:04d}-{:02d}-{:02d}".format(
233 *list(map(int, m2.group(1, 2, 3))))
234 elif m3:
235 v = "0000-{:02d}-{:02d}".format(
236 *list(map(int, m3.group(1, 2))))
237 else:
238 v = ""
239 v = re.sub(
240 r"(、\s*)+", "、",
241 "、".join(re.sub(r"\n+", r"\n", v).split("\n")))
242 if k in ("デビュー作", "事務所・レーベル", "共同作業者",):
243 v = "、".join(list(filter(None, [result.get(k), v])))
244 elif k == "occ":
245 v = "、".join(
246 list(
247 set("、".join(list(filter(None, [result.get(k, ""), v]))).split("、"))))
248 elif k == "身長/体重":
249 v = re.sub(r"\s*、\s*cm", " cm", v) # なんで??
250 result[k] = v
251 actst, actst_rough = _try_get_actst(soup, actst, actst_rough)
252 actst_rough_min = list(sorted(actst_rough))[0]
253 armv = int(re.match(r"(\d+)", actst_rough_min).group(1))
254 if actst < 9999 and armv // 10 > actst // 10:
255 result["actst"] = "{:04d}".format(actst)
256 elif armv < 9999:
257 result["actst"] = actst_rough_min
258 else:
259 result["actst"] = "0000"
260 if not result.get("生年月日"):
261 # infobox なしなのに律儀に生年月日は書いてる、てのが結構ある。
262 m = re.search(
263 r"[^、。()\n]+\s*\([^、。()\n]+、(\d{4}年)?(\d+月)?(\d+日)?[^()]*\)は、日本の",
264 soup.text)
265 if m:
266 yy, mm, dd = m.group(1, 2, 3)
267 if yy:
268 yy = int(yy[:-1])
269 else:
270 yy = 0
271 if mm and dd:
272 result["生年月日"] = "{:04d}-{:02d}-{:02d}".format(
273 yy, int(mm[:-1]), int(dd[:-1]))
274 elif mm:
275 result["生年月日"] = "{:04d}-{:02d}-??".format(
276 yy, int(mm[:-1]))
277 else:
278 result["生年月日"] = "{:04d}-??-??".format(yy)
279 else:
280 result["生年月日"] = "0000-??-??"
281 if not result.get("性別"):
282 if any([("男性" in c or "男優" in c) for c in categos]):
283 result["性別"] = 1
284 elif any([("女性" in c or "女優" in c) for c in categos]):
285 result["性別"] = 2
286 else:
287 result["性別"] = 0
288 else:
289 result["性別"] = 1 if ("男" in result["性別"]) else 2
290 return result
291
292
293 def _from_wp_en(result):
294 # 基本的に日本語版を信じ、欠落のものだけ英語版に頼ることにする。
295 if "生年月日" in result and "0000" not in result["生年月日"]:
296 return result
297 try:
298 fn = _urlretrieve(result["wikipedia_en"])
299 except urllib.error.HTTPError:
300 return result
301 with io.open(fn, "r", encoding="utf-8") as fi:
302 soup = bs4.BeautifulSoup(fi.read(), features="html.parser")
303 try:
304 trecords = iter(
305 soup.find("table", {"class": "infobox"}).find("tbody").find_all("tr"))
306 except Exception:
307 return result
308 for tr in trecords:
309 th, td = tr.find("th"), tr.find("td")
310 if not th or not td:
311 continue
312 k_en = th.text.strip()
313 if k_en == "Born":
314 if "生年月日" not in result or "0000" in result["生年月日"]:
315 bd = td.find("span", {"class": "bday"})
316 if bd:
317 bd = re.sub(r"\[[^\[\]]+\]", "", bd.text)
318 result["生年月日"] = bd
319 return result
320
321
322 def _from_wp(actorpagename):
323 result = _from_wp_jp(actorpagename)
324 if "wikipedia_en" in result:
325 _from_wp_en(result)
326 return result
327
328
329 if __name__ == '__main__':
330 def _yrgrp(ymd, actst):
331 y = 0
332 if ymd:
333 y, _, md = ymd.partition("-")
334 y = int(y)
335 if y and md < "04-02":
336 y -= 1
337 return [
338 y,
339 actst.replace("0000", "????"),
340 ymd.replace("0000", "????")]
341 def _names(inf):
342 # inf["名前"]
343 # 通常は
344 # 0: wikipediaページ名からあいまい回避サフィクスを取り除いた名前
345 # 1: 2のよみ
346 # 2: 姓+スペース+名
347 # ページによってこれがバラつく。0しか取れないものは結構ある。外国人名は「姓+スペース+名」
348 # に従わない。通常ケースでは 2、1 をとって 0 を捨てるのが使いやすい。
349 nms = inf["名前"]
350 if len(nms) == 3:
351 if nms[0] == re.sub(r"\s+", "", nms[2]) or nms[0] == nms[1]:
352 return ", ".join([nms[2], nms[1]]), nms[2]
353 return ", ".join([nms[0], nms[2], nms[1]]), nms[0]
354 elif len(nms) == 1:
355 return nms[0], nms[0]
356 else: # ふりがながないケース、と思う。
357 if nms[0] != re.sub(r"\s+", "", nms[1]):
358 return ", ".join([nms[0], nms[1]]), nms[0]
359 return nms[1], nms[1]
360
361 actorpages = list(filter(None, list(set(
362 map(lambda s: s.strip(),
363 io.open("wppagenames.txt", encoding="utf-8").read().strip().split("\n"))))))
364 result = []
365 for i, a in enumerate(actorpages):
366 inf = _from_wp(a)
367 g = _yrgrp(inf.get("生年月日", ""), inf.get("actst", "0000"))
368 nms_disp, n0 = _names(inf)
369 wp = inf["wikipedia"]
370 if n0.replace(" ", "") == wp:
371 wp = ""
372 result.append((
373 g[0], g[1], g[2].replace("-", ""),
374 inf.get("没年月日", "").replace("-", ""),
375 inf.get("性別", 0),
376 wp,
377 nms_disp,
378 re.sub(r"\s*型", "", inf.get("血液型", "")),
379 re.sub(
380 r"\b日本・", "",
381 "、".join(list(filter(None, [inf.get("出生地", ""), inf.get("出身地", "")])))),
382 re.sub(r"、\s*([((])", r"\1", inf.get("愛称", "")),
383 re.sub(r"(\d)\s+([a-z])", r"\1\2", inf.get("身長/体重", "")),
384 re.sub(r"、\s*([((])", r"\1", inf.get("事務所・レーベル", "")),
385 re.sub(r"、\s*([((])", r"\1", inf.get("デビュー作", "")),
386 inf.get("共同作業者", ""),
387 inf.get("occ", ""),
388 ))
389 print("{}/{}".format(i + 1, len(actorpages)))
390 result.sort()
391 with io.open("actor_basinf.html", "w", encoding="utf-8") as fo:
392 print("""\
393 <html>
394 <head jang="ja">
395 <meta charset="UTF-8">
396 <link href="https://cdnjs.cloudflare.com/ajax/libs/tabulator/5.0.6/css/tabulator_site.min.css" rel="stylesheet">
397 <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/tabulator/5.0.6/js/tabulator.min.js"></script>
398 <script type="text/javascript" src="https://oss.sheetjs.com/sheetjs/xlsx.full.min.js"></script>
399
400
401 <!-- Tabulator ではなく csv parser として jquery-csv を使いたくて、のための jquery -->
402 <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
403 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-csv/1.0.21/jquery.csv.js"></script>
404
405 <!--
406 {}
407 -->
408 </head>""".format(__doc__), file=fo)
409 print("""\
410 <body>
411 <div style="margin: 1em 0em">
412 <select id="filter-gender">
413 <option value=""></option>
414 <option value="1">男性のみ</option>
415 <option value="2">女性のみ</option>
416 </select>
417 <select id="filter-alive">
418 <option></option>
419 <option>存命者のみ</option>
420 <option>死没者のみ</option>
421 </select>
422 </div>
423 <div id="actor_basinf"></div>
424 <div style="margin: 1em 0em">
425 <button id="download-csv">Download CSV</button>
426 <button id="download-json">Download JSON</button>
427 <button id="download-xlsx">Download XLSX</button>
428 <button id="download-html">Download HTML</button>
429 </div>
430 <script>
431 var filter_gender = document.getElementById("filter-gender");
432 function _update_genderfilter() {
433 let i = filter_gender.selectedIndex;
434 let v = filter_gender.options[i].value;
435 let eflts = table.getFilters(true);
436 for (let efi in eflts) {
437 let f = eflts[efi];
438 if (f["field"] == "gen") {
439 table.removeFilter(f["field"], f["type"], f["value"]);
440 break;
441 }
442 }
443 if (v) {
444 table.addFilter("gen", "regex", v);
445 }
446 }
447 filter_gender.addEventListener("change", _update_genderfilter);
448 var filter_alive = document.getElementById("filter-alive");
449 function _update_alivefilter() {
450 let i = filter_alive.selectedIndex;
451 let eflts = table.getFilters(true);
452 for (let efi in eflts) {
453 let f = eflts[efi];
454 if (f["field"] == "dymd") {
455 table.removeFilter(f["field"], f["type"], f["value"]);
456 break;
457 }
458 }
459 if (i == 1) {
460 table.addFilter("dymd", "=", "");
461 } else if (i == 2) {
462 table.addFilter("dymd", "!=", "");
463 }
464 }
465 filter_alive.addEventListener("change", _update_alivefilter);
466
467 document.getElementById("download-csv").addEventListener("click", function() {
468 table.download("csv", "data.csv");
469 });
470 document.getElementById("download-json").addEventListener("click", function() {
471 table.download("json", "data.json");
472 });
473 document.getElementById("download-xlsx").addEventListener("click", function() {
474 table.download("xlsx", "data.xlsx", {sheetName:"My Data"});
475 });
476 document.getElementById("download-html").addEventListener("click", function() {
477 table.download("html", "data.html", {style: true});
478 });
479
480 function _dt2int(dt) {
481 function _pad(n) {
482 let ns = "" + Math.abs(n);
483 if (ns.length === 1) {
484 ns = "0" + ns;
485 }
486 return ns;
487 }
488 return parseInt(dt.getFullYear() + _pad(dt.getMonth() + 1) + _pad(dt.getDate()));
489 }
490 var nowi = _dt2int(new Date());
491 function _calcage(cell, formatterParams) {
492 var v = cell.getValue().replace(new RegExp("\-", "g"), "");
493 if (v.startsWith("????")) {
494 return "";
495 }
496 v = parseInt(v.replace(/\?/g, "0"));
497 var result = "" + parseInt((nowi - v) / 10000);
498 result += "歳";
499 let unk = cell.getValue().includes("-??");
500 if (unk) {
501 result += "?";
502 }
503 var d = cell.getRow().getCell("dymd").getValue().replace(
504 "?", "0").replace(new RegExp("\-", "g"), "");
505 if (d) {
506 result += " (";
507 result += parseInt((parseInt(d) - v) / 10000);
508 result += "歳";
509 if (unk) {
510 result += "?";
511 }
512 result += "没)";
513 }
514 return result;
515 }
516
517
518 var actor_basinf_data_csv = `""", file=fo)
519 def _esc(c):
520 return re.sub(r'''([$`])''', r"\\\1", str(c))
521 tmpobj = io.StringIO()
522 writer = csv.writer(tmpobj)
523 writer.writerows(((_esc(col) for col in row) for row in result))
524 print(tmpobj.getvalue().strip().replace("\r", ""), file=fo, end="")
525 print("""`;
526 var actor_basinf_data = Array();
527 actor_basinf_data_csv.trim().split("\\n").forEach(function (row, i) {
528 let r = jQuery.csv.toArray(row);
529 let bymd = r[2];
530 if (bymd) {
531 bymd = bymd.slice(0, 4) + "-" + bymd.slice(4, 6) + "-" + bymd.slice(6, 8);
532 }
533 let dymd = r[3];
534 if (dymd) {
535 dymd = dymd.slice(0, 4) + "-" + dymd.slice(4, 6) + "-" + dymd.slice(6, 8);
536 }
537 actor_basinf_data.push({
538 "by": r[0], /* 生誕年度 */
539 "as": r[1], /* 活動開始年? */
540 "bymd": bymd, /* 生年月日 */
541 "dymd": dymd, /* 没年月日 */
542 "gen": r[4], /* 性別 */
543 "wp": r[5], /* wikipedia */
544 "nm": r[6], /* 名前 */
545 "bld": r[7], /* 血液型 */
546 "bor": r[8], /* 出生地・出身地 */
547 "nn": r[9], /* 愛称 */
548 "tw": r[10], /* 身長/体重 */
549 "bel": r[11], /* 事務所・レーベル */
550 "fst": r[12], /* デビュー作 */
551 "tea": r[13], /* 共同作業者 */
552 "occ": r[14], /* 職業/職種/ジャンル */
553 })
554 })
555
556
557 var table = new Tabulator("#actor_basinf", {
558 "height": "800px",
559 "columnDefaults": {
560 /* 注意: 「tooltips」ではない。「tooltip」である。 */
561 "tooltip": true,
562
563 /* これはいつから使えたのかな? かつては「columnDefaults」の外にいたやつ。 */
564 "headerSortTristate": true,
565 },
566 "columns": [
567 {
568 "field": "by",
569 "title": "生誕年度",
570 "headerFilter": "input",
571 "headerFilterFunc": "regex",
572 "formatter": function (cell, formatterParams, onRendered) {
573 let v = parseInt(cell.getValue());
574 return !v ? "????" : v;
575 },
576 },
577 {
578 "field": "as",
579 "title": "活動開始年?",
580 "headerFilter": "input",
581 "headerFilterFunc": "regex",
582 },
583 {
584 "field": "bymd",
585 "title": "生年月日",
586 "headerFilter": "input",
587 "headerFilterFunc": "regex",
588 },
589 {
590 "field": "bymd",
591 "formatter": _calcage,
592 "headerTooltip": "存命の場合は年齢そのもの。亡くなっている方の場合は「生きていれば~歳」。 "
593 },
594 {
595 "field": "dymd",
596 "title": "没年月日",
597 },
598 {
599 "field": "gen",
600 "title": "性別",
601 "formatter": function (cell, formatterParams, onRendered) {
602 let g = cell.getValue();
603 if (g == 1) {
604 return "男";
605 } else if (g == 2) {
606 return "女";
607 }
608 return "";
609 },
610 },
611 {
612 "field": "nm",
613 "title": "名前",
614 "headerFilter": "input",
615 "headerFilterFunc": "regex",
616 },
617 {
618 "field": "wp",
619 "title": "wikipedia",
620 "formatter": function (cell, formatterParams, onRendered) {
621 let pn = cell.getValue();
622 if (!pn) {
623 let n0 = cell.getRow().getCell("nm").getValue();
624 n0 = n0.replace(new RegExp(", .*$"), "");
625 pn = n0.replace(new RegExp("\\\\s+", "g"), "");
626 }
627 return "<a href='https://ja.wikipedia.org/wiki/" +
628 pn + "' target=_blank>" + pn + "</a>";
629 },
630 },
631 {
632 "field": "bld",
633 "title": "血液型",
634 "headerFilter": "input",
635 "headerFilterFunc": "regex",
636 },
637 {
638 "field": "bor",
639 "title": "出生地・出身地",
640 "headerFilter": "input",
641 "headerFilterFunc": "regex",
642 },
643 {
644 "field": "nn",
645 "title": "愛称",
646 "headerFilter": "input",
647 "headerFilterFunc": "regex",
648 },
649 {
650 "field": "tw",
651 "title": "身長/体重",
652 "headerFilter": "input",
653 "headerFilterFunc": "regex",
654 },
655 {
656 "field": "bel",
657 "title": "事務所・レーベル",
658 "headerFilter": "input",
659 "headerFilterFunc": "regex",
660 },
661 {
662 "field": "fst",
663 "title": "デビュー作",
664 "headerFilter": "input",
665 "headerFilterFunc": "regex",
666 },
667 {
668 "field": "tea",
669 "title": "共同作業者",
670 "headerFilter": "input",
671 "headerFilterFunc": "regex",
672 },
673 {
674 "field": "occ",
675 "title": "職業/職種/ジャンル",
676 "headerFilter": "input",
677 "headerFilterFunc": "regex",
678 },
679 ],
680 "layout": "fitColumns",
681 "data": actor_basinf_data
682 });
683 </script>
684 </html>
685 """, file=fo)
変更前の構造が「actor_basinf_data_csv を改行で split して各行ごとに Object に変換」してたので、ひとまずそのままのノリで jquery-csv は「toArrays」ではなく「toArray」を使った。それはまぁいいのだが、Windows 固有の問題としての復帰コード(\r)問題でハマってしまった。いつもながらうっとうしい…。
結果は以下の通りだが、一つ前のバージョンよりもさらに 500 人くらい多く扱っている:
生成された html はかな~り小さくなってて、一つ前で説明した WordPress の制限には、たぶんあと 1500 人くらい増やさないと引っかからなさそう。
つーかやっぱり「key-value だけが世界を救うので csv なんかウンコだ」ノリが javascript/ecmascript 文化全体で徹底しちゃってるのはどうにかして欲しいなぁ。「ところにより csv」が欲しいのなんか、絶対ワタシだけじゃないぞ。