前两篇讲了 10 关,都是"答案看着像、其实不对"。这一篇的 5 关,问题往资料层和层级逻辑里钻了一层:市名把自己下辖的区名吃掉、历史回放认错全名、同一个地名在不同年份指两个不同的地方,还有一处根因不在引擎、而在事件库本身。
第 11 关:黄石港区 里的"黄石"
输入 湖北省黄石港区,输出 湖北省-黄石市。区没了。
因为市名"黄石市"去掉后缀的短形式"黄石",正好是"黄石港区"的开头,被当成市名消费掉了。同一批还有 本溪满族自治县,"本溪"既是市名又是县名的开头。
改法:市名短形式匹配之前,先看本市有没有更长的县名以它开头。有就不按"市"消费,交给县级解析。
第 12 关:时间机器查"芜湖县"
我这个引擎有个时间机器的功能,可以查某年某地叫什么。查 芜湖县(1992 年),返回的是 安徽省-芜湖市。
根因是收集当年命中的时候没区分"全名命中"和"短形式命中",排序只看位置和级别,于是短形式的地级先胜出。
改法是记录是否全名命中,同名多命中时全名优先。第 2 关那个"1987 年的海南省",语义照样保住。
第 13 关:巢湖市 的两种身份
"巢湖市"既是 2011 年被撤销的地级市,也是现在仍然存在的县级市,隶属合肥市。所以两个输入,答案必须不一样:
安徽省合肥市巢湖市中垾镇,这里的"巢湖市"必须按现行县级市理解;巢湖市庐江县,这里的"巢湖市"必须按已撤销的地级市理解,因为庐江县当年属巢湖市,现在属合肥市。
原来两种情况一律做历史替换,于是第一句被替换成"合肥市",县级市和乡镇一起消失。
改法是让别名替换变成上下文敏感。别名本身是现行区划名时,如果整串就是它,或者它前面已经出现了省名或市名,就不做历史替换。
第 14 关:(山东省)莒县 只解析到省级
根因是特别小的一个实现细节。判断"是否在词边界"时,我用"字符码小于 U+4E00"来区分汉字,而全角括号的码位比"一"还大,被当成了汉字,于是"莒县"这两个字不认可前边界。
改成按 CJK 区间判断就对了。
第 15 关:事件库里少写了两个字段
万县区、昌平县、顺义县 这些旧名一直解析不出来。查到根上,是事件库里有 128 条记录的新路径只写了末段。比如 万县区 → 万州区,缺了"重庆市",被引擎的完整性检查整条丢掉了。
补齐之后,自动生成的历史别名从 888 条涨到 915 条。一个数据缺口,换回一批能用的旧名映射,包括北京昌平、顺义、大兴、房山、平谷、怀柔、密云、延庆这些撤县设区的旧名。
同一批还顺手改了一个小的:开源版不随包分发逐年快照,时间机器原来会直接抛异常。改成返回"本发行版不提供该能力"的诚实拒绝。宁可说没有,不要崩。
这一批的账
| 项 | 数(截至本篇) |
|---|---|
| 本篇这 5 关 | 各补 3 条以上回归,共 18 条 |
| 累计修掉的解析缺陷 | 15 个 |
| 累计新增回归用例 | 59 条,回归集从 110 条扩到 169 条,全绿 |
| 自动生成的历史别名 | 915 条 |
引擎和数据在 GitHub:github.com/nbsetsail/r… 。代码 MIT,数据 CC-BY-4.0,署名即可使用。变更事件库的每条都带出处,可以用它做时间机器式的回放。
最后一篇讲第四轮:把"交付件"本身也拉去考试之后,又发现了什么。