近期看到了一篇蛮不错的论文《基于文本及符号密度的网页正文提取方法》,因为刚好在学习Rust写hello world,想想干脆试试能不能实现这个思路吧,于是开始尝试代码实现。
总的来说,这篇论文分析了大部分新闻网站页面,思考了其中的数学共性。虽然不同的网站,结构千变万化,但是从读者的直观印象来说,正文部分是十分明显的。打开一个新闻网页,我们总能很轻松的找出正文在哪里,而不会把广告或者页脚什么的当成正文。
按照这个思路走下去,我们大脑是如何识别正文的呢?首先,文本的数量必然是关键变量之一。其次就是文本的密集程度,正文的内容总是密集耦合在一起的,而不是部分在页面上方,部分在右下角这样分散排布的,正文总是会被一个“框”给包裹起来成为一个整体。对于读者来说,这个“框”是设计上的分割线,对于网页来说,这个“框”就是所有正文所公共的父级DOM节点。
假如我们能抽象出一个公式来模拟大脑的识别,那么我们就能像读者找出正文所在的“框”一样,找出包裹着正文的最近的DOM节点,从而精确提取出正文。
DOM树生成
首先根据拿到的网页源代码,我们可以很简单的生成一颗DOM树,这个在不同语言都有自己的实现,可以很轻松的生成。
Info树的生成
因为我们实际需要的每个树节点的部分信息,因此,我们需要遍历DOM树,拿到每个节点的特定信息后生成一棵新的Info树。之后所有的操作都基于这颗Info树来进行。
p.s. 因为正文必然是在Body标签内部的,所以我们可以直接把Body标签作为新树的根节点。
p.p.s 同样因为我们不关心渲染与执行,所以script、form、img、style等等标签除了影响结果外毫无用处,所以新树生成的时候需要过滤掉这些节点。
p.p.p.s 在现代网页中,因为网页很多标签是动态生成的,所以会有大量的无内容的空标签占位,同样需要将其移除。
最后每个Info 树的节点大概会像这样的:
pub struct Node {
pub tag_num: i16, // 该节点子代tag节点数量
pub text_length: i32, // 该节点总文本长度
pub link_tag_num: i16, // 该节点子代中超链接tag节点数量
pub text_tag_num: i16, // 该节点子代中文本tag节点数量
pub punctuation_num: i16, // 该节点子代中所有文本符号数量
pub link_tag_text_length: i16, // 该节点子代节点中超链接tag节点文本长度
pub td: f32, // 该节点计算出的文本密度
pub sbd:f64, // 该节点计算出的符号密度
pub score: f64, // 该节点计算出的分数
pub node_type: NodeTypeEnum, // 该节点的类型
pub tag_name: String, // 标签名称
pub text: String, // 如果是文本标签,则保存了对应的文本值
pub children: Vec<Node>, // 子代节点
}
计算
可以看出,除去基本信息,我们需要计算的就是每个节点的文本密度、符号密度以及所有节点的文本密度的标准差。然后带入公式对每个节点进行打分。
p.s. 具体公式以及公式推导请参考论文,这里不太适合将别人研究成果直接放在文章内。《基于文本及符号密度的网页正文提取方法》
遍历
遍历整棵树找出分数最高的节点,就是对应的正文节点。
测试
论文中提到的新闻网差不多正确率百分之九十九左右,虽然是论文中自己提供的正确率,但是实际测试确实还没有遇到提取失败的情况。
在基础实现后,我尝试对掘金、简书等技术文章网站进行了测试,发现效果极差,几乎没有成功的案例。
所以和新闻网站结构类似的文章网站为什么会失效呢?从我们之前的分析来说,应该不会因为是文章而不是新闻而产生差异啊。
经过对比后发现,主要原因在于掘金等这些偏技术的文章网站,文章内容主要是由MD格式的文章转换成HTML而来的,而MD特殊之处导致了公式中很多变量产生了变动.
虽然从结果来看,直接写html的文章和写md格式的文章转换成html显示,这两种方式并不会有什么差别,用户也可以无感知的使用在线编辑器。
但是对比可以发现,因为MD的语法习惯,所以会有大量的斜体、加粗、强调、引用等等样式标记被装换成b、i、strong等等标签,而在新闻网中,这些往往是交给css处理的样式。这样就导致产生了大量的零碎的标签,降低父文本的文本密度。
其次,在转换中,p标签的出现次数也极低,大部分文本在转换中直接成了div的子元素,而不是被包裹在标准的p标签中。
对于标准html来说,正文往往就是被一大堆p标签分割的段落组成。然而在MD转换的html代码中,正文大部分被div、li、code、pre等等多种多样的标签包裹,影响打分。
同时,因为MD语法被程序员用的最多,并且技术文章的结构特性,也导致了文章中大概率会出现非常多的有序列表和无序列表还有代码举例,所以li、ol、code、pre这几个标签分别占有了文本总长度中非常高的比例。
综上所述,如果按照原有计算方式计算,最后父节点的打分会非常低,而一些直接包裹文本的节点会因为文本密度非常高而分数变得异常的高。
针对以上情况,可以考虑做以下处理:
修改后的代码,对于掘金、简书等文章基本可以正确的提取正文了。( p.s. 缺乏充足的用例测试 )
最后吐槽,rust的思路真的太难受了,比起用其他语言实现,起码多花了一倍的时间在思考语法该怎么写上面Orz。
附上链接:
论文:《基于文本及符号密度的网页正文提取方法》
我的 rust 代码例子: https://github.com/x956606865/test-extract
另一位大佬用python写的更牛逼的版本: https://github.com/kingname/GeneralNewsExtractor
Link:https://www.notion.so/aj0k3r/2127bd615e034209b818875d2fcec664