这本身是一个很有意思的问题,而且也很实用。
Instapaper和Pocket这样的稍后阅读类软件当然需要,各种搜索引擎的Spider也需要,甚至于SNS类网站也需要——G+、Facebook和豆瓣等都会为你添加的Link自动生成内容摘要,而如果这份摘要结果一看是Header里的Menu,这就糗大了。
这个任务对于人肉智能来说,是不足为据的——我们打开一个页面,扫一扫,事情就结束了。
但对于电脑里的人工智能来说,这事就蛋疼了。
这里,就简要分析一下要如何获得一张页面的内容区,这么一个问题。
PS:话说最近打算写而没有写完的文章还有很多,到底怎么会今天突然想写这个的啊。。。
我们先来想一下人是如何看一个页面,并找出其中的内容区的。
这事其实分两部分。
第一步,就是根据以往的经验来筛选出一个页面的大致布局,从而锁定内容区的位置。
比如说,当我们打开简书页面的时候,如果不是初来乍到,那么我们的视线会先扫过整个页面,然后很自然地就知道了内容的大致布局——左面的是NavBar,右上是Menu,中间是内容,靠左是作者信息,底下是评论。
这个我们扫一眼基本就心里有数了,而这个行为又可以分解为三个部分:根据过往经验从页面布局分析功能;根据过往经验从UI元素的形状和相对位置来分析出其功能;根据过往经验从UI元素的文字提示看出这货的功能。
大致的筛选过程就是这样——从最大的布局,到中间层次的UI元素形状和位置,再到细致的UI元素本身。这些根据我们的经验,都可以知道大概是干嘛的。
这放在计算机来说,就可以分解为两个不同的部分。
第一部分就是根据页面的Tag、Class和ID做语义分析,来了解大概。
不过这个对国内大部分网站来说是很糟糕的,因为国内网站(其实国外老网站也是如此)基本都不管语义标签,内容不用article,都是用div或者p,菜单也不用menu或者nav,依然是div或者p。所以这个也只能作为辅助,不能当真。
第二部分就是大数据下的学习了,这个这里略过不提。
所以,以“过往经验”做判断,对机器来说基本算是死路一条。
第二步,就是文本分析了。
文本分析的基本模式,可以分解为这么几步——
首先,将文章拆分成多个节点。
这里,对HTML文本做Parse,给出类似DOM-Tree,这就做完了第一步。
当然,这里也会做一定的筛选——比如将前面提到的header、footer、nav、menu都去掉,iframe肯定也要去掉,video啊audio啊object啊这些统统去掉。
接着,是做Style的筛选。
这里比较牛叉的,是从DOM-Tree生成网站的Render-Tree,然后将所有浮动元素去除——内容显然不应该是浮动的。
但,如果是要后台服务器端批量高效处理的话,那么绘制出Render-Tree显然没戏。
所以,我们能做的只是根据节点上的Style做出简单处理,CSS匹配这个最多就做一些简单的匹配,否则实用全套CSS匹配,就等于给出Render-Tree,这事就没谱了。
我们将所有浮动的元素去除,所有不可见的元素去除,留下那些可见的、固定的节点,筛选这事就算完成了第一步了。
第三步,内容筛选。
Style筛选只是将页面上铁定有问题的东西给去除,但比如简书左侧导航条这样的存在依然是无法就这么轻松地去除的。
于是,下面就是内容筛选。
页面上的有用的内容,大致可以分为这么三类:
文字,图片,标题。
这里“标题”可以更好地说成是“文档结构指示文本”,不过太麻烦了,还是用“标题”吧,足够了。
而页面上的非内容资源,则可以有很多,包括跳转链接,导航链接,功能模组,等等。
除了比较特殊的功能模组,前面两了链接和内容中的“文字”其实是有很大的区别的,但却和“标题”比较容易混淆。
但这两类链接和文字也不是完全地泾渭分明,因为文本中本来就可以就某些特定的文字设置超链接,所以如果简单粗暴地将所有含有链接的节点都去除,那就没剩下什么了。
我们先来看链接和文字是如何区分的。
作为页面交互元素的链接(而不是文章内的超链接),其特点就是一般用的都是短语或者简单的词汇,基本不用用句子,更加不会用长句或者复杂句——一个回到主页写出一个定语从句这样的奇葩网站我也是真醉了。
所以,如果我们发现一个节点里有一定量的句子,那么这个节点就有很高的几率是内容而非功能链接。
但,很多时候这样的判断未必是靠谱的,比如下面这样的东西:
<div>
<span>Fox</span>
<span>Book</span>
<span>Food</span>
</div>
这货到底是当内容好还是菜单好?
上面这样的东西在恰当的CSS和JS(比如给click事件绑定一个页面跳转)下,就是一个菜单,第一个选项是卖狐狸,第二个卖书,第三个卖食物,你一点话都没有。
但同样在一定的CSS(比如第一个背景色红色第二个背景黄色第三个背景绿色)下,却是一句普通的话而已。
而,如果要说是用语义分析来判断这是否构成一句话的话,现在还没法做到这点,就算能做到一定程度,对服务器端的压力也是可想而知的。
所以,就我们现在的问题来说,只能是给每个节点一个可能的分数,最后通过综合分析各项指标,做出恰当的筛选。
比如,这里就没个span的节点来说,构成内容的分数都是0.1(我瞎编的值),而对于整体的div这个节点来说,可能是内容的分是0.1+0.1+0.1+0.1=0.4,前面三个是每个span子节点的,最后一个0.1是从整个节点来看这句“话”的。
通过这样的方法,可以一定程度上解决非内容文本与内容文本的筛选——剩下的就是调整参数和通过前面说的东西来优化的问题了——如果还对每个域或者子域将优化结果和调整参数都记录下来,那么对某个特定网段的页面就可以有较好的匹配和抓取结果了。
但,这还只是分辨非内容文本和内容文本,我们还需要分辨非内容文本和标题——也就是文档结构指示文本。
前面的分析对这两类东西是几乎无能为力的,比如如下这段东西:
<blockquote>第一章 阿拉蕾
很久很久以前,上帝说要有欢笑,于是造了阿拉蕾。
第二章 宝瓜
过了没多久,上帝说要有胡闹,于是造了宝瓜。</blockquote>
如果我们按照上面所说的来分析,我们将看到,“第一章”这一行见被判定为“非内容文本”,第二行是内容文本,第三行是非内容文本,第四行是内容文本。
这显然不是我们想要的结果。
于是,这里我们将采用一种在Photoshop中很常见的技术,那就是将选区扩大,然后再收缩,从而将原本不相连的选区连接起来。
根据一个大多数页面都遵守的规律——内容都和内容在一起,非内容都和非内容在一起,从而来计算每个判定为非内容的节点(也就是前面所说的分值低于某个程度的节点)到内容节点的距离,超过临界值就认为真的不是内容。
或者也可以用更加Photoshop的方法,计算一个节点的“是内容节点可能性”的扩散,并根据“如果扩散到的值比节点原有的值小则保留原有值否则使用扩散值”这样的基本规律,从新计算扩散后的结果。
上面的处理可以将内容之间的标题给找出来,缺点是会将一头一尾的非内容节点也包括进去,所有最后就是对首尾做额外的判断。
进过这么一轮,我们就可以筛选出页面的内容区了。
当然,上面所说的只是基本思路,而且很多系统参数都没有,这些都需要在实际操作的时候进行调整和修改。
在Git上,有一个叫做Goose的项目,其ContentExtractor的基本算法思路就是这样的一套流程。
如果加上一定的Tag、Class、ID和语义分析,以及对各域的记录与分析(当然也就需要用户的恰当反馈与操作),那么就可以获得一个不错的内容抓取模块了吧——我猜。