上一篇主要如何通过向浏览器页面注入 JavaScript 代码来尽可能地获取页面上的链接信息,最后完成一个稳定可靠的单页面链接信息抓取组件。这一篇我们跳到一个更大的世界,看一下整个漏扫爬虫的运转流程,这一篇会着重描写爬虫架构设计以及调度部分。
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 了。
Scheduler 决定了 Request 的优先级、去留,漏扫爬虫的 Scheduler 和普通爬虫的 Scheduler 最大的区别是如何决定 Request 的去留,也就是爬虫的去重问题。
去重真的是我在写漏扫爬虫除了 QtWebkit 之外最头疼的事情了。针对漏扫爬虫的去重,完全就没有什么比较好的公开的策略去处理, 老生常谈的 Bloom Filter 在漏扫爬虫中毫无用武之地。
普通爬虫一般来说只会丢弃非目标、已爬取的 Request,但在漏扫爬虫中完全不能只做这些, 因为这样不仅会浪费爬虫的资源,也会浪费后续检测的资源,所以我们需要自己造一个去重策略对 Request 进行更深层次的去重。
我们在使用 Chromium 加载一个页面的时候,Chromium 会对网络资源做分类,这些分类主要有:
我们在之前注入的 JavaScript 代码在获取链接信息的时候也采取了这样的分类(虽然我之前没讲=。=),那很明显,我们只需要对 Doc 类型的 Request 进行再入 download 队列,其他资源都没必要再使用浏览器再下载渲染一遍。
在最初的几年前在头疼去重这个问题的时候,剑心和我讨论的结果是可以把 request 中的参数分为 action 类型和 data 类型:
action 类型: 对代码逻辑产生影响的参数
data 类型: 在代码中作为数据使用,一般不会影响到代码逻辑的参数
简单的讲,action 类型的参数就是语言 vm 中 opcode,data 类型就是语言 vm 中的操作数, 我们就是希望能够从 request 数据中分析出哪些是 action类型的参数,哪些是 data 类型的参数,然后再进行去重。
我们看个简单的例子:
其中 a 就是属于 action 类型的参数,因为 a 的值必须是 create 才会有数据库操作的逻辑。 b 属于 data 类型的参数,因为 b 的值无关紧要,不会影响到代码执行逻辑。
从代码中很容易分析出参数的类型,可是仅仅从 url 中怎么区别参数类型呢? 这个时候我们就需要从开发人员写代码的心理去推测参数类型了。
首先一般开发人员不会使用中文作为 action 类型参数的值,很难想象会有人这样写代码:
所以带中文字符的参数,可以直接被认为是 data 类型的参数。
其次一般开发人员的不会使用超过 2 位的纯数字作为 action 类型的值:
再次一般开发人员也不会使用 HASH/UUID 值作为 action 类型的值:
还记得上一篇我们提到自动化填写表单的时候,最好能够自定义输入的地方都填上带 casterjs 字符吗? 就是为了能够在这里直接区分出带 casterjs 值的参数都是 data 类型参数。
最后一般开发人员也不会使用 … (自由想象、发挥、总结规律)
其实我们这个过程就是在猜,猜测一个正常的开发人员的编码规范。 前期通过各种猜测,我们可以对下面这些类型的 url 简单去重:
因为上面的参数 b 被识别成 data 类型参数,所以理论上 b 的值被替换成什么都无所谓, 我们将 data 类型参数的值替换成 { { data }} 得到 “临时规则”:
上面这些去重步骤仅仅是第一步,接下来我们还要考虑下面这种情况:
通过第一步简单替换之后,得到 “临时规则”:
这样的结果我们并不是特别满意,但通过第一步简单替换也只能得到这样的结果了。但随着第三条 “临时规则” 命中的 url 越来越多, 我们就越有理由相信参数 b 就是 data 类型的参数,参数 a 就是 action 类型的参数, 所以刚刚得到的 “临时规则” 在命中次数达到我们所设定的一个阈值后,还可以变成 “最后规则” :
上面这条就是去重过程中生成的去重 “最后规则”,根据这条 “最后规则” 我们又可以直接对下面的链接直接去重:
“临时规则” 只有统计的作用,并不能参与去重复步骤,但是 “最后规则” 可以参与去重。就如同刚才所示,”临时规则” 可以发展成 “最后规则”。 在 Scheduler 去重中,我们最希望拿到的并不是 url,而是实时在变化的去重规则,通过越来越多的 url 生成越来越精准的去重规则, 再通过越来越精准的规则反过来再对以后以及之前的 url 进行去重,得到重复度越来越低的 url,这就是我们造的去重策略。
前面我们根据猜测开发人员心理去制定去重策略,这里我们还需要继续猜测 URL Rewrite 配置人员的心理去完善我们的去重策略。
我们先看一下几种常规的 URL Rewrite 之后 url 的样子:
我们先假设上面的 view 就是我们所说的 action 类型参数,123 就是 data 类型参数, 针对 URL Rewrite 之后的 url,我们首先应该找到各个参数之间的间隔符号是什么,上面的例子中参数间隔符号分别是 /、-、_, 然后以根路径开始,用 1、2、3 顺序作为 key,对应 path 深度的值作为 value,最终还是可以直接转换成 key-value 格式:
最后还是通过之前的去重策略进行去重,发现了么,漏扫去重这块大多数时候都只能猜测,并没有一个万能的解决方法。
经过简单的测试,在 2 核 4G 内存服务器上能同时跑 50 个 Chromium Tab, 在 4 核 8G 内存服务器上一个 CasterPy 能够同时跑 1000 个任务, 也就是说一台 CasterPy 服务器可以和 20 台 CasterJS 服务器构成一个小规模的爬虫。 如果任务并发数增加,那也得相对应增加 CasterPy 服务器的资源以及 CasterJS 服务器的数量了。
至此,扫描器中爬虫部分就算简单地过了一遍,虽然讲得比较粗略,,但不管怎么样也得切到下一个话题了。
下一篇我们讲一下 Web 漏洞扫描器中漏洞检测技巧部分。