正文
0x00 前言
上一篇
主要如何通过向浏览器页面注入 JavaScript 代码来尽可能地获取页面上的链接信息,最后完成一个稳定可靠的单页面链接信息抓取组件。这一篇我们跳到一个更大的世界,看一下整个漏扫爬虫的运转流程,这一篇会着重描写爬虫架构设计以及调度部分。
0x01 设计
这张图片是不是很熟悉,其实这就是 Scrapy 的架构设计图,我们简单看一下这张图的流程:
-
Engine 拿到 Requests
-
Engine 将 Requests 丢到 Scheduler 中,并向 Scheduler 请求下一个准备抓取的 Request
-
Scheduler 返回下一个准备抓取的 Request
-
Engine 将 Request 丢到 Downloader 中,中途经过 Downloader Middlewares 处理
-
Downloader 处理 Request 产生 Response 返回给 Engine,中途经过 Downloader Middlewares 处理
-
Engine 将 Response 丢到 Spider 中,中途经过 Spider Middleware 处理
-
Spider 处理 Response 产生出 item 和新的 Requests 返回给 Engine,中途经过 Spider Middleware 处理
-
Engine 将 item 丢到 Item Pipelines 处理,同时将 Requests 丢到 Scheduler 中
-
重复 1-8 步骤,直到 Scheduler 没有新的 Requests
在整体架构上我直接参考了 Scrapy 的设计,只不过我实在受不了 Twisted 那种扭曲的写法, 所以直接换了个网络库重新造了个和 Scrapy 差不多的轮子,新的架构图如下:
上面架构图中消息队列(MQ)左边的内部名为 CasterPy,右边的内部名为 CasterJS, 我们前两篇主要介绍的单页面链接信息抓取组件(CasterJS)就是上面的架构设计中的 Downloader, 我们的架构设计和 Scrapy 的区别是:
-
我们的 Downloader 直接返回链接信息而不是返回响应内容
-
我们的 Downloader 是分布式的,可部署在不同的服务器上
-
我们的 Engine 通过消息队列与 Downloader 通信
-
我们的 Downloader 针对同一个站点并发数始终为 1
-
我们的 CasterPy 使用协程同时处理多个站点,可同时和多个 Downloader 进行通信
我们的 Spider 组件也只是简单的解析链接信息返回相对应的 item 和新的 Request,这部分没什么好讲的, 我们的 Engine 组件和 Scrapy 的也差不多,就是 Item、Request、Response 的搬运工,这部分也不用细讲, 至于 Item Pipelines,最后数据怎么存储、存储到哪里去,每家公司都有自己的想法(每家公司的想法差距都挺大的),这个就仁者见仁,
剩下就只有 Scheduler 了。
0x02 调度
Scheduler 决定了 Request 的优先级、去留,漏扫爬虫的 Scheduler 和普通爬虫的 Scheduler 最大的区别是如何决定 Request 的去留,也就是爬虫的去重问题。
去重真的是我在写漏扫爬虫除了 QtWebkit 之外最头疼的事情了。针对漏扫爬虫的去重,完全就没有什么比较好的公开的策略去处理, 老生常谈的 Bloom Filter 在漏扫爬虫中毫无用武之地。
普通爬虫一般来说只会丢弃非目标、已爬取的 Request,但在漏扫爬虫中完全不能只做这些, 因为这样不仅会浪费爬虫的资源,也会浪费后续检测的资源,所以我们需要自己造一个去重策略对 Request 进行更深层次的去重。
资源去重
我们在使用 Chromium 加载一个页面的时候,Chromium 会对网络资源做分类,这些分类主要有:
我们在之前注入的 JavaScript 代码在获取链接信息的时候也采取了这样的分类(虽然我之前没讲=。=),那很明显,我们只需要对 Doc 类型的 Request 进行再入 download 队列,其他资源都没必要再使用浏览器再下载渲染一遍。
链接去重
在最初的几年前在头疼去重这个问题的时候,剑心和我讨论的结果是可以把 request 中的参数分为 action 类型和 data 类型:
简单的讲,action 类型的参数就是语言 vm 中 opcode,data 类型就是语言 vm 中的操作数, 我们就是希望能够从 request 数据中分析出哪些是 action类型的参数,哪些是 data 类型的参数,然后再进行去重。
我们看个简单的例子:
其中 a 就是属于 action 类型的参数,因为 a 的值必须是 create 才会有数据库操作的逻辑。 b 属于 data 类型的参数,因为 b 的值无关紧要,不会影响到代码执行逻辑。
从代码中很容易分析出参数的类型,可是仅仅从 url 中怎么区别参数类型呢? 这个时候我们就需要从开发人员写代码的心理去推测参数类型了。