Tabulator で csv 「的」データを入力にしたい、兼「声優世代表のおとも」(2)

一つ前の続き。

ひとつ前で言っていた「csv 」と言っていたのは、まだ単に「非 key-value なリスト」の意味だった。

今回も「データと html ソースは一体」を維持するが、「csv 的」については一歩進めてみる。つまり「カンマセパレーティッド形式の表現」を入力とする話。前回言ったとおり Tabulator からのサポートはどうやらなさげなので、本質的な部分は Tabulator 関係ない。

javascript の言語から離れて汎用データテキスト表現を使うことは、デメリットもあるがメリットもある。フィールドの「型」を失うかわりに「引用符が必須でなくなる」:

javascript の Array を 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 ]
同じものを csv で表現
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 <!-- ... -->

そういうわけで:

wikipedia をスクレイプして「声優世代表のおとも html」、な、ver 9、だっけか
  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」が欲しいのなんか、絶対ワタシだけじゃないぞ。



Related Posts