最近产品提了个需求,要按事先制定的规则,解析网页,提取结构化数据。由于不涉及爬虫,不会触犯相关法律,本文就讲一下提取结构化数据的一般方法。
浏览器从服务器下载网页并渲染成用户看到的界面,其中渲染过程分成五个步骤:1. 解析HTML并构建DOM树;2. 处理CSS并构建CSSOM树;3. 将DOM和CSSOM合并成一个渲染树;4. 根据渲染树来布局,计算每个节点的位置;5. 调用GPU绘制,合成图层,显示在用户屏幕上。
现代浏览器提供了操作DOM树的方法,以Chrome浏览器为例,打开开发者工具,在Console导航下,利用全局document对象,即可操作dom。Chrome给document对象提供了两个方法,分别是querySelector(css selectors)
和 querySelectorAll(css selectors)
,其中前者用于查找第一个匹配到指定选择器的dom元素,后者用于查找全部匹配到指定选择器的dom元素。利用这两个方法提取dom元素的好处在于,用户无需关心页面数据加载方式,无论是动态加载(脚本获取)还是静态加载(直接输出到页面),只要页面加载完全了,需要的数据就在dom里,真正所见即所得。
面对互联网上数以亿计的网站,对它们一个个定制规则显然不可取。定制规则是死的,而网站却总在变化中,需要一套随机应变的做法去适应万千不同。现代搜索引擎技术,积累了大量解析经验,产生了不少成熟的方法。俗话说不要轻易造轮子,不妨借助前人的经验。本文要讲的基于文本密度提取正文,即为一种业界常用的方法。
对于新闻稿件,一般而言,有一些固定的组成元素。比如标题、正文、发布时间;更规范点的,还有作者、稿源、分类、摘要等信息。不同的元素有不同的规则去解析,比如发布时间,可以通过匹配常用的日期时间格式去获取;标题可以在tilte、h1、h2、h3、h4等常用于凸显标题的标签里获取;而正文,由于其一般占据网页里文本最集中、密度最大的区块,可以认为文本密度最大的区块就近似为正文所在区块。文本密度算法正是依据这种一般而言成立的推断,实现对正文的提取。
原理就是这么简单,不多说,直接上代码了:
import reimport requestsreBODY = r'<body.*?>([\s\S]*?)<\/body>'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(object):"""根据文本密度提取正文区"""def __init__(self, html='', blockSize=3, saveImage=False):self.blockSize = blockSizeself.saveImage = saveImageself.html = htmlself.ctexts = []self.cblocks = []def __processTags(self):"""处理html标签"""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(reTRIM.format('SCRIPT'), '', re.sub(reTRIM.format('STYLE'), '', self.body))self.body = re.sub(reTAG, '', self.body)def __processBlocks(self):"""处理body区块, 获取文本密度最大的区块"""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)self.start = self.end = self.cblocks.index(maxTextLen)while self.start > 0 and self.cblocks[self.start] > min(self.textLens):self.start -= 1while self.end < lines - self.blockSize and self.cblocks[self.end] > min(self.textLens):self.end += 1return ''.join(self.ctexts[self.start:self.end])def __processImages(self):"""处理图片"""self.body = reIMG.sub(r'{{\1}}', self.body)def getContent(self):"""获取纯文本(不带html标签)"""body = re.findall(reBODY, self.html)self.body = ''if body:self.body = body[0]if self.saveImage:self.__processImages()self.__processTags()return self.__processBlocks()
上述代码中,封装了一个解析类Extractor
,接受三个输入参数html,、blockSize、和saveImage。分别代表要解析的网页html文本、文本行长度列表窗口滑动次数、是否保留图片。其中blockSize
这个参数的设置是个经验值,默认设置为3。类提供了三个私有方法和一个公开方法,其中最核心的是私有方法__processBlocks
。该方法按行切割清理后的纯文本,保存每一行字符长度。然后根据设定的blockSize参数滑动文本行长度列表,做区间加计算,获得一个文本区块按行统计的文本密度队列。从文本密度最大的位置开始,起点向前移动,终点向后移动,当起点和终点都达到文本密度最低处时,即可确定起点和终点的位置,从而获取到正文的范围。
这样解释也许有点蒙,没关系,用起来就好了。现在就拿它测试几个网页试试:
if __name__ == '__main__':html = requests.get('https://dy.163.com/article/FFPLSI8P05439A3X.html').textex = Extractor(html=html, blockSize=3, saveImage=False)content = ex.getContent()print(content)
打印结果如下:
中国式的父母大多是不善于表达的 可能从来没听过他们嘴里说过“我爱你” 却从小到大用行动偷偷爱了你一辈子......马宗谦说:“我小的时候我母亲为了照顾我付出了很大的心血,现在我母亲身体不行了,有病了,我就是再照顾60年也报答不了她的恩情。” 即使看不见这个世界,但我能看见你爱我。 28 告别总有最后一次,希望不是每次都满含愧疚。父母在,人生尚有来处,父母去,人生只剩归途。 在一起的时候,当时只道是寻常 寻常到不懂得怎么去珍惜 转眼他们都老了,他们也走了 时光却不能倒流..... 树欲静而风不止,子欲养而亲不待 且行且珍惜!
结果很完美,就是期待得到的正文部分。
换个网页试试:
if __name__ == '__main__':html = requests.get('https://sports.sina.com.cn/basketball/nba/2020-06-22/doc-iirczymk8281587.shtml').contenthtml = str(html, 'utf-8')ex = Extractor(html=html, blockSize=3, saveImage=False)content = ex.getContent()print(content)
结果如下:
意甲-C罗破荒+进球无效迪巴拉世界波尤文2-0胜两名球员确定放弃参加复赛家人和健康更重要英超-福登马赫雷斯双响席尔瓦进球曼城5-0大捷咋跟曼城玩啊!一看替补席就绝望了坐着4亿英镑意甲-攻击线全面开花恰神造4球AC米兰客场4-1胜大乐透开2注封顶奖3600万!下期一等派奖6400万神仙打架!32+12+10对轰55分!这可是总决赛啊尼克斯获准面试基德意在助力招募字母哥?曼联阵中世界级从两人变为一人他必须扛起球队曼联水货终于有人要了?!大罗:他可以来我们队
这显然不是期待的正文内容,而是侧边栏的阅读排行榜。这里就体现了按文本密度计算的局限性了。正文很短,短到还不如页面其他部分文本密度高,结果就出错了。
总结一下,文本密度算法提取正文适用于正文很长(至少比其他部分字数多)的场景;对于正文很短的场景,需要搭配定制的规则(选择器,解析规则),才能获得预期的结果。




