- 欢迎来到THBWiki!如果您是第一次来到这里,请点击右上角注册一个帐户
- 有任何意见、建议、求助、反馈都可以在 讨论板 提出
- THBWiki以专业性和准确性为目标,如果你发现了任何确定的错误或疏漏,可在登录后直接进行改正
用户Wiki:NicoNicoNii
关于我
| 基本信息 | |
| 人物名 | NicoNicoNii |
|---|---|
| 称号 | 二十一世纪的二十世纪延长型的开发者 |
| 能力 | 操纵程序计数器程度的能力LDA #$c0, PHA, LDA #$de, EOR #3, PHA, RTS
|
| 入东方时间 | 见下文 |
入东方时间
根据我对网络评论区的观察,我自认为“入东方时间”是只有资深人士才能安全回答的问题,因为只有“游戏LNN全通关”、“发起过社团”、“熟悉100+角色设定”、“创作过 XXX 作品,点击数破万”是不会产生异议的证据。但对“只是看过什么”的泛 ACG 网民来说则十分危险,不能轻易回答。如果在错误场合给出错误答案,“老登”、“梗小鬼”、“伪粉丝”、“引流狗”、“云玩家”、“二设入脑”、“只认识一部作品的Bad Apple!!厨”等骂名会让你应接不暇(曾经,亲自动手移植Bad Apple!!到新平台可以得到“大神”称号作为免死金牌,但后来因为过度泛滥和某些开发者引发的争议,这免死金牌已经无效了,甚至可能会认为是蹭热点的——然而我自己就移植了两部,让我不寒而栗)。所以我也不敢给出答案,请读者自行甄别。
如果我要假装我是老资历,我会说 2013 年的初中,然后指指我的 6 位数 B 站 UID,再指指我 2014 年搬运的东方 meme(甚至不用我自己指出 UID,单纯现身评论区即可),百试百灵,甚至我主观上没想试,也会灵验。考虑到目前 B 站用户的平均年龄,70% 的人都无法反驳;如果我要更加浑水摸鱼地假装我是老资历,我说 2016 年,绝对无法被证伪,能忽悠 90% 的人(可以拿阅读过《正直者之死》的设定作为挡箭牌——谁闲着没事会去看这个?);如果我看到评论区正在吵架,要假装自己是一般通过以自保,我会说我从未入坑。
这忽然让我想起了计算机领域的一项传统:你可以自称你热爱黑客技术(hacking),你也可以称呼你的好友为“黑客”(hacker),但你不能说你自己是“黑客”。“黑客”是一个头衔,是别人对你的尊称。只有足够多的人承认你为“黑客”之后,你才能自称“黑客”。在过去,“东方众”只是网民的自称,刷一刷“此生无悔入东方,来世愿生幻想乡”,那你就是“东方众”。而到了现在,“东方众”似乎也开始遵守这套“黑客”的逻辑了(实际上,可能从十几年前“伪宅”的自称出现后,这套逻辑就生效了)。如果这样理解,许多令人费解的问题就都能解释通了。我自己从未违反过“黑客守则“:我认识很多“黑客”,其中也有人也说我是”黑客“,但我不说我是“黑客”。这也更加坚定了我无论认识多少“东方”和“东方众”,也不会自称“东方众”的决心。
但另一方面,这个问题在客观上实际上很有趣,涉及到我究竟参与了哪一段 ACG 发展史,因此我也经过了一番基于客观数据的考证(使用stat(1)、浏览器开发工具获取微博时间戳等方法),结果如下:
- 候选答案:2012?可能这段时间观看过《琪露诺算术教室》、《最终鬼畜蓝蓝路》、《sweet little sister》(我还记得后者来自蓝蓝路的相关推荐,因为原曲同为U.N. Owen Was Her?)。不过暂无客观数据记录出土,无法证明具体时间。
- 候选答案:2013-08-02 07:20:39 +0000(硬盘文件《【音乐】精选东方同人曲【按角色分类】》下载时间)
- 候选答案:2013-08-31 18:29:20 +0000(硬盘文件《喜闻乐见的ACG歌曲》下载时间。其中包含当时举办的“世萌吧 ACG 音乐榜单 2008-2012 组”排名前 50 位歌曲,其中《色は匂へど散りぬるを》、《華鳥風月》分别取得第 5 名、第 48 名的成绩)
- 候选答案:2013-10-01 15:48:51 +0000(参与了当时 ACG 圈空前的《有屏幕的地方就有 Bad Apple!!》热潮,将其移植到了自己的树莓派 + SSD1306 的 LCD 屏幕上)
- 候选答案:2014-04-28 18:17:21 +0800(与网友聊天时调侃:“OpenBSD 与 OpenSSH 连点香油钱都没有,跟博丽神社一个情况)
- 候选答案:2014-07-17 17:14:48 +0800(从微博网友“@ACG音乐吧官博娘”抽奖得到一张 CD 专辑《EastNewSound - Felsic Mirage》)
- 候选答案:2014-07-24 04:53:55 +0000(将《魔理沙偷走了重要的东西》的 4chan 编程区 meme《阿伯尔森偷走了重要的课程》搬运至 B 站)
- 候选答案:2014-05-24 18:22:21 +0000(硬盘文件《【东方纯音乐】 蝉在叫 人坏掉!》下载时间)
- 候选答案:2015-01-30(几周前参加 Novell/SUSE 公司的北京办公室举办的 openSUSE 操作系统的新版本发布聚会,友人送了我几张东方角色贴纸)
- 候选答案:2016-01-22 04:51:50 +0000(硬盘文件《凋叶棕 - 屠》下载时间,以《正直者之死》为题材的专辑)
- 候选答案:2018-11-05 03:47:09 +0800(入坑复古计算,经过一个月的努力,成功将《Bad Apple!!》移植到 TI-84 PCSE 计算器上,内循环使用 Z80 汇编语言完成,但因为是半成品没有发表,演示视频在 2024 年分享至 B 站)
- 候选答案:2019-06-13 22:58:57 +0800(继续复古计算,顺手为一个 Atari 800 计算机的《Bad Apple!!》移植视频制作中文字幕并分享,在微博获得 800+ 转发)。
- 候选答案:2021-03-31 16:17:14 +0800(继续复古计算,天天折腾 Z80、6502、m68k 古董计算机相关内容,然后意外发现了东方同人音乐一个极为冷门的分支——复古平台的chiptune,一举三得——能听东方音乐,能结合复古计算爱好,此外东方曲风无论旧作新作本来就都深受 chiptune 影响,几乎直接当原作音乐听,相当于平替了原有的播放列表。当时天天在微博上分享仅有的少数 chiptune 专辑——美国的 C64 与 Amiga 平台的东方二创可能比熊猫还稀有。在 2021 年的这一天,因为我高强度分享专辑,被关注我的信息安全专家“教主” @tombkeeper 评论道:“原来你也是越共”)
- 候选答案:迄今为止,从未入坑(2010 年代的东方 Project 在 ACG 界,就相当于民用航空业的波音 707。如果你向一位当年的普通乘客提起波音 707,他们更可能回忆起年轻时候的事业,或者自己如何周游世界——而不是像本百科的各位大佬一样,讲解波音 707 的空气动力学设计或者发动机的技术创新。我就是当年的普通ALISON航空??????乘客——这也是我最倾向的答案。我的业余时间 90% 用在软件硬件开发,剩下 10% 不足研究任何 ACG 内容,只够了解一些梗,方便交流,毕竟 ACG 在中国大陆极客圈的地位好比《星际迷航》之于美国)
以下是一个真实的故事:从前有个小男孩,他的父亲说:“要学着像其他人一样。不要愁眉苦脸。”他试啊,试啊,但就是做不到。于是他父亲用皮鞭抽打他,然后狮子把他吃掉了。读者们,如果你还年轻,那么请将他的悲惨人生与死亡作为警告。[...] 你最好隐藏起来,假装自己是庄重、木讷的人。[...] 特别是如果你打算写书 [...] 要严谨:这样可以掩盖许多罪过。并且不要愁眉苦脸。——《电磁学理论》第 3 卷,第 9 章《移动源产生的波》by 奥利弗·亥维赛
各种番剧的片名,一定给我牢记:我是个中二病、恋爱果然有问题、刀剑神域、未闻花名、柯南、奈亚子潜行,没有看过没关系,关键说话有底气。动漫的名场面,只看很多人刷的就行,“不要停下来啊!”、“kksk!“,不用担心梗的前后有没有联系,毕竟在虚构的世界找真实的一定有问题。“NicoNicoNii~”、“没什么好怕的了!”,今天问问神奇海螺也很喧嚣呢,“真是 High 到不行!”、“咋瓦鲁多!我不做人了!”、“你的下一句话是:‘你为什么这么熟练啊!’”。
但是有一些圈,最好别装熟练:V 家、拉拉人、车万众、月球人,普罗丢色、劈叉舰长——平时不会探头,惹恼他们后果就是被打屁滚尿流。作品也就图一乐,二创才是活力,出了圈的鬼畜、同人、手办一定要被唾弃。说他们恰烂钱,没骨气,骂得越凶,你就越像二次元菊苣……既然如此,不妨大方地说:你好,我不懂二次元。——《装二次元?简单!两分钟教会你!》by 永远的MG
东方 OST 信息检索工具开发笔记:THBWiki 获取信息的歪门邪道
THBWiki 最有用的公共服务之一是音乐资料API,其中包含上万种东方音乐专辑的相关信息。然而,音乐资料 API 中只返回同人专辑信息(以及对应的原曲),但无法查询关于原曲本身的相关信息,如出场作品、关卡位置、中英日标题、作者评论等。我在开发东方原曲信息检索工具的时候,发现了许多利用 MediaWiki API 与 Wiki 语法本身获取原曲信息的多种歪门邪道方法,特别在此记载。
获取全部东方乐曲名称映射表
为了正规化东方乐曲的日文、中文、英文名称,THBWiki 使用了独有的 Table Mapping 这一 MediaWiki 插件,将角色头衔、角色能力、符卡名称、乐曲信息等属性作为 key-value 格式的数据结构统一管理。其技术细节,详见帮助:管理映射方案。这就为获取全部东方乐曲名称提供了极佳的技术手段。
本节介绍三种获取全部东方乐曲名称映射表,从简单的方法(效率低),一般的方法(效率低),再到走火入魔的方法(效率极高)。
难度 Easy:映射表 API
获取映射表信息的最普通方式,是使用映射表 API:action=tablemapping。
CSRF
action=tablemapping 受 CSRF 攻击保护。要使用此 API,必须先获取 MediaWiki API 通用的 CSRF 安全令牌:
curl 'https://thwiki.cc/api.php'
-d "action=query"
-d "meta=tokens"
-d "format=json" 2>/dev/null | jq
系统返回:
{
"batchcomplete": "",
"query": {
"tokens": {
"csrftoken": "+\\"
}
}
}
所有 action=tablemapping 后续请求,都需要将csrftoken的字符串传入必选参数token。
值得注意的是,API 任何人均可使用,无需登录。由于未登录用户不涉及安全问题,令牌一律为空占位符+\,所以实际上和没有一样。但考虑到 MediaWiki 后续修改占位符的可能性,不建议硬编码内容,还是额外请求一下为好。
使用 action=tablemapping, maction=list
使用 maction=list 可以获得某个分类的映射表。其中selectcat是可选参数,如果不填写,则列出全部映射表(角色头衔、角色能力、符卡名称等)。我们需要查询的是乐曲信息,因此传入参数selectcat=音乐名日文。
使用例:
$ curl -X POST --data-urlencode "action=tablemapping" \
--data-urlencode "format=json" \
--data-urlencode "maction=list" \
--data-urlencode "selectcat=音乐名日文" \
--data-urlencode "token=+\\" \
https://thwiki.cc/api.php 2>/dev/null | jq
"token=+\\"是未登录用户的统一 CSRF token,必填。为了安全,只能使用POST请求,无法使用GET。此外,因token字段含有特殊符号,必须使用--data-urlencode。
系统返回:
{
"tablemapping": {
"result": "success",
"schemes": [
{
"id": 1,
"scheme": "红魔乡音乐名/日文"
},
{
"id": 3,
"scheme": "花映塚音乐名/日文"
},
{
"id": 5,
"scheme": "莲台野夜行音乐名/日文"
},
{
"id": 7,
"scheme": "蓬莱人形音乐名/日文"
},
{
"id": 9,
"scheme": "辉针城音乐名/日文"
},
值得注意的是,一次不能查询多个映射表,但/日文后缀可以在得到结果后,人工修改为/英文或/中文。
使用 action=tablemapping, maction=browse
得到映射表标题后,使用 maction=browse 获得映射表内容。
使用例:
$ curl -X POST --data-urlencode "action=tablemapping" \
--data-urlencode "format=json" \
--data-urlencode "maction=browse" \
--data-urlencode "scheme=红魔乡音乐名/日文" \
--data-urlencode "token=+\\" \
https://thwiki.cc/api.php 2>/dev/null | jq
系统返回:
{
"tablemapping": {
"result": "success",
"scheme_data": {
"id": "1",
"scheme": "红魔乡音乐名/日文",
"data": [
{
"index_text": "!CAT",
"value_text": "音乐名日文"
},
{
"index_text": "!DEF",
"value_text": "缺少参数"
},
{
"index_text": "!TEM",
"value_text": "红魔乡音乐名"
},
{
"index_text": "1|T",
"value_text": "赤より紅い夢"
},
{
"index_text": "2|1-1",
"value_text": "ほおずきみたいに紅い魂"
},
{
"index_text": "3|1-2",
"value_text": "妖魔夜行"
},
{
"index_text": "4|2-1",
"value_text": "ルーネイトエルフ"
},
...
{
"index_text": "!COR",
"value_text": "东方红魔乡"
},
一个 value 可能对应多个 key(游戏 Music Room 编号、游戏关卡编号、专辑编号等),用|分隔。此外,COR 字段指向该映射表所对应的作品,可以用于判断原作的所属类型。这一点在下文中会讲解。
效率问题
API action=tablemapping 最大的问题是效率低下。由于 API 不支持一次请求多个映射表,这会制造严重的请求放大效应。根据jq工具的统计,目前 THBwiki 的音乐名映射表共有 73 个。每个映射表有日、中、英文版本,请求次数放大三倍。这意味着,获取全部映射表需 219 次 HTTPS 请求。可见,重用 HTTPS 连接是极为有必要的,否则光 TLS 握手就握几百次。最后,缓存请求结果同样是重要的,建议只在程序初始化阶段请求一次,之后便不再请求。否则,这上百个请求很容易使你的工具成为管理员的眼中钉。如果管理员一怒之下禁用 API,那就谁都不能用了。
$ jq ".tablemapping.schemes | length" < list.json
73
由于效率问题,这迫使开发者天堂有路你不走,地狱无门你偏行。在下文中,我们将了解比 action=tablemapping 更高效的两种歪门邪道。
难度 Normal:expandtemplates 展开模板
THBWiki 为规范各种信息的格式,要求使用音乐名模板、角色模板等工具。这实际上,这些模板已经是变相的标准库函数或者 API 端点了。但这些模板只能被 MediaWiki 使用。除非客户端拥有完整的 MediaWiki 解析器、数据库与源代码(这显然是不可能的),否则客户端无法解析 MediaWiki 模板。MediaWiki 考虑到这种情况,十分贴心得为开发者准备了 expandtemplates API,可以运行任意模板,堪称 Remote Procedure Call。
音乐名模板的语法,见帮助:音乐名模板。模板名称是 maction=list 得到的映射表名称前缀(不包括/日文, /中文, /英文 后缀),第一个参数是语言代码,第二个参数是映射表的 key。
使用例:
《东方妖妖梦》5 面关底 Boss 的音乐是什么?
$ curl -X POST --data-urlencode "action=expandtemplates" \
--data-urlencode "text={{妖妖梦音乐名|2|5-2}}" \
--data-urlencode "prop=wikitext" \
--data-urlencode "format=json" \
https://thwiki.cc/api.php 2>/dev/null | jq
系统返回:
{
"expandtemplates": {
"wikitext": "広有射怪鳥事 ~ Till When?"
}
}
音乐名模板摇身变成了神奇海螺,有问必答。
获取《东方红魔乡》音乐盒中的第一首歌曲的日、英、中文标题:
$ curl -X POST --data-urlencode "action=expandtemplates" \
--data-urlencode "text={{红魔乡音乐名|2|1}},{{红魔乡音乐名|4|1}},{{红魔乡音乐名|1|1}}" \
--data-urlencode "prop=wikitext" \
--data-urlencode "format=json" \
https://thwiki.cc/api.php 2>/dev/null | jq
系统返回:
{
"expandtemplates": {
"wikitext": "赤より紅い夢,A Dream More Scarlet than Red,比赤色更红的梦"
}
}
效率问题
连接重用与缓存:这个 API 任何人都可使用,无需登录。同理,为防止滥用 THBWiki 资源而使你的程序成为管理员的眼中钉(最后导致公共 API 被禁用,谁都不能用),请求 API 时建议保持连接上下文以重用 HTTPS 连接,不要握手几千次。查询后的结果应该存入本地文件或数据库,只在首次初始化时获取信息。
GET 与 POST 请求:POST请求只适用于超长请求。如果内容简短,GET请求是更合适的,以免阻碍 Web 服务器的缓存机制(即使目前不存在,理论上也可以加入,而 POST 请求在 HTTP 层面不可缓存):
curl 'https://thwiki.cc/api.php' -G \
-d 'action=expandtemplates' \
-d 'text={{红魔乡音乐名|2|1}},{{红魔乡音乐名|4|1}},{{红魔乡音乐名|1|1}}' \
-d 'prop=wikitext' -d 'format=json' \
2>/dev/null | jq
获取大量模板内容:如需获取大量模板内容,与其发送多个 GET 请求,不如构造一个包含大量模板请求的字符串,用没有歧义的特殊符号分割(如 Newline、Tab、Unicode Zero-Width Space U+200B),然后发生 POST 请求。如果请求过长,则分批查询。用这个方法,一分钟很容易获得上千个展开结果。GET 字符串不能超过数千个字符,而 POST 字符串长度可达 MiB 级别。
模板展开是最灵活的数据获取方式,但是上文介绍的方法中,客户端仍需利用 maction=list 先得到映射表名称,获取所有日文映射表的完整内容(以获得所有的 key),然后构造wikitext以展开中文、英文的模板,最后再切分字符串获得结果。这依然会产生至少 75 次请求,效率低下。
难度 Hard:展开解析函数模板
在 MediaWiki 中,除了普通的 HTML 模板,还存在一些内建函数。在 THBWiki,系统的大量扩展能实现极其强大的编程功能,具体细节可查阅帮助:管理映射方案与解析函数了解详情。以下,我们介绍几个重要的函数:
getmapname 与 getmaparray 函数
getmapname:获取某个分类的映射表,相当于 maction=list。
使用例:
$ wikitext='{{#getmapname:音乐名日文}}'
$ curl -X POST --data-urlencode "action=expandtemplates" \
--data-urlencode "text=$wikitext" \
--data-urlencode "prop=wikitext" \
--data-urlencode "format=json" \
https://thwiki.cc/api.php 2>/dev/null | jq
系统返回:
{
"expandtemplates": {
"wikitext": "红魔乡音乐名/日文\n花映塚音乐名/日文\n莲台野夜行音乐名/日文\n蓬莱人形音乐名/日文\n辉针城音乐名/日文\n风神录音乐名/日文\n..."
}
}
getmaparray:获取某个映射表中的 key-value,相当于 maction=browse。
使用例:
$ wikitext='{{#getmaparray:红魔乡音乐名/日文}}'
$ curl -X POST --data-urlencode "action=expandtemplates" \
--data-urlencode "text=$wikitext" \
--data-urlencode "prop=wikitext" \
--data-urlencode "format=json" \
https://thwiki.cc/api.php 2>/dev/null | jq
系统返回:
{
"expandtemplates": {
"wikitext": "音乐名日文\n东方红魔乡\n缺少参数\n6\n红魔乡音乐名\n赤より紅い夢\nほおずきみたいに紅い魂\n妖魔夜行\n..."
}
}
arraymap 函数
以上两个操作与 action=tablemapping 有何区别?最重要的区别,就是 Wikitext 模板可以嵌套。一个函数的返回结果,可以作为下一个函数的输入,从而实现非常强大的数据处理效果。
其中,最为强大的函数是arraymap,它可以按将输入字符串INPUT_EXPRESSION利用分隔符INPUT_SEPARATOR进行分割。分割结果中的每一项,赋予变量名VARIABLE_NAME,然后将每一项加工为OUTPUT_EXPRESSION的值,最后将这些结果再利用分隔符 OUTPUT_SEPARATOR 进行分割,输出处理后的字符串。
{{#arraymap:
INPUT_EXPRESSION|
INPUT_SEPARATOR|
VARIABLE_NAME|
OUTPUT_EXPRESSION|
OUTPUT_SEPARATOR
}}因此,它相当于函数式编程中的array.map(lambda var: do_something(var)),或者过程式编程中的foreach。利用这一函数,可以轻松循环处理上一个函数的所有结果。例如,我们可以对音乐名日文的每一个结果进一步应用getmaparray,获得其中的乐曲名称,从而得到所有映射表中的所有乐曲名称。
$ wikitext=$(cat <<'EOF'
{{#arraymap:
{{#getmapname:音乐名日文}}|
\n|
tablename|
{{#getmaparray:tablename|\n|pair}}|
,
}}
EOF
)
$ curl -X POST --data-urlencode "action=expandtemplates" \
--data-urlencode "text=$wikitext" \
--data-urlencode "prop=wikitext" \
--data-urlencode "format=json" \
https://thwiki.cc/api.php 2>/dev/null | jq
系统返回:
{
"expandtemplates": {
"wikitext": "!CAT 音乐名日文\n!COR 东方红魔乡\n!DEF 缺少参数\n!SOR 6\n!TEM 红魔乡音乐名\n1 赤より紅い夢\n1-1 ほおずきみたいに紅い魂\n1-2 妖魔夜行\n2 ほおずきみたいに紅い魂\n...!CAT 音乐名日文\n!COR 东方花映塚\n!DEF 缺少参数\n!SOR 9\n!TEM 花映塚音乐名\n1 花映塚 ~ Higan Retour\n2 春色小径 ~ Colorful Path\n3 オリエンタルダークフライト\n4 フラワリングナイト\n5 東方妖々夢 ~ Ancient Temple\n6 狂気の瞳 ~ Invisible Full Moon\n7 おてんば恋娘の冒険\n8...!CAT 音乐名日文\n!COR 莲台野夜行\n!DEF 缺少参数\n!SOR 902\n!TEM 莲台野夜行音乐名\n1 夜のデンデラ野を逝く\n2 少女秘封倶楽部\n3 東方妖々夢 ~ Ancient Temple\n4 古の冥界寺\n5 幻視の夜 ~ Ghostly Eyes\n6 魔術師メリー\n7 月の妖鳥、化猫の幻\n8 過去の花 ~ Fairy of Flower\n9 魔法少女十字軍\n10 少女幻葬 ~ Necro-Fantasy\n11 幻想の永遠祭,!CAT 音乐名日文\n!COR 蓬莱人形\n!DEF 缺少参数\n!SOR 901\n!TEM 蓬莱人形音乐名\n1 蓬莱伝説\n2 二色蓮花蝶 ~ Red and White\n3 桜花之恋塚 ~ Japanese Flower\n4 明治十七年の上海アリス\n5 東方怪奇談\n6 エニグマティクドール\n7 サーカスレヴァリエ\n8 人形の森\n9 Witch of Love Potion\n10 リーインカーネイション\n11 U.N.オーエンは彼女なのか?\n12 永遠の巫女\n13 空飛ぶ巫女の不思議な毎日,!CAT 音乐名日文\n!COR 东方辉针城\n...“
}
}
效率问题
可以看到,这种数据获取方式效率极高,相当于利用 Mediawiki 模板作为一种数据查询编程语言,把整个数据库直接转储出来了。整个乐曲映射表可以仅用 3 个 HTTP 请求全部获取,如果把以上代码重复三次,那么可以同时请求音乐名日文、音乐名英文、音乐名中文,只需 1 个请求即可获取全部数据。
难度 Lunatic:用解析函数手工构造 JSON 字符串
在以上教程中,我们利用 MediaWiki 解析函数成功获取了所有映射表中的所有字段。然而,应用程序依然需要对 API 返回的字符串进行后处理(如切分字符串),才能使用。如果同时请求多种语言,应用程序还需要额外的逻辑,将同一份映射表的不同语言版本关联起来。如果我们让 MediaWiki 返回的数据更结构化,就能大幅简化应用逻辑。
回忆一下,我们现有的模板用getmapname获取所有的日文模板名,切分出每一个模板名tablename,然后对每一个模板名,使用getmaparray获取其中的全部键、值,一行一对。
{{#arraymap:
{{#getmapname:音乐名日文}}|
\n|
tablename|
{{#getmaparray:tablename|\n|pair}}|
,
}}切分每一行
我们可以进一步改进 MediaWiki 模板,对每一个映射表中的键、值切分为一行一个的形式。为了验证切分是否正确,我们用<item></item>包围每一行的内容,改进后的程序如下:
$ wikitext=$(cat <<'EOF'
{{#arraymap:
{{#getmapname:音乐名日文}}|
\n|
tablename|
{{#arraymap:
{{#getmaparray:tablename|\n|pair}}|
\n|
line |
<item>line</item> |
\n
}}|
,
}}
EOF
)
$ curl -X POST --data-urlencode "action=expandtemplates" \
--data-urlencode "text=$wikitext" \
--data-urlencode "prop=wikitext" \
--data-urlencode "format=json" \
https://thwiki.cc/api.php 2>/dev/null | jq
系统返回:
{
"expandtemplates": {
"wikitext": "<item>!CAT 音乐名日文</item>\n<item>!COR 东方红魔乡</item>\n<item>!DEF 缺少参数</item>\n<item>!SOR 6</item>\n<item>!TEM 红魔乡音乐名</item>\n<item>1 赤より紅い夢</item>\n<item>1-1 ほおずきみたいに紅い魂</item>\n<item>1-2 妖魔夜行</item>\n<item>2 ほおずきみたいに紅い魂</item>\n<item>2-1 ルーネイトエルフ</item>\n<item>2-2 おてんば恋娘</item>\n<item>3 妖魔夜行</item>\n<item>3-1 上海紅茶館 ~ Chinese Tea</item>\n<item>3-2 明治十七年の上海アリス</item>\n<item>4 ルーネイトエルフ</item>\n<item>4-1 ヴワル魔法図書館</item>\n<item>4-2 ラクトガール ~ 少女密室</item>\n<item>5 おてんば恋娘</item>\n<item>5-1 メイドと血の懐中時計</item>\n<item>..."
}
}
可以看到,代码逻辑正确,每一行都被准确切分。
切分键与值
现在我们更进一步,我们对切分出的每一行进一步切分,从而可以使数据以 key: value 的格式显示出来。要实现这个目标,我们可以使用pos函数确定字符串中第一个空格的位置,然后用sub获得子字符串:这个空格位置之前的子字符串一律视为 key,这个空格之后的子字符串一律视为 value。为了提升代码可读性,我们可以用vardefine函数将切分结果存储至临时变量。我们还需要使用<nowiki>保护这个空格本身,防止其被解析器自动删除。
{{#vardefine:key_value_boundary | {{#pos:line|<nowiki> </nowiki>}}}}
{{#vardefine:rawkey | {{#sub:line|0|{{#var:key_value_boundary}}}}}}
{{#vardefine:rawvalue | {{#sub:line|{{#var:key_value_boundary}}}}}}
{{#var:rawkey}}: {{#var:rawvalue}}事实上,rawvalue的位置并非{{#sub:line|{{#var:key_value_boundary}}而应该是{{#sub:line|{{#var:key_value_boundary + 1}}。然而,在模板中进行算术运算会使得代码更加复杂,所以这个错误可以将错就错——正如上文所说,rawvalue变量被赋值时,解析器会忽略参数中的空白符。
完整代码如下:
$ wikitext=$(cat <<'EOF'
{{#arraymap:
{{#getmapname:音乐名日文}}|
\n|
tablename|
{{#arraymap:
{{#getmaparray:tablename|\n|pair}}|
\n|
line |
{{#vardefine:key_value_boundary | {{#pos:line|<nowiki> </nowiki>}}}}
{{#vardefine:rawkey | {{#sub:line|0|{{#var:key_value_boundary}}}}}}
{{#vardefine:rawvalue | {{#sub:line|{{#var:key_value_boundary}}}}}}
{{#var:rawkey}}: {{#var:rawvalue}} |
\n
}}|
,
}}
EOF
)
$ curl -X POST --data-urlencode "action=expandtemplates" \
--data-urlencode "text=$wikitext" \
--data-urlencode "prop=wikitext" \
--data-urlencode "format=json" \
https://thwiki.cc/api.php 2>/dev/null | jq
系统返回:
{
"expandtemplates": {
"wikitext": "!CAT: 音乐名日文\n!COR: 东方红魔乡\n!DEF: 缺少参数\n!SOR: 6\n!TEM: 红魔乡音乐名\n1: 赤より紅い夢\n1-1: ほおずきみたいに紅い魂\n1-2: 妖魔夜行\n2: ほおずきみたいに紅い魂\n2-1: ルーネイトエルフ\n2-2: おてんば恋娘\n3: 妖魔夜行\n3-1: 上海紅茶館 ~ Chinese Tea\n3-2: 明治十七年の上海アリス\n4: ルーネイトエルフ\n4-1: ヴワル魔法図書館\n4-2: ラクトガール ~ 少女密室\n5: おてんば恋娘\n5-1: メイドと血の懐中時計\n5-2: 月時計 ~ ルナ・ダイアル\n..."
}
}
构造 JSON 键与值格式
现在的键与值已经对机器十分友好了,我们接下来只需要继续略微改进输出格式。
转义引号
我们将key与value的字符串用引号包围,为了防止字符串本身含有引号的情况,使用replace函数将所有的"替换为\"
{{#vardefine:key_value_boundary | {{#pos:line|<nowiki> </nowiki>}}}}
{{#vardefine:rawkey | {{#sub:line|0|{{#var:key_value_boundary}}}}}}
{{#vardefine:rawvalue | {{#sub:line|{{#var:key_value_boundary}}}}}}
{{#vardefine:key | {{#replace:{{#var:rawkey}}|"|\"}}}}
{{#vardefine:value | {{#replace:{{#var:rawvalue}}|"|\"}}}}
"{{#var:key}}": "{{#var:value}}"加入方括号与花括号
接下来,我们将外循环(循环变量tablename)插入如下字符,包围内循环产生的key: value字符串:
[
{
"tablename", {
inner string
}
}
]
即:
[
{{#arraymap:
{{#getmapname:音乐名日文}}|
\n|
tablename|
{
"tablename": {
{{#arraymap:
{{#getmaparray:tablename|\n|pair}}|
\n|
line |
.... |
,
}}
}
}|
,
}}
]第一个成功的 JSON 输出
完整代码如下:
$ wikitext=$(cat <<'EOF'
[
{{#arraymap:
{{#getmapname:音乐名日文}}|
\n|
tablename|
{
"tablename": {
{{#arraymap:
{{#getmaparray:tablename|\n|pair}}|
\n|
line |
{{#vardefine:key_value_boundary | {{#pos:line|<nowiki> </nowiki>}}}}
{{#vardefine:rawkey | {{#sub:line|0|{{#var:key_value_boundary}}}}}}
{{#vardefine:rawvalue | {{#sub:line|{{#var:key_value_boundary}}}}}}
{{#vardefine:key | {{#replace:{{#var:rawkey}}|"|\"}}}}
{{#vardefine:value | {{#replace:{{#var:rawvalue}}|"|\"}}}}
"{{#var:key}}": "{{#var:value}}" |
,
}}
}
}|
,
}}
]
EOF
)
$ curl -X POST --data-urlencode "action=expandtemplates" \
--data-urlencode "text=$wikitext" \
--data-urlencode "prop=wikitext" \
--data-urlencode "format=json" \
https://thwiki.cc/api.php 2>/dev/null | jq
系统返回:
{
"expandtemplates": {
"wikitext": "[\n {\n \"红魔乡音乐名/日文\": {\n \"!CAT\": \"音乐名日文\",\"!COR\": \"东方红魔乡\",\"!DEF\": \"缺少参数\",\"!SOR\": \"6\",\"!TEM\": \"红魔乡音乐名\",\"1\": \"赤より紅い夢\",\"1-1\": \"ほおずきみたいに紅い魂\",\"1-2\": \"妖魔夜行\",\"2\": \"ほおずきみたいに紅い魂\",\"2-1\": \"ルーネイトエルフ\",\"2-2\": \"おてんば恋娘\",\"3\": \"妖魔夜行\",\"3-1\": \"上海紅茶館 ~ Chinese Tea\",\"3-2\": \"明治十七年の上海アリス\",\"4\": \"ルーネイトエルフ\",\"4-1\": \"ヴワル魔法図書館\",\"4-2\": \"ラクトガール ~ 少女密室\",\"5\": \"おてんば恋娘\",\"5-1\": \"メイドと血の懐中時計\",\"5-2\": \"月時計 ~ ルナ・ダイアル\",\"6\": \"上海紅茶館 ~ Chinese Tea\",\"6-1\": \"ツェペシュの幼き末裔\",\"6-2\": \"亡き王女の為のセプテット\",\"7\": \"明治十七年の上海アリス\",\"7-1\": \"魔法少女達の百年祭\",\"7-2\": \"U.N.オーエンは彼女なのか?\",\"8\": \"ヴワル魔法図書館\",\"9\": \"ラクトガール ~ 少女密室\",\"10\": \"メイドと血の懐中時計\",\"11\": \"月時計 ~ ルナ・ダイアル\",\"12\": \"ツェペシュの幼き末裔\",\"13\": \"亡き王女の為のセプテット\",\"14\": \"魔法少女達の百年祭\",\"15\": \"U.N.オーエンは彼女なのか?\",\"16\": \"紅より儚い永遠\",\"17\": \"紅楼 ~ Eastern Dream...\",\"E\": \"紅より儚い永遠\",\"S\": \"紅楼 ~ Eastern Dream...\",\"T\": \"赤より紅い夢\"\n }\n },............\n]"
}
}
我们成功将输出结果构造为了一个符合 JSON 标准的字符串。由于这个字符串本身是依靠外层 JSON 包装的,这个内层字符串本身可以作为 JSON 再解析一次。这可以靠 jq 的 fromjson过滤器实现。
$ curl -X POST --data-urlencode "action=expandtemplates" \
--data-urlencode "text=$wikitext" \
--data-urlencode "prop=wikitext" \
--data-urlencode "format=json" \
https://thwiki.cc/api.php 2>/dev/null | jq ".expandtemplates.wikitext | fromjson"
系统返回:
[
{
"红魔乡音乐名/日文": {
"!CAT": "音乐名日文",
"!COR": "东方红魔乡",
"!DEF": "缺少参数",
"!SOR": "6",
"!TEM": "红魔乡音乐名",
"1": "赤より紅い夢",
"1-1": "ほおずきみたいに紅い魂",
"1-2": "妖魔夜行",
"2": "ほおずきみたいに紅い魂",
"2-1": "ルーネイトエルフ",
"2-2": "おてんば恋娘",
...
}
},
{
"花映塚音乐名/日文": {
"!CAT": "音乐名日文",
"!COR": "东方花映塚",
"!DEF": "缺少参数",
"!SOR": "9",
"!TEM": "花映塚音乐名",
"1": "花映塚 ~ Higan Retour",
"2": "春色小径 ~ Colorful Path",
"3": "オリエンタルダークフライト",
"4": "フラワリングナイト",
"5": "東方妖々夢 ~ Ancient Temple",
...
}
},
...
}
构造多语言 JSON
现在,我们已经成功利用维基模板这一“函数式编程语言”,将映射表扩展的全部日文乐曲数据构造为了 JSON 字符串。接下来,我们将这一成果扩展到多语言版本。
要做到这一点,我们可以利用explode函数将模板的前缀提取。接着,我们再手工构造三个字符串,分别是该模板的日文、中文、英文版本:
{{#vardefine:table_basename | {{#explode:tablename|/|0}}}}
{{#vardefine:tablename_ja | {{#var:table_basename}}/日文}}
{{#vardefine:tablename_en | {{#var:table_basename}}/英文}}
{{#vardefine:tablename_zh_hans | {{#var:table_basename}}/中文}}然后,我们在模板中加入更多花括号,使得输出符合这样的格式:
[
{
"table_basename", {
"ja": { },
"en": { },
"zh-hans": { },
}
}
]
即:
[
{{#arraymap:
{{#getmapname:音乐名日文}}|
\n|
tablename|
{{#vardefine:table_basename | {{#explode:tablename|/|0}}}}
{{#vardefine:tablename_ja | {{#var:table_basename}}/日文}}
{{#vardefine:tablename_en | {{#var:table_basename}}/英文}}
{{#vardefine:tablename_zh_hans | {{#var:table_basename}}/中文}}
{
"{{#var:table_basename}}": {
"ja": {
{{#arraymap:
{{#getmaparray:{{#var:tablename_ja}}|\n|pair}}|
\n|
line |
.... |
,
}}
},
"en": {
{{#arraymap:
{{#getmaparray:{{#var:tablename_en}}|\n|pair}}|
\n|
line |
.... |
,
}}
},
"zh-hans": {
{{#arraymap:
{{#getmaparray:{{#var:tablename_zh_hans}}|\n|pair}}|
\n|
line |
.... |
,
}}
}
}
}|
,
}}
]然后我们再将提取 key-value 的所有内循环命令重复,三回啊三回!
$ wikitext=$(cat <<'EOF'
[
{{#arraymap:
{{#getmapname:音乐名日文}}|
\n|
tablename|
{{#vardefine:table_basename | {{#explode:tablename|/|0}}}}
{{#vardefine:tablename_ja | {{#var:table_basename}}/日文}}
{{#vardefine:tablename_en | {{#var:table_basename}}/英文}}
{{#vardefine:tablename_zh_hans | {{#var:table_basename}}/中文}}
{
"{{#var:table_basename}}": {
"ja": {
{{#arraymap:
{{#getmaparray:{{#var:tablename_ja}}|\n|pair}}|
\n|
line |
{{#vardefine:key_value_boundary | {{#pos:line|<nowiki> </nowiki>}}}}
{{#vardefine:rawkey | {{#sub:line|0|{{#var:key_value_boundary}}}}}}
{{#vardefine:rawvalue | {{#sub:line|{{#var:key_value_boundary}}}}}}
{{#vardefine:key | {{#replace:{{#var:rawkey}}|"|\"}}}}
{{#vardefine:value | {{#replace:{{#var:rawvalue}}|"|\"}}}}
"{{#var:key}}": "{{#var:value}}" |
,
}}
},
"en": {
{{#arraymap:
{{#getmaparray:{{#var:tablename_en}}|\n|pair}}|
\n|
line |
{{#vardefine:key_value_boundary | {{#pos:line|<nowiki> </nowiki>}}}}
{{#vardefine:rawkey | {{#sub:line|0|{{#var:key_value_boundary}}}}}}
{{#vardefine:rawvalue | {{#sub:line|{{#var:key_value_boundary}}}}}}
{{#vardefine:key | {{#replace:{{#var:rawkey}}|"|\"}}}}
{{#vardefine:value | {{#replace:{{#var:rawvalue}}|"|\"}}}}
"{{#var:key}}": "{{#var:value}}" |
,
}}
},
"zh-hans": {
{{#arraymap:
{{#getmaparray:{{#var:tablename_zh_hans}}|\n|pair}}|
\n|
line |
{{#vardefine:key_value_boundary | {{#pos:line|<nowiki> </nowiki>}}}}
{{#vardefine:rawkey | {{#sub:line|0|{{#var:key_value_boundary}}}}}}
{{#vardefine:rawvalue | {{#sub:line|{{#var:key_value_boundary}}}}}}
{{#vardefine:key | {{#replace:{{#var:rawkey}}|"|\"}}}}
{{#vardefine:value | {{#replace:{{#var:rawvalue}}|"|\"}}}}
"{{#var:key}}": "{{#var:value}}" |
,
}}
}
}
}|
,
}}
]
EOF
)
最终成果
使用例(需要运行上小结末尾的bash变量定义):
$ curl -X POST --data-urlencode "action=expandtemplates" \
--data-urlencode "text=$wikitext" \
--data-urlencode "prop=wikitext" \
--data-urlencode "format=json" \
https://thwiki.cc/api.php 2>/dev/null | jq ".expandtemplates.wikitext | fromjson"
系统返回:
[
{
"红魔乡音乐名": {
"ja": {
"!CAT": "音乐名日文",
"!COR": "东方红魔乡",
"!DEF": "缺少参数",
"!SOR": "6",
"!TEM": "红魔乡音乐名",
"1": "赤より紅い夢",
"1-1": "ほおずきみたいに紅い魂",
"1-2": "妖魔夜行",
...
},
"en": {
"!CAT": "音乐名英文",
"!DEF": "缺少参数",
"!TEM": "红魔乡音乐名",
"1": "A Dream More Scarlet than Red",
"1-1": "A Soul as Scarlet as a Ground Cherry",
"1-2": "Apparitions Stalk the Night",
...
},
"zh-hans": {
"!CAT": "音乐名中文",
"!DEF": "缺少参数",
"!TEM": "红魔乡音乐名",
"1": "比赤色更红的梦",
"1-1": "如鬼灯般的红色之魂",
"1-2": "妖魔夜行",
...
}
}
},
{
"花映塚音乐名": {
"ja": {
"!CAT": "音乐名日文",
"!COR": "东方花映塚",
"!DEF": "缺少参数",
"!SOR": "9",
"!TEM": "花映塚音乐名",
"1": "花映塚 ~ Higan Retour",
"2": "春色小径 ~ Colorful Path",
"3": "オリエンタルダークフライト",
...
},
"en": {
"!CAT": "音乐名英文",
"!DEF": "缺少参数",
"!TEM": "花映塚音乐名",
"1": "Flower Reflecting Mound ~ Higan Retour",
"2": "Spring Lane ~ Colorful Path",
"3": "Oriental Dark Flight",
...
},
"zh-hans": {
"!CAT": "音乐名中文",
"!DEF": "缺少参数",
"!TEM": "花映塚音乐名",
"1": "花映塚 ~ Higan Retour",
"2": "春色小径 ~ Colorful Path",
"3": "Oriental Dark Flight",
...
}
}
},
{
"莲台野夜行音乐名": {
"ja": {
"!CAT": "音乐名日文",
"!COR": "莲台野夜行",
"!DEF": "缺少参数",
"!SOR": "902",
"!TEM": "莲台野夜行音乐名",
"1": "夜のデンデラ野を逝く",
"2": "少女秘封倶楽部",
"3": "東方妖々夢 ~ Ancient Temple",
...
},
"en": {
"!CAT": "音乐名英文",
"!DEF": "缺少参数",
"!TEM": "莲台野夜行音乐名",
"1": "Passing on Through the Dendera Fields in the Night",
"2": "Girls' Sealing Club",
"3": "Eastern Ghostly Dream ~ Ancient Temple",
...
},
"zh-hans": {
"!CAT": "音乐名中文",
"!DEF": "缺少参数",
"!TEM": "莲台野夜行音乐名",
"1": "走在夜晚的莲台野",
"2": "少女秘封俱乐部",
"3": "东方妖妖梦 ~ Ancient Temple",
...
}
}
},
{
"蓬莱人形音乐名": {
"ja": {
"!CAT": "音乐名日文",
"!COR": "蓬莱人形",
"!DEF": "缺少参数",
"!SOR": "901",
"!TEM": "蓬莱人形音乐名",
"1": "蓬莱伝説",
"2": "二色蓮花蝶 ~ Red and White",
"3": "桜花之恋塚 ~ Japanese Flower",
...
},
"en": {
"!CAT": "音乐名英文",
"!DEF": "缺少参数",
"!TEM": "蓬莱人形音乐名",
"1": "Legend of Hourai",
"2": "Dichromatic Lotus Butterfly ~ Red and White",
"3": "Lovely Mound of Cherry Blossoms ~ Japanese Flower",
...
},
"zh-hans": {
"!CAT": "音乐名中文",
"!DEF": "缺少参数",
"!TEM": "蓬莱人形音乐名",
"1": "蓬莱传说",
"2": "二色莲花蝶 ~ Red and White",
"3": "樱花之恋塚 ~ Japanese Flower",
...
}
}
},
...
]
效果拔群!
query, list=categorymembers 获取全部含有音乐的原作
THBWiki 所有的音乐都属于 Category:Music_Room 分类,而 MediaWiki 提供了 query 这一 API 获取同一分类下的全部页面。
使用例:
$ curl 'https://thwiki.cc/api.php' -G \
-d 'action=query' -d 'list=categorymembers' \
-d 'cmlimit=50' -d 'cmtitle=Category:Music_Room' \
-d 'format=json' \
2>/dev/null | jq
系统返回:
{
"batchcomplete": "",
"query": {
"categorymembers": [
{
"pageid": 179698,
"ns": 0,
"title": "东方兽王园/Music"
},
{
"pageid": 49061,
"ns": 0,
"title": "东方凭依华/Music"
},
{
"pageid": 151115,
"ns": 0,
"title": "东方刚欲异闻/Music"
},
...
query, prop=revisions 获取 Music Room 页面源代码
MediaWiki 的 query, prop=revisions API 可以获取任意页面的源代码。
每一个原作的 Music Room 都使用了 Music Room 模板进行编写,这似乎是 THBWiki 特有的一种扩展语法,而不是 MediaWiki 的模板。不过因为语法简单,是简单的 K-V 格式,并不难自己解析。其文件见帮助:翻译表。
使用例:
$ curl 'https://thwiki.cc/api.php' -G \
-d 'action=query' -d 'prop=revisions' \
-d 'rvprop=content' -d 'rvslots=main' \
-d 'titles=东方兽王园/Music' \
-d 'format=json' -d 'formatversion=2' \
2>/dev/null | jq
系统返回:
{
"batchcomplete": true,
"query": {
"pages": [
{
"pageid": 179698,
"ns": 0,
"title": "东方兽王园/Music",
"revisions": [
{
"slots": {
"main": {
"contentmodel": "wikitext",
"contentformat": "text/x-wiki",
"content": "__MUSICROOM__\n*本词条内容为官方游戏TH19'''东方兽王园'''的Music Room\n*如果发现翻译问题可进行改正\n*翻译:[[用户:140706inu|140706inu]]\n__NOTOC__\n\n== MusicRoom ==\ncategory\n标题画面\ntitleJA = {{兽王园音乐名|2|1}}\ntitleZH = [[{{兽王园音乐名|1|1}}]]\ncomposer = ZUN\nmp3 = {{音乐室音频文件|th19_01.mp3}}\nja\n タイトル画面のテーマです。\n \n 獣っぽく力強いタイトル画面にしました。\n 重厚さと儚さ、力強さと無力さ、そういった矛盾する要素が\n 同居した獣の知性を表現しています。\nzh\n 标题画面主题曲。\n \n 本次采用了颇具兽性、充满力量感的标题画面。\n 厚重与纤弱、强大与无力,\n 这些相互矛盾的特征表现着集群而居的兽类的智慧。\ncategory\n[[博丽灵梦]]\n剧情模式前期主题曲\ntitleJA = {{兽王园音乐名|2|2}}\ntitleZH = [[{{兽王园音乐名|1|2}}]]\ncomposer = ZUN\nmp3 = {{音乐室音频文件|th19_02.mp3}}\nja\n 博麗 霊夢 & ストーリーモード序盤のテーマです。\n \n
...
番外篇:用 Wikidata 获取东方原作版本号与日、中、英标题
东方原作的作品名有时用日文表示、有时用英文表示、有时用中文表示。开发相关工具时,建议将所有原作统一用版本号表示。
那么如何获得原作版本与标题的映射表呢?如果解析文本,那方法有很多,但都比较费力。幸运的是,维基百科背后有一个强大的“维基数据”(Wikidata),这可能是整个人类互联网上最大的公开数据库之一,几乎包罗万象(许多维基百科新条目的资料都是用数据形式,而非模板表示的),其中也自然包括东方 Project 作品信息。
只需要使用 SPARQL 语言向 Wikidata 的 API (https://query.wikidata.org/sparql) 提交一个查询,就可以获得东方作品信息了。可以使用页面 https://query-main.wikidata.org/ 在浏览器中测试。
使用例:
SELECT ?game ?thRelease ?titleJa ?titleEn ?titleZh ?titleZhHans WHERE {
wd:Q907907 p:P527 [
ps:P527 ?game ;
pq:P1545 ?thReleaseValue
] .
BIND(CONCAT("TH", ?thReleaseValue) as ?thRelease)
OPTIONAL { ?game rdfs:label ?titleJa FILTER(LANG(?titleJa) = "ja") }
OPTIONAL { ?game rdfs:label ?titleEn FILTER(LANG(?titleEn) = "en") }
OPTIONAL { ?game rdfs:label ?titleZh FILTER(LANG(?titleZh) = "zh") }
OPTIONAL { ?game rdfs:label ?titleZhHans FILTER(LANG(?titleZhHans) = "zh-hans") }
}
ORDER BY xsd:float(?thReleaseValue)
其中Q907907是《东方 Project》对象编号,P527是下属作品,P1545是下属作品的发行编号。
系统返回:
game thRelease titleJa titleEn titleZh titleZhHans http://www.wikidata.org/entity/Q5367731 TH1 東方靈異伝 〜 Highly Responsive to Prayers. Touhou Reiiden ~ Highly Responsive to Prayers 東方靈異傳 ~ Highly Responsive to Prayers. 东方灵异传 ~ Highly Responsive to Prayers. http://www.wikidata.org/entity/Q5368031 TH2 東方封魔録 〜 the Story of Eastern Wonderland. Touhou Fuumaroku ~ Story of Eastern Wonderland 東方封魔錄 ~ the Story of Eastern Wonderland. 东方封魔录 ~ the Story of Eastern Wonderland. http://www.wikidata.org/entity/Q5367726 TH3 東方夢時空 〜 Phantasmagoria of Dim.Dream. Touhou Yumejikuu ~ Phantasmagoria of Dimensional Dream 東方夢時空 ~ Phantasmagoria of Dim.Dream. 东方梦时空 ~ Phantasmagoria of Dim.Dream. http://www.wikidata.org/entity/Q5368459 TH4 東方幻想郷 〜 Lotus Land Story. Touhou Gensoukyou ~ Lotus Land Story 東方幻想鄉 ~ Lotus Land Story. 东方幻想乡 ~ Lotus Land Story. http://www.wikidata.org/entity/Q5367349 TH5 東方怪綺談 〜 Mystic Square. Touhou Kaikidan ~ Mystic Square 東方怪綺談 ~ Mystic Square. 东方怪绮谈 ~ Mystic Square. http://www.wikidata.org/entity/Q1075123 TH6 東方紅魔郷 〜 the Embodiment of Scarlet Devil. Touhou Koumakyou ~ The Embodiment of Scarlet Devil 東方紅魔鄉 ~ the Embodiment of Scarlet Devil. 东方红魔乡 ~ the Embodiment of Scarlet Devil. http://www.wikidata.org/entity/Q1042119 TH7 東方妖々夢 〜 Perfect Cherry Blossom. Touhou Youyoumu ~ Perfect Cherry Blossom 東方妖妖夢 ~ Perfect Cherry Blossom. 东方妖妖梦 ~ Perfect Cherry Blossom. http://www.wikidata.org/entity/Q1050071 TH7.5 東方萃夢想 〜 Immaterial and Missing Power. Touhou Suimusou ~ Immaterial and Missing Power 東方萃夢想 ~ Immaterial and Missing Power. 东方萃梦想 ~ Immaterial and Missing Power. http://www.wikidata.org/entity/Q1042155 TH8 東方永夜抄 〜 Imperishable Night. Touhou Eiyashou ~ Imperishable Night 東方永夜抄 ~ Imperishable Night. 东方永夜抄 ~ Imperishable Night. http://www.wikidata.org/entity/Q1050849 TH9 東方花映塚 〜 Phantasmagoria of Flower View. Touhou Kaeidzuka ~ Phantasmagoria of Flower View 東方花映塚 ~ Phantasmagoria of Flower View. 东方花映冢 ~ Phantasmagoria of Flower View. http://www.wikidata.org/entity/Q1050387 TH9.5 東方文花帖 〜 Shoot the Bullet. Touhou Bunkachou ~ Shoot the Bullet 東方文花帖 ~ Shoot the Bullet. 东方文花帖 ~ Shoot the Bullet. http://www.wikidata.org/entity/Q1045759 TH10 東方風神録 〜 Mountain of Faith. Touhou Fuujinroku ~ Mountain of Faith 東方風神錄 ~ Mountain of Faith. 东方风神录 ~ Mountain of Faith. http://www.wikidata.org/entity/Q5228137 TH10.5 東方緋想天 〜 Scarlet Weather Rhapsody. Touhou Hisouten ~ Scarlet Weather Rhapsody 東方緋想天 ~ Scarlet Weather Rhapsody. 东方绯想天 ~ Scarlet Weather Rhapsody. http://www.wikidata.org/entity/Q867949 TH11 東方地霊殿 〜 Subterranean Animism. Touhou Chireiden ~ Subterranean Animism 東方地靈殿 ~ Subterranean Animism. 东方地灵殿 ~ Subterranean Animism. http://www.wikidata.org/entity/Q860055 TH12 東方星蓮船 〜 Undefined Fantastic Object. Touhou Seirensen ~ Undefined Fantastic Object 東方星蓮船 ~ Undefined Fantastic Object. 东方星莲船 ~ Undefined Fantastic Object. http://www.wikidata.org/entity/Q1050081 TH12.3 東方非想天則 〜 超弩級ギニョルの謎を追え Touhou Hisoutensoku 东方非想天则 ~ 追寻特大型人偶之谜 东方非想天则 ~ 追寻特大型人偶之谜 http://www.wikidata.org/entity/Q867001 TH12.5 ダブルスポイラー 〜 東方文花帖 Double Spoiler ~ Touhou Bunkachou Double Spoiler ~ 東方文花帖 Double Spoiler ~ 东方文花帖 http://www.wikidata.org/entity/Q867003 TH12.8 妖精大戦争 〜 東方三月精 Yousei Daisensou ~ Touhou Sangetsusei 妖精大戰爭 ~ 東方三月精 妖精大战争 ~ 东方三月精 http://www.wikidata.org/entity/Q1051713 TH13 東方神霊廟 〜 Ten Desires. Touhou Shinreibyou ~ Ten Desires 東方神靈廟 ~ Ten Desires. 东方神灵庙 ~ Ten Desires. http://www.wikidata.org/entity/Q5899909 TH13.5 東方心綺楼 〜 Hopeless Masquerade. Touhou Shinkirou ~ Hopeless Masquerade 東方心綺樓 ~ Hopeless Masquerade. http://www.wikidata.org/entity/Q13218187 TH14 東方輝針城 〜 Double Dealing Character. Touhou Kishinjou ~ Double Dealing Character 東方輝針城 ~ Double Dealing Character. http://www.wikidata.org/entity/Q16866101 TH14.3 弾幕アマノジャク 〜 Impossible Spell Card. Danmaku Amanojaku ~ Impossible Spell Card 彈幕天邪鬼 ~ Impossible Spell Card http://www.wikidata.org/entity/Q19364189 TH14.5 東方深秘録 〜 Urban Legend in Limbo. Touhou Shinpiroku ~ Urban Legend in Limbo 东方深秘录 ~ Urban Legend in Limbo. http://www.wikidata.org/entity/Q19891042 TH15 東方紺珠伝 〜 Legacy of Lunatic Kingdom. Touhou Kanjuden ~ Legacy of Lunatic Kingdom 東方紺珠傳 ~ Legacy of Lunatic Kingdom. http://www.wikidata.org/entity/Q28689822 TH15.5 東方憑依華 〜 Antinomy of Common Flowers. Touhou Hyouibana ~ Antinomy of Common Flowers 東方憑依華 ~ Antinomy of Common Flowers. http://www.wikidata.org/entity/Q30899159 TH16 東方天空璋 〜 Hidden Star in Four Seasons. Touhou Tenkuushou ~ Hidden Star in Four Seasons 東方天空璋 ~ Hidden Star in Four Seasons. http://www.wikidata.org/entity/Q55652942 TH16.5 秘封ナイトメアダイアリー 〜 Violet Detector. Hifuu Nightmare Diary ~ Violet Detector 秘封夢魘日記 ~ Violet Detector. http://www.wikidata.org/entity/Q63208650 TH17 東方鬼形獣 〜 Wily Beast and Weakest Creature. Touhou Kikeijuu ~ Wily Beast and Weakest Creature 東方鬼形獸 ~ Wily Beast and Weakest Creature. http://www.wikidata.org/entity/Q70207024 TH17.5 東方剛欲異聞 〜 水没した沈愁地獄 Touhou Gouyoku Ibun ~ Sunken Fossil World 东方刚欲异闻 ~ 被水淹没的沉愁地狱 http://www.wikidata.org/entity/Q105707774 TH18 東方虹龍洞 〜 Unconnected Marketeers. Touhou Kouryuudou ~ Unconnected Marketeers 東方虹龍洞 ~ Unconnected Marketeers. http://www.wikidata.org/entity/Q113195313 TH18.5 バレットフィリア達の闇市場 〜 100th Black Market. Barettofiriatachi no Yami-Ichiba ~ 100th Black Market 弹幕狂们的黑市 ~ 100th Black Market. http://www.wikidata.org/entity/Q118255366 TH19 東方獣王園 〜 Unfinished Dream of All Living Ghost. Touhou Juuouen ~ Unfinished Dream of All Living Ghost 東方獸王園 ~ Unfinished Dream of All Living Ghost. http://www.wikidata.org/entity/Q133858296 TH20 東方錦上京 〜 Fossilized Wonders. Touhou Kinjoukyou ~ Fossilized Wonders 东方锦上京_~_Fossilized_Wonders.
唯一的不足是,有些作品并没有简体中文(zh-hans)信息,需要本地做繁简转换;还有些标题不规范,例如《东方锦上京》的空格变成下划线,应该是有维基百科编辑者直接复制粘贴链接所致,需要本地替换一下。不过这类问题最佳的解决方法,是直接用维基百科账号编辑 Wikidata,修正全人类的知识库。
工作流程
总体工作流程如下:
- 使用方法 2 获取所有 Music Room 条目。
- 使用方法 3 对于每个 Music Room 条目,获取源代码。
- 解析 Music Room 数据的每个字段,将其转化为字典、数组等数据结构。
- 对于所有出现在 Music Room 的音乐名模板,那么使用方法 1 获取原曲名称。Music Room 数据中并没有英文标题,但可以通过自行替换参数中的语言代码解决。
- 将处理后的记录保存为 JSON、YAML、TOML、SQL 等格式本地存取,避免重复查询 THBWiki。此外,程序调试时,建议缓存请求参数完全一致的 URL 到本地,如果检测到请求一致,就直接重放结果。
角色与场景信息的提取
每一首音轨有可能对应一个或多个游戏场景,或者对应一个或多个角色,甚至还包括“角色限定的场景曲”(而非角色曲)。这几种信息同样应该作为“上下文”属性以机器可读的形式存储。
角色模板展开
按照编辑规范,每个角色都应该是一个维基链接。而为了方便编辑,链接文字(角色名称)可能是由帮助:角色名模板动态生成的。例如,音轨《小小的贤将》的 category 源代码为:
1面BOSS
[[{{Naz}}]]角色曲
我们需要先将{{Naz}}展开为[[娜兹玲]],然后再提取所有的维基链接,得到文本娜兹玲。
文本格式
在 Music Room 的category字段,一种有以下几种格式:
1 行,1 个场景,无角色:
"魔界6至9面主题曲"
"[[魔法森林]]场景用曲"
"神灵庙(场景)"
"原对话曲"
"对话曲1"
"对话用曲1"
"Ending画面主题曲"
"角色选择画面"
2 行,2 个场景,无角色
"1至4面\n地狱16至19面主题曲"
2 行,1 个场景,1 个角色
"5面主题曲\n[[神玉]]角色曲",
2 行,1 个场景,多个角色
"3面BOSS\n[[云山]]&[[云居一轮]]角色曲",
2 行,1 个角色,1 个场景,角色曲为“角色曲”
"[[博丽灵梦]]角色曲\n[[博丽神社]]",
2 行,1 个角色,1 个场景,角色曲为“主题曲”
"4面BOSS\n[[{{帕秋莉}}]]主题曲"
1 行,1 个角色,无场景
"[[博丽灵梦(旧作角色)|博丽灵梦]]角色曲"
"冈崎梦美过场曲"
1 行,1 个角色限定的场景曲(不同于角色曲)
"[[雾雨魔理沙(旧作角色)|雾雨魔理沙]]路线1面主题曲"
经过试错,暂时采用的算法是:
- 如果字符串里有空格,替换为换行("a b" -> "a\nb")
- 如果字符串里有“路线”一词,插入换行("雾雨魔理沙路线1面主题曲" -> "雾雨魔理沙路线\n1面主题曲")
- 定义“场景标识符”:
["面", "boss", "场景", "对话曲", "对话用曲", "ending", "角色选择画面"] - 定义“角色标识符”:
["角色", "路线", "过场曲"] - 用换行符切割字符串
- 对于每一行 循环
- 如果该行里匹配“场景标识符”关键词,将该字符串插入无歧义场景列表
- 如果该行里匹配“角色标识符”关键词,将该字符串插入无歧义角色列表
- 否则该行加入“歧义列表”
- 结束循环
- 对于歧义列表每一项 循环
- 如果无歧义场景列表为空,但无歧义角色列表有内容,将该项加入无歧义场景列表
- 如果无歧义角色列表为空,但无歧义场景列表有内容,将该项加入无歧义角色列表
- 如果两者皆空,且该项含有维基链接,将该项加入无歧义角色列表(因为有链接角色远比有链接的场景多
- 结束循环
- 最后,展开无歧义场景列表、无歧义角色列表的全部维基链接与模板。
这个算法的准确率并非 100%,仍需要微调,但似乎已经达到了 90% 以上。
除非使用 LLM 模糊匹配,否则很难有无需调试就 100% 准确的算法(但脚本的技术路线是完全确定的传统程序,因此排除该选项)。
抽象原曲标题的提取
同一首东方乐曲的不同版本可能出现在多部作品中。为了解决这个问题,我们不仅要定义“东方音轨”,还需要定义一个抽象的超级对象“东方原曲”。多个“东方音轨”指向同一个“东方原曲”(在 MusicBrainz 中,这一抽象类别被称作“作品”,而具体的音轨被称为“录音”,原理相同)。
更复杂的是,多个乐曲版本的标题不一定相同,因此无法通过标题本身判断官方乐曲之间的联系,而是需要独立的数据表来追踪(即通过 THBWiki 本身的信息进行推断)。在 THBWiki 中,按照编辑规范,每一首音轨的中文标题都是一个超链接,指向相对应的“东方原曲”页面。因此,我们可以将利用链接关系本身建立抽象“东方原曲”对象,“原曲”用于管理“音轨”之间的关系。
例如,《东方灵异传》的乐曲《死なばもろとも》的中文标题源代码为:[[{{灵异传音乐名|1|12}}]],而《东方怪绮谈》的乐曲《Civilization of Magic》的中文标题源代码为:[[{{幺乐团5音乐名|1|12}}]]。首先,我们需要将音乐名模板展开,分别得到 [[同归于尽]], [[Civilization of Magic]]。接着,分别利用 API 访问这两个页面,均会被重定向至条目同归于尽。此时我们建立一个抽象的“东方原曲”对象,并将”音轨“对象《同归于尽》、《Civilization of Magic》都指向“原曲”对象《同归于尽》。
更复杂的是,不同“原曲”对象之间也可能存在引用关系。如果要进一步完善数据,还需要建立“原曲关系”来管理“原曲”的关系,然后由“原曲”来管理“音轨”的关系……这属于相当高阶的特性,初步开发时没有必要实现。
保存原始信息
分析音轨信息时需要展开大量模板,为了方便开发与分析,我建议在数据文件中使用一个特殊的内部保留字段extra保留 THBWiki(或者其他 Wiki)上的原始信息(我使用的是extra.thbwiki.category),而将解析后的信息保存至公开字段,如context.location.zh-hans, context.character.zh-hans。
例如,娜兹玲的角色曲被保存为以下形式:
[[soundtrack_list]]
[soundtrack_list.title]
ja = "小さな小さな賢将"
zh-hans = "小小的贤将"
en = "A Tiny, Tiny, Clever Commander"
[soundtrack-list.context.character-list]
zh-hans = [
"娜兹玲",
]
[soundtrack-list.context.location-list]
zh-hans = [
"1面BOSS",
]
[soundtrack_list.extra.thbwiki]
title-template = [
"星莲船音乐名",
3,
]
[soundtrack_list.extra.thbwiki.category]
zh-hans = [
"1面BOSS",
"[[{{Naz}}]]角色曲",
]
[soundtrack_list.extra.thbwiki.linked-page]
text = "小小的贤将"
page = "小小的贤将"
已知问题与后续工作
- THBWiki 的网页并不能用 Python requests 等 HTTP/1.1 的程序访问(防御编写糟糕的垃圾爬虫?),但 API 无限制,本方法只涉及 API,不受影响(但如需获取网页,建议使用 curl、pycurl 等 HTTP/2 工具)
- 如果音乐作者不是 ZUN,那么音乐名模板的 Music Room 数据中还会有演奏者信息,同样用模板实现,这一模板需要手动解析。
- 原曲不仅包括官方游戏,还有官方专辑,其格式有所不同。
这几个问题是平凡的,留给读者作为练习,仿照上例不难,解决方案显然可得。
TouhouWiki.net 同样使用 MediaWiki,虽然模板不同,但同样可以使用 MediaWiki API 获取源代码,将数据额外解析并与 THBWiki 解析的数据合并后,可以获得 ZUN 评论的日、中、英版本。
初步结果
目前的初步结果是将乐曲转换为了以下 TOML 格式。
$ cat data/ost/TH6.toml
threlease = "TH6"
[title]
ja = "東方紅魔郷 〜 the Embodiment of Scarlet Devil."
en = "Touhou Koumakyou ~ The Embodiment of Scarlet Devil"
zh = "東方紅魔鄉 ~ the Embodiment of Scarlet Devil."
zh-hans = "东方红魔乡 ~ the Embodiment of Scarlet Devil."
[[soundtrack_list]]
[soundtrack-list.title]
ja = "妖魔夜行"
zh-hans = "妖魔夜行"
en = "Apparitions Stalk the Night"
[soundtrack-list.context.character-list]
zh-hans = [
"露米娅",
]
[soundtrack-list.context.location-list]
zh-hans = [
"1面BOSS",
]
[soundtrack-list.composer]
ja = "ZUN"
[soundtrack-list.commentary]
ja = "ルーミアのテーマです。\nこの曲に限らず、今回、全体的に軽快な曲になっていま>す。\nこの曲は夜の妖怪をイメージしました、\n・・・って言っても良いんだろうか(^^;\nノリ的には結構馬鹿っぽいです。"
zh-hans = "露米娅的主题曲。\n不仅是这首音乐,这次,总体来说音乐变得轻快了。\n这音乐是表现夜晚里妖怪的印象,\n···这样说也可以吧(^^;\n节奏上有些像笨蛋的感觉。"
[soundtrack-list.source.midi]
file-list = [
"th06_03.mid",
]
[soundtrack-list.source.midi.file_metadata]
ja = "【 妖魔夜行 】from 東方紅魔郷 for 88Pro Comp.ZUN"
zh-hans = "【 妖魔夜行 】from 东方红魔乡 for 88Pro Comp.ZUN\n"
[soundtrack-list.extra.thbwiki]
title-template = [
"红魔乡音乐名",
3,
]
[soundtrack-list.extra.thbwiki.category]
zh-hans = [
"1面BOSS",
"[[露米娅]]主题曲",
]
[soundtrack-list.extra.thbwiki.linked-page]
text = "妖魔夜行"
page = "妖魔夜行"
# [[soundtrack_list]]
# ...
结论
本评论提供了一种从 THBWiki 获取东方原曲的歪门邪道。
此外,如果不想自己折腾,欢迎使用源代码与解析好的 TOML 文件:https://github.com/TouhouOML/TouhouOML
目前开发未完成,存在许多问题,代码质量极低,日后需要全部重写。
模糊获取二创作品的东方原曲标题
虽然 THBWiki 的音乐资料 API 可以获取同人专辑以及对应的原曲,而且覆盖率极高,但依然不可能 100% 覆盖大量未经正式发布的作品(YouTube, Bandcamp, Soundcloud, BiliBili, NicoNico 等网站大量存在,例如“曲风改编”的实验作品)。此外,有些用户可能希望离线运行,不希望将正在播放的音轨发送到 THBWiki。
大多数作者都在网站备注中注明了音轨的原曲,因此有必要模糊格式五花八门的原曲标题。下面是一些典型代表的开发笔记,其中有写原曲英文标题的,有写日、英标题的,有只写原作编号的,有什么都不懈的,还有和 ZUN 一样写曲评的……
目前初步的想法是:先试 THBWiki,再试备注,再试音轨本身的标题……
Bandcamp
from Touhou Chiptune Remix ~ Aan De Slag Gaan by 40Nix
<div class="tralbumData tralbum-about">東方非想天則
<br>
(Did You See That Shadow? - Unthinkable Natural Law)</div>
from Story of Modulated Frequency by +TEK
<div class="tralbumData tralbum-about">from Touhou 12</div>
from Metamorphosis of Danmaku by Molkirill
<div class="tralbumData tralbum-about">Explicit content (elements of horror music and death metal).<br>
<br>
original track - "U.N. Owen Was Her?" from "Embodiment of Scarlet Devil".</div>
from 東方chipdisk by Pigu
<div class="tralbumData tralbum-about">Original: Septette for a Dead Princess<br>
<br>
オリジナル:亡き王女の為のセプテット</div>
from 東方秘封情報部 ~ Tôhô Spies and Private Eyes by Tangentg
<div class="tralbumData tralbum-about">Inspired by the James Bond theme.<br>
<br>
Original: Heian Alien (composer: ZUN)<br>
Arranged by Tangentg (me).<br>
<br>
This is an unofficial fan arrangement. Original composition and Touhou Project belong to ZUN.</div>
from PC-98 Perfect Cherry Blossom by HertzDevil
<div class="tralbumData tralbum-about">Title Screen theme
<br>
"The title theme from previous games, done in a Japanese style while keeping its essence. This song is meant to belie the tense atmosphere of "Now, let the Danmaku begin!". Frankly, I think this song isn't necessary, but it's also meant to restrain the player's excitement while awaiting Danmaku."
<br>
-- ZUN
<br>
<br>
In practice the title screen theme *is* used with a lot of SFX going on due to menu selection and so on, however this track still ended up using all six FM channels. The perfect fifth (well, 12-TET) playing at a quaver after the bass note reminds me heavily of Disunified Field Theory of Magic, although obviously there are more than that one song that do this.</div>