从"花舞鸟只有一种"到 275 形态全覆盖:宝可梦题库数据管线复盘

做前端十年,最近在独立维护一个宝可梦答题微信小程序。前阵子用户提了个不起眼的问题:数独里想填「花舞鸟」只有火/飞行一种,可官方明明有四种舞;洛托姆只有电/幽灵,可它有六种家电形态。我顺着查下去,发现这不只是漏几条数据,而是"同物种多形态"整类都没进数据。

三层根因

图鉴数据是物种级的,1025 条、form 字段全空,数独判题直接映射物种,形态从源头就进不来。我之前写的 forms.js 只收了五类(地区形态、超级进化、原始回归、超极巨化、白名单限定),"同物种多形态"这一大类整段缺失——花舞鸟、洛托姆、武道熊师双属性全在里面。即便 forms.js 里有某形态,数独判题也不读它,还是填不进去。用户点名的三个案例,全被这三层解释通了。

用扫描替代白名单

花舞鸟、洛托姆、武道熊师是三次不同时间各自漏掉的,手动维护白名单注定还会漏。我改成一个系统性扫描脚本:把 PokeAPI 全部 402 个带横杠的条目拉出来,和物种默认形态逐项比属性,差异即形态,再叠加排除规则。

排除规则里有几个真坑。nidoran-f/m 名字带横杠,但它俩是两个独立物种,不是形态;mr-mime、ho-oh 这种"物种名本身就带横杠"的,要靠 is_default 过滤;皮卡丘的帽子/换装 16 条、-totem 霸主也要排除。扫完得到 299 形态 / 216 物种(属性不同 107、属性相同 192),再剔除 24 个裁定不收项,最终 275 形态落地:107 个属性不同的进数独池,168 个属性相同的只进图鉴。

官方中文名从 52poke 抓取(|formN= 字段),繁→简映射用项目现成数据建立的语料库比对,而不是码位阈值判断。

两个阴间的坑

最值得说的是繁简检测假阴性。我最早按"Unicode 码位大于 0x9FA5 算繁体"判断,结果 達、凱、爾 全漏报,脚本还打印"没有未识别的繁体字",名单里混进了「阿罗拉小拉達」这种东西。改法是建一个简体语料库(用项目内 pokedex/forms/flavor/moves/abilities 建字符集),凡是"不在语料库、也不在繁简映射表"的汉字一律报出来,宁可误报绝不静默。第一次跑就报出 31 个字,补完归零。

另一个坑是下游口径漂移。我把图鉴改成"收全形态"时忘了同步数独池的过滤,池子被写成含 192 个属性相同形态的版本,数独会出现"问格斗/水,答本体和答形态没区别"的 bug,加一行 !h.same 属性差异过滤才修好。还有一次数独池生成脚本把后端副本路径写成 ../pokemon-back/...,但仓库里 pokemon-back 与 pokemon 平级,只回退一级,前端池写成功了、后端 forms_all.json 静默 ENOENT——前端"看起来"成功,后端那份悄悄没写。

把正确性锁死

形态进数独用环境变量 FORMS_SWITCH_DATE 按天取池,前端不抄切换日、靠响应 formsOn 字段决定是否启用,保证前端可先发、历史日期棋盘不变(避免破坏"全国同一天同一套题"的铁律)。正确性靠两件事兜底:程序化复核(275 张出图高度全为 475,宽度分布与既有管线一致)和守门断言(test_pokedoku 16/16 覆盖前后端池逐条一致、开关可逆、全/半角归一、忽略分隔符兜底)。

做独立项目最容易被忽视的就是这种"数据正确性"——它不是看得见的功能,但错了用户立刻感觉得到。任何采集口径变更,必须同步检查所有下游消费方,这是我这次最实在的收获。