Python实现大规模爬虫的一些套路

2020-12-26 16:10 Python之禅

我们身边接触最频繁、同时也是最大的爬虫莫过于几大搜索引擎。但是搜索引擎的爬取方式和我们爬虫工程师接触的方式差异比较大,没有太大的参考价值,我们今天要讲的是舆情方向的爬虫(架构以及关键技术原理),主要涉及:

1.网页文本智能提取;2.分布式爬虫;3.爬虫 DATA/URL 去重;4.爬虫部署;5.分布式爬虫调度;6.自动化渲染技术;7.消息队列在爬虫领域的应用;8.各种各样形式的反爬虫;

一、网页文本智能提取

舆情其实就是舆论情况,要掌握舆情,那么就必须掌握足够多的内容资讯。除了一些开放了商业接口的大型内容/社交类平台(例如微博)之外,其他都需要依靠爬虫去采集。因此,舆情方向的爬虫工程师需要面对的是千千万万个内容和结构都不同的站点。我们用一个图来表示他们面对的问题:

没错,他们的采集器必须要能够适配千千万万个站点的结构,从风格迥异的 HTML 文本中提取出主体内容——标题、正文、发布时间和作者。

如果是你,你会用什么样的设计来满足业务需求呢?

曾经我也设想过这样的问题,在技术群里也看到有群友提出类似的问题,但是很难得到满意的答案。有的人说:

1.用归类法,相似的内容归类到一起,然后给一类内容配置提取规则;2.用正则,提取指定标签中的内容;3.用深度学习,NLP 语义分析出哪里是有意义的内容,提取出来;4.用计算机视觉,让人去点击,然后按照页面相似度分类提取(其实就是归类法的自动化版本);5.用算法,计算出文本的密度,然后提取;

总之各种各样的想法层出不穷,但是最后都没有听到实际应用的消息。目前来说,大部分公司使用的都是人工配置 XPATH 规则的方式,采集的时候通过网址来匹配对应的提取规则,然后调用规则来实现多站点的爬取。这种方法很有效,而且在企业中应用已久,比较稳定,但缺点也显而易见——费时间、费人工、费钱!

偶有一天,我在微信技术群里看到有人(优秀的 Python 工程师青南)发表了一个用于自动化提取文本的算法库,GeneralNewsExtractor[1] (以下简称 GNE)。这个库参考了武汉邮电科学研究院洪鸿辉、丁世涛、黄傲、郭致远等人编写的论文——《基于文本及符号密度的网页正文提取方法》,并在论文的基础上用 Python 代码进行了具体实现,也就是 GNE。它的原理是通过提取网页 DOM 中的文本以及其中的标点符号,以文本中标点符号的密度作为基础,使用算法从一句话延伸到一段文字和一篇文章。

图片

GNE 能够有效排除正文以外的的广告、推荐栏、介绍栏等“噪声”内容,准确识别出网页正文,且识别率高达 99%(测试选用的内容为国内主流门户/媒体平台中的文章)。

GNE 的具体算法细节以及源码解析请翻阅《Python3 网络爬虫宝典》第 5 章。

有了它,基本上就可以解决 90% 以上的舆情方向爬虫解析的需求,剩下的 10% 可以基于提取规则进行针对性调整或者完全定制化开发,解放了一大波 XPATH 工程师。

二、爬虫 DATA/URL 去重

舆情业务必须紧盯网站是否有新的内容发布,要求是越快越好,但由于各项软硬件限制,通常会要求在 30 分钟内或者 15 分钟内监听到新内容。要实现对目标网站内容变化的监听,那么我们可以选择的比较好的方式就是轮询。不停地访问网页,并且判断是否有“新内容”出现,如果有的话就执行爬取,没有“新内容”就不爬取。

那么问题来了,应用程序如何知道哪些内容是“新的”、哪些内容又是“旧的”的呢?

问题拆解一下,“新内容”就是没有爬过的内容。这时候我们需要用一个东西来记录这篇文章是否被爬取过,每次有要爬取的文章时就比对一下,这就是解决这个问题的办法。

那又依靠什么来进行比对呢?

我们都知道文章的 URL 几乎都是不变且不会重复的,因此可以选择文章的 URL 作为判定的依据,也就是把爬取过的 URL 放到一个类似列表一样的容器里存储起来,每次有待爬取的 URL 就判断它是否已经被存在容器里,如果在就说明已经爬过了,直接丢弃,进入下一个 URL 的判断流程。整体逻辑就像下面这张图一样:

这就是爬虫领域的“去重”。实际上去重可以粗略的分为内容(DATA)去重和链接(URL)去重,这里我们讲的只是舆情方向的去重需求,如果是电商方向的去重,那么就不能用 URL 作为判断依据,因为电商爬虫(例如比价软件)的目的主要是判断价格的变化,这时候判断变化的依据应该是商品的关键信息(例如价格、折扣),也就是 DATA 去重。

去重的原理明白了,那用什么东西作为存放去重依据的容器呢?MySQL?Redis?MongoDB?内存?实际上大部分工程师都选择 Redis 作为存放去重依据的容器,但实际上 MySQL、MongoDB 和内存都是可以充当容器的,至于为什么会选择 Redis,它又比其他数据存储好在哪里?你可以翻阅《Python3 网络爬虫宝典》的第 3 章。

三、分布式爬虫

无论是舆情方向的爬虫还是电商方向的爬虫,要承担的爬取量都是非常大的。少则每日百万数据,多则每日数十亿数据。以往大家了解的单机爬虫,在性能和资源方面都无法满足需求。既然 1 个满足不了,那就 10 个、100 个!这就是分布式爬虫出现的背景。

众所周知,分布式和单机要面对的问题是有差异的,除了业务目标是相同的之外,分布式还要考虑多个个体之间的协作,尤其是资源的共享和竞争。

在只有 1 个爬虫应用的时候,也只有它 1 个读取待爬队列、只有 1 个存储数据、只有它 1 个判断 URL 是否重复。但有几十几百个爬虫应用的时候,就需要区分先后顺序,避免出现多个爬虫应用访问同一个 URL 的情况(因为这不仅浪费时间,还浪费资源)。而且,只有 1 个爬虫应用的时候只需要把它放在 1 台计算机(服务器)上运行就可以了,但是爬虫应用突然变得这么多,又应该如何部署到不同的计算机上呢?手动一个个上传,然后一个个启动吗?

资源问题

先说资源共享和竞争的情况,为了解决 URL 待爬队列和已经爬队列的共享,那么必须将队列(也就是上面提到的存放 URL 的容器)放到一个可以公开(多个爬虫应用)访问的地方,例如部署在服务器上的 Redis。

这时候又出现一个新状况,随着数据量越来越大,要存储的 URL 越来越多,后面很有可能出现因为存储空间需求过大而导致成本递增的问题。因为 Redis 是利用内存来存储数据的,所以存放的 URL 越多就需要越多的内存,而内存又是硬件设备里价格相对较高的硬件,所以不得不考虑这个问题。

好在一个叫做布隆的人发明了一种算法——Bloom Filter(布隆过滤器),这种算法通过哈希映射的方式来标记一个对象(这里是 URL)是否存在,这样可以将内存的占用率大大降低,按 1 亿条长度为 32 字符的 URL MD5 值来计算,使用 Bloom Filter 前后的差距大约在 30倍。关于 Bloom Filter 的算法原理和代码实现解读可翻阅《Python3 网络爬虫宝典》第 3 章 。

部署问题

一个个文件上传,一次次手动运行爬虫实在是太累了。你可以向运维同事寻求技术支持,但也可以自己探寻这些能够减轻你工作量的自动化部署方式。目前业内知名的持续集成和部署莫过于 GitLab 的 GitLab Runner 和 GitHub Action,又或者是借助 K8S 的容器化来实现。但它们只能帮助你实现部署和启动,而爬虫应用的一些管理功能就指望不上了。遂,今天要给大家介绍的是另一种实现方式——使用 Crawlab。

Crawlab 是一款由知名外企工程师开发的分布式爬虫管理平台,它不仅支持 Python 语言编写的爬虫,几乎可以兼容大部分编程语言和应用程序。借助 Crawlab,我们可以将爬虫应用分散到不同的计算机(服务器)上,而且能够在可视化界面设定定时任务、查看平台上爬虫应用的状态以及环境依赖等信息。具体如下图所示:

面对一款如此实用的平台工具,作为工程师的我们不禁想问:

1.它是如何把文件分散到不同计算机的?2.它如何实现不同计算机(多节点)之间通信的?3.它是如何实现多语言兼容的?4.……

其中我们比较关注的多节点通信是借助 Redis 实现的,文件分散同步是借助 MongoDB 实现的。更多细节可翻阅《Python3 网络爬虫宝典》 第 6 章。

除了这样的平台之外,Python 爬虫工程师常常接触的莫过于 Scrapy 框架以及相关衍生的库。Scrapy 团队官方开发了一个名为 Scrapyd 的库,它专门用来部署 Scrapy 框架开发的爬虫应用。在部署 Scrapy 应用时,我们通常只需要执行 1 行命令就可以把爬虫程序部署到服务器上。你想不想知道背后的逻辑:

1.程序以什么样的形式上传到服务器的?2.程序在服务器上如何运行的?3.为什么可以查看到每个任务运行的开始时间和结束时间?4.中途取消任务执行的功能是怎么实现的?5.它的版本控制是怎么实现的?6.如果不是 Scrapy 框架编写的 Python 应用,能实现像上面几点那样的监控和操作吗?

实际上 Scrapy 应用会被打包成为一个后缀为“.egg” 的压缩包,以 HTTP 的形式上传到服务器上。当服务端程序需要执行这个程序时,先将它复制到操作系统的临时文件夹,执行时将其导入到当前 Python 环境,执行完毕后删除该文件。至于它的执行时间和中断操作,实际上借助了 Python 进程接口,具体细节翻阅《Python3 网络爬虫宝典》 第 6 章。

四、自动化渲染技术

为了实现炫酷的效果,或者说为了节省静态资源对带宽的占用,很多网站都是借助 JavaScript 来实现对页面内容的优化。Python 程序本身是无法解释 JavaScript 和 HTML 代码的,因此无法获得我们在浏览器中“看到”,但实际上并不是“真实存在”的内容,因为这些内容都是由浏览器渲染出来的,只存在于浏览器中,HTML 文档里面还是那些文本、JavaScript 文件中还是那些代码,图片、视频和那些特效并不会出现在代码中,我们看到的一切都是浏览器的功劳。

由于 Python 也无法获取浏览器渲染后的内容,所以当我们像往常一样写代码爬取上面的数据时,就会发现拿到的数据和看到的并不一样,任务它就失败了。

这时候我们就需要用到自动化渲染技术了,实际上像 Chrome 和 FireFox 这样的浏览器都开放了接口,允许其他编程语言按照协议规范操控浏览器。基于这样的技术背景,有团队开发出了像 Selenium 和 Puppeteer 这样的工具,然后我们就可以用 Python (其他语言也可以)代码来操作浏览器了。让浏览器帮助我们做一些用户名密码输入、登录按钮点击、文本和图片渲染、验证码滑动等操作,从而打破 Python 与浏览器本身的差异壁垒,借助浏览器渲染内容后再返回给 Python 程序,然后拿到和我们在网页上看到的一样的内容。

除了浏览器,APP 也有类似的情况。具体操作实践和案例细节可翻阅《Python3 网络爬虫宝典》 第 2 章。

五、消息队列在爬虫领域的应用

之前的描述中,我们并没有提到爬取时候的细节。假设这样一个正常的爬虫场景:爬虫先访问网站的文章列表页,然后根据列表页的 URL 进入详情页进行爬取。这里要注意,文章详情页的数量一定是比列表页的数量多 N 倍的,如果列表展示的是 20 条内容,那么就是多 20 倍。

如果我们需要爬取的网站很多,那么就会用到分布式爬虫。如果分布式爬虫只是把 1 个爬虫程序复制出 N 份来运行,那么就会出现资源分配不均衡的情况,因为在上面提到的这种情况下,每 1 个爬虫都需要这么干活。实际上我们可以有更好的搭配方式,让它们的资源得到最大利用。例从列表页到详情页可以抽象为生产者和消费者模型:

4 号和 5 号爬虫应用只负责将列表页中抽取详情页的 URL,然后推送到一个队列中,另外几个爬虫程序从队列中取出详情页的 URL 进行爬取。当列表页和详情页数量差距比较大的时候,我们可以增加右侧的爬虫程序数量,差距较小的时候就减少右侧的爬虫程序(或者增加左侧的爬虫程序,具体视情况定)。

左侧的爬虫程序相对于队列这条“数据采集生产线”来说,它就是生产者,右侧爬虫程序的就是消费者。有了这样的结构,我们就可以根据实际情况对生产者或者消费者的熟练进行调整,实现资源的最大化利用。另外一个好处是当生产者拿到的 URL 越来越多,但消费者一时消费不过来时,URL 会一直存放在队列中,等消费能力上升时就能够再次实现均衡。有了这样的生产线,我们就不用担心一下突然涌来很多的 URL 或者一下突然把队列的 URL 消费一空,队列这种削峰填谷的能力除了在后端应用中大放异彩之外,在爬虫应用中也发挥了很大的作用。

文章里面提及的Python3 网络爬虫宝典就是下面这本书。关于大规模爬虫的套路,可以从这本书中去找答案。

福利时间

最后来抽个奖,一共10本书,扫下方二维码关注公众号之后直接回复666即可参与抽奖,感谢博文视点杨老师赞助支持。

本文章转载自公众号:VTtalk

首页 - Python 相关的更多文章: