列表页智能解析算法的实现
本节中我们来动手实现列表页的提取算法。
数据预处理
和提取详情页正文时一样,由于原始HTML代码中包含很多干扰内容,所以先对HTML代码进行预处理,整个预处理方法和提取详情页正文时也基本类似,这里为了更加灵活地修改处理逻辑,单独定义了一个 preprocess4list 方法:
这里同样定义了一些规则,LIST_USELESS_TAGS代表一些噪声节点,可以直接调用strip_elements 方法把其整个节点和内容删除。LISTSTRIP_TAGS节点的文本内容需要保留,标签可以删掉。 LIST_NOISEXPATHS代表一些明显不是列表内容的节点,例如评论、广告等,直接删除就好。
其中还用到了工具方法remove_children、remove_element等,它们的定义也已经在14.3节阐明,这里不再赞述。
选取组节点
现在实现前两步:根据成员节点的特征(同类型且连续)找出所有符合条件的候选组节点,然后 根据规定的组节点特征(例如字数、成员节点数量等)排除穴余组节点。
为了方便操作,这里扩展一下Element对象的属性:
这里我们扩展了如下几个属性。
-
number_of_siblings:兄弟节点的数量。用于过滤余组节点,当一个节点的兄弟节点的数量小于一定数值时,就过滤掉对应的组节点。
-
a_descendants_group_text_min_length:组节点内成员节点的文本内容的最小长度。用于过滤究余组节点。
-
a_descendants_group_text_max_length:组节点内成员节点的文本内容的最大长度。同样用于过滤究余组节点。
-
similarity_with_siblings:节点和兄弟节点的相似度。如果这个相似度过低,那么这些节点可能并不是同类型且连续的节点,对应的组节点也不是我们想要的节点。
-
parent_selector:父节点的选择器。成员节点用它选择组节点,它们是父子关系。
接下来就找出同类型且连续的成员节点对应的组节点吧,代码实现如下:
这里我们先定义了几个阈值。
-
min_number:用于限制兄弟节点的数量,这里定义为 5,即成员节点至少要是 5 个同类型且连续的节点。
-
min_length:用于限制成员节点的文本内容的最小长度,这里定义为 8。比较 a_descendants_group_text_max_length 和 min_length 的值,如果前者小于后者,也就是组节点内成员节点的文本内容的最大长度小于8,就说明该组节点内所有成员节点的文本内容都很短,而标题一般得8个字以上,说明该组节点内不可能包含标题,就把它排除了。用这个阈值可以过滤掉很多导航菜单组节点。
-
max_length:用于限制成员节点的文本内容的最大长度,这里定义为 44。比较 a_descendants_group_text_min_length 和 max_length 的值,如果前者大于后者,也就是组节点内成员节点的文本内容的最小长度大于 44,就说明该组节点内所有成员节点的文本内容都很长,而标题一般最多 40 字,说明该组节点内不可能包含标题,同样把它排除了。用这个阈值可以过滤掉很多长文本组节点。
-
similarity_threshold:用于限制兄弟节点的相似度,这里可以用 tag_name、class 属性值、子节点的数量或其他属性来判断节点的相似度,例如 <atitle="block" class="itemitem-1"></a> 和 <atitle="block" class="itemitem-2"></a> 的相似度是比较高的,而 <atitle="block" class="itemitem-1"> 和 <span class="bold></span> 的相似度就很低。如果成员节点的相似度很低,就证明这个组节点里根本没有同类型且连续的成员节点,那么这个组节点就不能作为目标组节点了。
这里我们将 descendants_tree 定义为了 defaultdict(list) 类型,其键名是父节点的选择器,键值是成员节点组成的列表。
运行上述代码,就能得到一些符合要求的组节点了,结果如图 14-19 所示。
图14-19 初步筛选出的组节点
合并组节点
现在要根据相似度合并组节点了,怎么计算相似度呢?简单来说可以直接使用选择器的路径表达式,相似组节点对应的选择器相似度一定很高。例如这里一共有 5 个组节点的 XPath 路径表达式:
(省略)
你能找出哪些组节点属于同一组吗?很明显,前 3 个属于同一组,后 2 个属于同一组。那么如何用算法实现呢?这个算法属于聚类的范畴了。聚类就是把相似的内容聚在一起成为一堆,聚类方法有很多,例如 K-means、DBSCAN 等,不过这里我们仅仅根据选择器聚类。一个简单的聚类方法如下:
这里的 cluster_dict 就是对字典类型的内容进行聚类处理,其输入数据就是 defaultdict(list) 类型的,键名是父节点的选择器,键值是成员节点列表。
这单我们可以根据上面的例子测试一下:
运行结果如下:
可以看到成功把前3个组节点聚在一起了,成为第一组,另外2个组节点则聚为第二组,和我们 肉眼观察到的结果一致。
接下来调用 cluster_dict 方法对上一步的 descendants_tree 进行聚类处理:
clusters = cluster_dict(descendants_tree)
这样得到的结果就是合并后的组节点了,结果变成图 14-20 所示的这样。
图14-20 合并后的组节点
挑选最佳组合节点
现在要从所有组节点中挑选最佳组节点,同样是依据多个指标计算各个组节点的分数。依据的指标有很多,例如成员节点的数量、平均字数分布、文本密度等,分数的计算公式可以自行设计。下面实现一个挑选方法:
这里向 choose_cluster 方法传入上一步得到的合并后的所有组节点,然后 evaluate_cluster 方法会计算每一个组节点的得分,最后返回得分最高的组节点。
其中 evaluate_cluster 方法比较关键,就是实现分数计算的方法,下面给出一个参考实现:
可以看到,这个方法根据成员节点的相似度、数量和节点大小计算出了组节点的分数。
拿上面的案例来说,由于标题列表中的组节点对应的成员节点数量更多、节点更大,因此最终得到的分数也更高,自然就被选为最佳组节点了。
提取标题和链接
最佳组节点已经选出来了,如果这个结果是正确的那么其每个成员节点里就包含着我们想要提 取的标题和链接。
例如在上面的例子中,一个成员节点就是一个 1i 节点,其源码为:
可以看到这个li节点内包含一个span节点,两个a节点,其中第二个a节点才包含我们想要提取的标题和链接信息。我们怎么用算法自动提取1i节点内的第二个a节点呢?同样可以根据一些特征,这里最明显的特征就是学数了。
字数有什么规律呢?经过大量统计,可以发现标题长度是满足高斯分布的,其概率密度函数为:
如果我们把这个概率密度函数应用到标题长度分布上,那么从就是标题长度的均值,sigma 就是标题长度的标准差。这里为了拟合一个较为合适的概率密度曲线,经过调优,u 取了 26,sigma 取了6,拟合的概率密度曲线如图 14-21 所示。
根据图 14-21 中的曲线,标题长度为 26 的概率最高,随着标题长度的减小或增大,概率会逐渐减小。当长度小于 5 的时候,概率已经趋近于 0,事实上一篇新闻的标题少于 5 个字的概率确实非常小。通过这个方式,我们能够筛选出更贴近于标题的节点,例如上文中第二个 a 节点计算得到的概率就比第一个 a 节点的概率更大,因此会倾向于选择第二个 a 节点。
图14-21 标题长度分布的概率密度曲线
当然,仅仅依靠单个节点计算还是不够科学,更优的方法是针对整个组节点内所有可能的同类a 节点,计算总体置信度,然后选出第二个a节点对应的选择器路径,再统一按照这个路径查找标题。
根据标题长度获取置信度的方法实现如下:
这里其实就是实现了高斯分布的概率密度函数,接收标题的长度值,返回对应的概率。借助这个方法,可以实现标题的提取逻辑:
这里我们定义了一个 extract_cluster 方法,参数是组节点的信息,遍历它可以得到所有成员节点。对于每个成员节点,提取其所有的 a 节点,然后根据 probability_of_title_with_length 方法计算出每个 a 节点可能是标题的概率,同时记录这个 a 节点相对成员节点的节点路径。接着根据上一步计算得到的置信度找出最优的 a 节点路径,即 best_path。最后根据 best_path 去成员节点里提取标题和链接,并组成一个列表返回。
至此,我们完成了列表页的提取。
整合
由于整个提取算法实现起来比较复杂,所以上述内容简化了部分代码逻辑。我已经将整个提取算法封装成了一个完整的 Python 包,大家可以直接调用,感兴趣的话也可以查看其源码。
这个包叫作 GerapyAutoExtractor,在 14.3 节已经介绍过,可以通过 pip3 工具安装:
pip3 install gerapy-auto-extractor
安装完成后便可以导入使用,调用流程非常简单:
from gerapy_auto_extractor import extract_list from gerapy_auto_extractor.helpers import content, jsonify
html = content('list.html') print(jsonify(extract_list(html)))
这里调用 content 方法读取了列表页的 HTML 代码,调用 extract_list 方法提取了列表页的内容,并调用 jsonify 方法对提取结果进行了格式化,运行结果如下:
可以看到,示例列表页的标题和链接被提取出来了。
另外,GerapyAutoExtractor 包在很多细节上对提取算法进行了优化,大家可以查看其说明来了解更多用法,也可以直接查看源码来详细了解本节介绍的实现流程。