两个整整齐齐的零
音乐 84 条标记里 0 条抽到了短评,舞台剧 5 条也是 0 条。一句告警都没有——因为「这条本来就没写短评」是合法的。
这一批修的都是同一个形状的 bug:产出看起来完全正常。 没有异常、没有报错,只是有些字不在里面了。
一个选择器只属于一种媒介
影视的短评在 <span class="comment"> 里。三种媒介共用了这个选择器,
于是:
影视 <li><span class="comment">这讲的是个啥…</span></li>
音乐 <li>
欢乐
</li>
舞台剧 <li>团建选了看舞台剧/音乐剧的项目…</li>
音乐和舞台剧的短评裸在 <li> 里,没有任何 class。
音乐 84 条标记 0 条有短评,舞台剧 5 条 0 条——两个整整齐齐的零。
它不报错,因为「这条标记没写短评」本来就是最常见的形态。
这已经是第二次撞上「每种媒介一套,这不是可以统一的东西」
(上一次是游戏的评分与短评),所以这回不再按 class 找,改成按位置找:
操作栏 <li class="clearfix opt-ln"> 前面那个 <li>。
实测短评 2027 → 2109(舞台剧 0→4,音乐 0→78)。
改的过程中,端到端那条断言从 3 跳到 4
项目里有一条端到端断言,钉住「有多版本的标记有几条」。改抽取器的时候它从 3 变成 4, 逼出了两个只会静默出错的坑。
「(N 有用)」和短评住在同一个 <li> 里
<span class="pl">(5 有用)</span> 是豆瓣的计数器,不是用户写的字。
实测同一条标记两次抓取之间从 (5 有用) 变成了 (1 有用),短评一个字没动。
把它算进短评,就会凭空多出一条「用户编辑了这条标记」的记录——
与之前那次「1740人浏览」混进日记正文是同一个错。
只有标题的条目,把片名当成了用户短评
有些标记只有一个标题,没有评分、日期、标签、短评。这种条目上, 兜底逻辑取到的是作品自己的名字,于是 8 部电影的「短评」变成了:
V字仇杀队
铁西区第一部分:工厂
它产出的是符合语法的正常文本。 这才是最隐蔽的地方——粗看导出的数据,
完全像是由用户真实撰写的评价。
过滤条件的漏洞在于仅检查了 <li> 的内部文本,而能够识别空评价的特异 class
正好位于开标签上,在此前的字符串切分中已被提前丢弃。
修完之后影视仍是 1506 条短评(没动),修订总数仍是 2943(没有伪造出任何编辑)。
顺带:页面上那些 "
样张站点上肉眼可见 "、'。
追下去发现两边各自都没做错事:
- 站点生成器按规矩把用户文本里的
&转义成&——不转义会丢字; - 解析器留下了一个没解开的
"。
于是 " → &#34; → 原样印在页面上。
解析器漏解一次,下游就会忠实地把它展示出来。
问题的根因在于四个抽取器各自维护了互不相同的实体解码映射表:一个支持八种、
一个支持六种、一个支持五种,而负责处理标题与短评的核心抽取器甚至完全没有配置解码表。
将实体解码逻辑收敛为唯一定义后,实测未解开的实体数量从 196 降至 1,
且仅存的一处为符合预期的真实嵌套实体(原文即为 HITMAN&amp;trade;,解一次得到字面的 &trade;)。
两条规矩,旧的四张表全都写错了:
-
一遍扫完,不要链式 replace。 四份实现都把
&排在最前面解, 于是&lt;先变成<、再变成<。 而它的原文是字面的四个字符<——正好是一条讨论 HTML 转义的短评会写出来的东西, 吃掉之后还原不回去。 -
认不出来的原样留着。 越界码位、没见过的实体,一律不猜。
猜错是不可逆的,留着则是可以 grep 出来的。同理
解成不换行空格而不是普通空格: 页面上写的就是不换行空格,改掉属于静默改写。
还有:长文的正文粘成了一坨
日记正文要变成 Markdown,所以结构必须在剥标签之后活下来。三处都是静默的:
- 点列表变成一坨。 五个
<li>只剥标签就粘成了一长串。 - 段落粘连。
</p>只给一个换行,CommonMark 当作段内软换行——三段在页面上并成了一段。 - 正文末尾挂着豆瓣的版权声明。 「科技 生活 本文版权归 ⋯ 所有」跟在用户日记后面。
往里收一层到 <div class="note"> 就解决了,但认不出就退回整段,绝不返回 null——
手上只有 2 篇带这个容器,n=2 推不出一个封闭集合。
这个项目在「从手上的样本推出封闭集合」上已经栽过四次了。