现在网上能搜到的正文抽取算法一般有两类:
Readability:该算法先建立DOM树,然后对网页源代码中不同的HTML标签进行判断,逐渐找到正文所在标签位置。该算法的主要优点是可以最大程度的保存网页正文的缩进、空行以及链接。
行块分布算法:主要是基于论文《基于行块分布函数的通用网页正文抽取》。该算法不需要建立DOM树,直接剔除HTML标签,剩下的网页文字之间会有一定的位置关系。
下面说一下行块分布算法:
先上代码
# -*- coding: utf-8 -*-
import requests as req
import re
DBUG = 0
reBODY =re.compile( r'<body.*?>([\s\S]*?)<\/body>', re.I)
reCOMM = r'<!--.*?-->'
reTRIM = r'<{0}.*?>([\s\S]*?)<\/{0}>'
reTAG = r'<[\s\S]*?>|[ \t\r\f\v]'
reIMG = re.compile(r'<img[\s\S]*?src=[\'|"]([\s\S]*?)[\'|"][\s\S]*?>')
class Extractor():
def __init__(self, url = "", blockSize=3, timeout=5, image=False):
self.url = url
self.blockSize = blockSize
self.timeout = timeout
self.saveImage = image
self.rawPage = ""
self.ctexts = []
self.cblocks = []
def getRawPage(self):
try:
resp = req.get(self.url, timeout=self.timeout)
except Exception as e:
raise e
if DBUG: print(resp.encoding)
resp.encoding = "UTF-8"
return resp.status_code, resp.text
def processTags(self):
self.body = re.sub(reCOMM, "", self.body)
self.body = re.sub(reTRIM.format("script"), "" ,re.sub(reTRIM.format("style"), "", self.body))
# self.body = re.sub(r"[\n]+","\n", re.sub(reTAG, "", self.body))
self.body = re.sub(reTAG, "", self.body)
def processBlocks(self):
self.ctexts = self.body.split("\n")
self.textLens = [len(text) for text in self.ctexts]
self.cblocks = [0]*(len(self.ctexts) - self.blockSize - 1)
lines = len(self.ctexts)
for i in range(self.blockSize):
self.cblocks = list(map(lambda x,y: x+y, self.textLens[i : lines-1-self.blockSize+i], self.cblocks))
maxTextLen = max(self.cblocks)
if DBUG: print(maxTextLen)
self.start = self.end = self.cblocks.index(maxTextLen)
while self.start > 0 and self.cblocks[self.start] > min(self.textLens):
self.start -= 1
while self.end < lines - self.blockSize and self.cblocks[self.end] > min(self.textLens):
self.end += 1
return "".join(self.ctexts[self.start:self.end])
def processImages(self):
self.body = reIMG.sub(r'{{\1}}', self.body)
def getContext(self):
code, self.rawPage = self.getRawPage()
self.body = re.findall(reBODY, self.rawPage)[0]
if DBUG: print(code, self.rawPage)
if self.saveImage:
self.processImages()
self.processTags()
return self.processBlocks()
# print(len(self.body.strip("\n")))
if __name__ == '__main__':
ext = Extractor(url="http://blog.rainy.im/2015/09/02/web-content-and-main-image-extractor/",blockSize=5, image=False)
print(ext.getContext())
原理如下:
1、只获取body的文本;
2、去除script、style的标签;
3、去掉所有的标签内容,只保留纯文本信息;
4、根据\n进行切割, 统计出有多少个块,每个块的文本长度是多少;
5、计算出最大长度的blocksize
6、返回内容
原文连接:
基于文本及符号密度的网页正文提取方法
https://max.book118.com/html/2019/0429/7004026036002023.shtm
下面讲一下readability:
readability 是一个可以从杂乱无章的网页中抽取出无特殊格式,适合再次排版阅读的文章的库,比如我们常见的手机浏览器的阅读模式很大程度上就是采用的这个库,还有 evernote 的 webclipper 之类的应用也都是利用了类似的库。readability 的各个版本都源自 readability.js 这个库,之前尝试阅读过 js 版本,无关的辅助函数太多了,而且 js 的 dom api 实在称不上优雅,读起来晦涩难通,星期天终于有时间拜读了一下 python-readability 的代码。
readability 核心是一个 Document 类,这个类代表了一个 HTML 文件,同时可以输出一个格式化的文件
几个核心方法和概念
summary
summary 方法是核心方法,可以抽取出一篇文章。可能需要对文章抽取多次才能获得符合条件的文章,这个方法的核心思想是:
- 第一次尝试抽取设定 ruthless,也就是强力模式,可能会误伤到一些标签
- 把给定的 input 解析一次并记录到 self.html,并去除所有的 script,sytle 标签,因为这些标签并不贡献文章内容
- 如果在强力模式,使用 removeunlikelycandidates 去掉不太可能的候选
- transformmisuseddivsintops 把错误使用的 div 转换成 p 标签,这样就不用考虑 div 标签了,其实这步挺关键的。其实还有一些其他的处理需要使用。
- 使用 score_paragraphs 给每段(paragraph)打分
- 使用 selectbestcandidates 获得最佳候选(candidates)
- 选出最佳候选,如果选出的话,调用 get_article 抽取文章
- 如果没有选出,恢复到非强力模式再试一次,还不行的话就直接把 html 返回
- 清理文章,使用 sanitize 方法
- 如果得到的文章太短了,尝试恢复到非强力模式重试一次
强力模式和非强力模式的区别就在于是否调用了 removeunlikelycandidates
对于以上的核心步骤,已经足够应付大多数比较规范的网页。但是还是会有不少识别错误。公司内部的改进做法在于:
此处省略 1000 个字。
下面按照在 summary 出场顺序依次介绍~
removeunlikelycandidates
匹配标签的 class 和 id,根据 unlikelyCandidatesRe 和 okMaybeItsACandidate 这个两个表达式删除一部分节点。
unlikelyCandidatesRe:combx|comment|community|disqus|extra|... 可以看出是一些边缘性的词汇
okMaybeItsACandidateRe: and|article|body|column|main|shadow... 可以看出主要是制定正文的词汇
transformmisuseddivsintoparagraphs
- 对所有的 div 节点,如果没有 divToPElementsRe 这个表达式里的标签,就把他转化为 p
- 再对剩下的 div 标签中,如果有文字的话,就把文字转换成一个 p 标签,插入到当前节点,如果子标签有 tail 节点的话,也把他作为 p 标签插入到当前节点中
- 把 br 标签删掉
socore_node
- 按照 tag、 class 和 id 如果符合负面词汇的正则,就剪掉 25 分,如果符合正面词汇的正则,就加上 25 分
- div +5 分, pre、td、backquote +3 分
- address、ol、ul、dl、dd、dt、li、form -3 分
- h1-h6 th -5 分
score_paragraphs
- 首先定义常量,MIN_LEN 最小成段文本长度
- 对于所有的 p,pre,td 标签,找到他们的父标签和祖父标签,文本长度小于 MIN_LEN 的直接忽略
- 对父标签打分(score_node),并放入排序队列
- 祖父标签也打分,并放入排序队列
- 开始计算当前节点的内容分(content_socre) 基础分 1 分,按照逗号断句,每句一分,每 100 字母+1 分,至少三分
- 父元素加上当前元素的分,祖先元素加上 1/2
- 链接密度 链接 / (文本 + 链接)
- 最终得分 之前的分 * (1 – 链接密度)
注意,当期标签并没有加入 candidates,父标签和祖父标签才加入
累计加分,如果一个元素有多个 p,那么会把所有子元素的 content score 都加上
selectbestcandidate
就是 ordered 中找出最大的
get_article
对于最佳候选周围的标签,给予复活的机会,以避免被广告分开的部分被去掉,阈值是 10 分或者最佳候选分数的五分之一。如果是 p 的话,nodelength > 80 and linkdensity < 0.25 或者 长度小于 80,但是没有连接,而且最后是句号
思考
readability 之所以能够 work 的原因,很大程度上是基于 html 本身是一篇文档,数据都已将在 html 里了,然后通过操作 DOM 获得文章。而在前端框架飞速发展的今天,随着 react 和 vue 等的崛起,越来越多的网站采用了动态加载,真正的文章存在了页面的 js 中甚至需要 ajax 加载,这时在浏览器中使用 readability.js 虽然依然可以(因为浏览器已经加载出了 DOM),但是如果用于抓取目的的话,需要执行一遍 js,得到渲染过的 DOM 才能提取文章,如果能够有一个算法,直接识别出大段的文字,而不是依赖 DOM 提取文章就好了~
原文连接: