14.4 Bloom Filter 的对接
首先回顾一下 Scrapy-Redis 的去重机制。Scrapy-Redis 将 Request 的指纹存储到了 Redis 集合中,每个指纹的长度为 40,例如 27adcc2e8979cdee0c9cecbbe8bf8ff51edefb61 就是一个指纹,它的每一位都是 16 进制数。 我们计算一下用这种方式耗费的存储空间。每个十六进制数占用 4 b,1 个指纹用 40 个十六进制数表示,占用空间为 20 B,1 万个指纹即占用空间 200 KB,1 亿个指纹占用 2 GB。当爬取数量达到上亿级别时,Redis 的占用的内存就会变得很大,而且这仅仅是指纹的存储。Redis 还存储了爬取队列,内存占用会进一步提高,更别说有多个 Scrapy 项目同时爬取的情况了。当爬取达到亿级别规模时,Scrapy-Redis 提供的集合去重已经不能满足我们的要求。所以我们需要使用一个更加节省内存的去重算法 Bloom Filter。
1. 了解 BloomFilter
Bloom Filter,中文名称叫作布隆过滤器,是 1970 年由 Bloom 提出的,它可以被用来检测一个元素是否在一个集合中。Bloom Filter 的空间利用效率很高,使用它可以大大节省存储空间。Bloom Filter 使用位数组表示一个待检测集合,并可以快速地通过概率算法判断一个元素是否存在于这个集合中。利用这个算法我们可以实现去重效果。 本节我们来了解 Bloom Filter 的基本算法,以及 Scrapy-Redis 中对接 Bloom Filter 的方法。
2. BloomFilter 的算法
在 Bloom Filter 中使用位数组来辅助实现检测判断。在初始状态下,我们声明一个包含 m 位的位数组,它的所有位都是 0,如图 14-7 所示。
图 14-7 初始位数组 现在我们有了一个待检测集合,我们表示为 S={x1, x2, …, xn},我们接下来需要做的就是检测一个 x 是否已经存在于集合 S 中。在 BloomFilter 算法中首先使用 k 个相互独立的、随机的哈希函数来将这个集合 S 中的每个元素 x1、x2、…、xn 映射到这个长度为 m 的位数组上,哈希函数得到的结果记作位置索引,然后将位数组该位置索引的位置 1。例如这里我们取 k 为 3,即有三个哈希函数,x1 经过三个哈希函数映射得到的结果分别为 1、4、8,x2 经过三个哈希函数映射得到的结果分别为 4、6、10,那么就会将位数组的 1、4、6、8、10 这五位置 1,如图 14-8 所示:
图 14-8 映射后位数组 这时如果再有一个新的元素 x,我们要判断 x 是否属于 S 这个集合,我们便会将仍然用 k 个哈希函数对 x 求映射结果,如果所有结果对应的位数组位置均为 1,那么我们就认为 x 属于 S 这个集合,否则如果有一个不为 1,则 x 不属于 S 集合。 例如一个新元素 x 经过三个哈希函数映射的结果为 4、6、8,对应的位置均为 1,则判断 x 属于 S 这个集合。如果结果为 4、6、7,7 对应的位置为 0,则判定 x 不属于 S 这个集合。 注意这里 m、n、k 满足的关系是 m>nk,也就是说位数组的长度 m 要比集合元素 n 和哈希函数 k 的乘积还要大。 这样的判定方法很高效,但是也是有代价的,它可能把不属于这个集合的元素误认为属于这个集合,我们来估计一下它的错误率。当集合 S={x1, x2,…, xn} 的所有元素都被 k 个哈希函数映射到 m 位的位数组中时,这个位数组中某一位还是 0 的概率是:
因为哈希函数是随机的,所以任意一个哈希函数选中这一位的概率为 1/m,那么 1-1/m 就代表哈希函数一次没有选中这一位的概率,要把 S 完全映射到 m 位数组中,需要做 kn 次哈希运算,所以最后的概率就是 1-1/m 的 kn 次方。 一个不属于 S 的元素 x 如果要被误判定为在 S 中,那么这个概率就是 k 次哈希运算得到的结果对应的位数组位置都为 1,所以误判概率为:
根据:
可以将误判概率转化为:
在给定 m、n 时,可以求出使得 f 最小化的 k 值为:
在这里将误判概率归纳如下: 表 14-1 误判概率
m/n
最优 k
k=1
k=2
k=3
k=4
k=5
k=6
k=7
k=8
2
1.39
0.393
0.400
3
2.08
0.283
0.237
0.253
4
2.77
0.221
0.155
0.147
0.160
5
3.46
0.181
0.109
0.092
0.092
0.101
6
4.16
0.154
0.0804
0.0609
0.0561
0.0578
0.0638
7
4.85
0.133
0.0618
0.0423
0.0359
0.0347
0.0364
8
5.55
0.118
0.0489
0.0306
0.024
0.0217
0.0216
0.0229
9
6.24
0.105
0.0397
0.0228
0.0166
0.0141
0.0133
0.0135
0.0145
10
6.93
0.0952
0.0329
0.0174
0.0118
0.00943
0.00844
0.00819
0.00846
11
7.62
0.0869
0.0276
0.0136
0.00864
0.0065
0.00552
0.00513
0.00509
12
8.32
0.08
0.0236
0.0108
0.00646
0.00459
0.00371
0.00329
0.00314
13
9.01
0.074
0.0203
0.00875
0.00492
0.00332
0.00255
0.00217
0.00199
14
9.7
0.0689
0.0177
0.00718
0.00381
0.00244
0.00179
0.00146
0.00129
15
10.4
0.0645
0.0156
0.00596
0.003
0.00183
0.00128
0.001
0.000852
16
11.1
0.0606
0.0138
0.005
0.00239
0.00139
0.000935
0.000702
0.000574
17
11.8
0.0571
0.0123
0.00423
0.00193
0.00107
0.000692
0.000499
0.000394
18
12.5
0.054
0.0111
0.00362
0.00158
0.000839
0.000519
0.00036
0.000275
19
13.2
0.0513
0.00998
0.00312
0.0013
0.000663
0.000394
0.000264
0.000194
20
13.9
0.0488
0.00906
0.0027
0.00108
0.00053
0.000303
0.000196
0.00014
21
14.6
0.0465
0.00825
0.00236
0.000905
0.000427
0.000236
0.000147
0.000101
22
15.2
0.0444
0.00755
0.00207
0.000764
0.000347
0.000185
0.000112
7.46e-05
23
15.9
0.0425
0.00694
0.00183
0.000649
0.000285
0.000147
8.56e-05
5.55e-05
24
16.6
0.0408
0.00639
0.00162
0.000555
0.000235
0.000117
6.63e-05
4.17e-05
25
17.3
0.0392
0.00591
0.00145
0.000478
0.000196
9.44e-05
5.18e-05
3.16e-05
26
18
0.0377
0.00548
0.00129
0.000413
0.000164
7.66e-05
4.08e-05
2.42e-05
27
18.7
0.0364
0.0051
0.00116
0.000359
0.000138
6.26e-05
3.24e-05
1.87e-05
28
19.4
0.0351
0.00475
0.00105
0.000314
0.000117
5.15e-05
2.59e-05
1.46e-05
29
20.1
0.0339
0.00444
0.000949
0.000276
9.96e-05
4.26e-05
2.09e-05
1.14e-05
30
20.8
0.0328
0.00416
0.000862
0.000243
8.53e-05
3.55e-05
1.69e-05
9.01e-06
31
21.5
0.0317
0.0039
0.000785
0.000215
7.33e-05
2.97e-05
1.38e-05
7.16e-06
32
22.2
0.0308
0.00367
0.000717
0.000191
6.33e-05
2.5e-05
1.13e-05
5.73e-06
表 14-1 中第一列为 m/n 的值,第二列为最优 k 值,其后列为不同 k 值的误判概率,可以看到当 k 值确定时,随着 m/n 的增大,误判概率逐渐变小。当 m/n 的值确定时,当 k 越靠近最优 K 值,误判概率越小。另外误判概率总体来看都是极小的,在容忍此误判概率的情况下,大幅减小存储空间和判定速度是完全值得的。 接下来我们就将 BloomFilter 算法应用到 Scrapy-Redis 分布式爬虫的去重过程中,以解决 Redis 内存不足的问题。
3. 对接 Scrapy-Redis
实现 BloomFilter 时,我们首先要保证不能破坏 Scrapy-Redis 分布式爬取的运行架构,所以我们需要修改 Scrapy-Redis 的源码,将它的去重类替换掉。同时 BloomFilter 的实现需要借助于一个位数组,所以既然当前架构还是依赖于 Redis 的,那么正好位数组的维护直接使用 Redis 就好了。 首先我们实现一个基本的哈希算法,可以实现将一个值经过哈希运算后映射到一个 m 位位数组的某一位上,代码实现如下:
1 |
class HashMap(object): |
在这里新建了一个 HashMap 类,构造函数传入两个值,一个是 m 位数组的位数,另一个是种子值 seed,不同的哈希函数需要有不同的 seed,这样可以保证不同的哈希函数的结果不会碰撞。 在 hash() 方法的实现中,value 是要被处理的内容,在这里我们遍历了该字符的每一位并利用 ord() 方法取到了它的 ASCII 码值,然后混淆 seed 进行迭代求和运算,最终会得到一个数值。这个数值的结果就由 value 和 seed 唯一确定,然后我们再将它和 m 进行按位与运算,即可获取到 m 位数组的映射结果,这样我们就实现了一个由字符串和 seed 来确定的哈希函数。当 m 固定时,只要 seed 值相同,就代表是同一个哈希函数,相同的 value 必然会映射到相同的位置。所以如果我们想要构造几个不同的哈希函数,只需要改变其 seed 就好了,以上便是一个简易的哈希函数的实现。 接下来我们再实现 BloomFilter,BloomFilter 里面需要用到 k 个哈希函数,所以在这里我们需要对这几个哈希函数指定相同的 m 值和不同的 seed 值,在这里构造如下:
1 |
BLOOMFILTER_HASH_NUMBER = 6 |
由于我们需要亿级别的数据的去重,即前文介绍的算法中的 n 为 1 亿以上,哈希函数的个数 k 大约取 10 左右的量级,而 m>kn,所以这里 m 值大约保底在 10 亿,由于这个数值比较大,所以这里用移位操作来实现,传入位数 bit,定义 30,然后做一个移位操作 1 << 30,相当于 2 的 30 次方,等于 1073741824,量级也是恰好在 10 亿左右,由于是位数组,所以这个位数组占用的大小就是 2^30b=128MB,而本文开头我们计算过 Scrapy-Redis 集合去重的占用空间大约在 2G 左右,可见 BloomFilter 的空间利用效率之高。 随后我们再传入哈希函数的个数,用它来生成几个不同的 seed,用不同的 seed 来定义不同的哈希函数,这样我们就可以构造一个哈希函数列表,遍历 seed,构造带有不同 seed 值的 HashMap 对象,保存成变量 maps 供后续使用。 另外 server 就是 Redis 连接对象,key 就是这个 m 位数组的名称。 接下来我们就要实现比较关键的两个方法了,一个是判定元素是否重复的方法 exists(),另一个是添加元素到集合中的方法 insert(),实现如下:
1 |
def exists(self, value): |
首先我们先看下 insert() 方法,BloomFilter 算法中会逐个调用哈希函数对放入集合中的元素进行运算得到在 m 位位数组中的映射位置,然后将位数组对应的位置置 1,所以这里在代码中我们遍历了初始化好的哈希函数,然后调用其 hash() 方法算出映射位置 offset,再利用 Redis 的 setbit() 方法将该位置 1。 在 exists() 方法中我们就需要实现判定是否重复的逻辑了,方法参数 value 即为待判断的元素,在这里我们首先定义了一个变量 exist,然后遍历了所有哈希函数对 value 进行哈希运算,得到映射位置,然后我们用 getbit() 方法取得该映射位置的结果,依次进行与运算。这样只有每次 getbit() 得到的结果都为 1 时,最后的 exist 才为 True,即代表 value 属于这个集合。如果其中只要有一次 getbit() 得到的结果为 0,即 m 位数组中有对应的 0 位,那么最终的结果 exist 就为 False,即代表 value 不属于这个集合。这样此方法最后的返回结果就是判定重复与否的结果了。 到现在为止 BloomFilter 的实现就已经完成了,我们可以用一个实例来测试一下,代码如下:
1 |
conn = StrictRedis(host='localhost', port=6379, password='foobared') |
在这里我们首先定义了一个 Redis 连接对象,然后传递给 BloomFilter,为了避免内存占用过大这里传的位数 bit 比较小,设置为 5,哈希函数的个数设置为 6。 首先我们调用 insert() 方法插入了 Hello 和 World 两个字符串,随后判断了一下 Hello 和 Python 这两个字符串是否存在,最后输出它的结果,运行结果如下:
1 |
True |
很明显,结果完全没有问题,这样我们就借助于 Redis 成功实现了 BloomFilter 的算法。 接下来我们需要继续修改 Scrapy-Redis 的源码,将它的 dupefilter 逻辑替换为 BloomFilter 的逻辑,在这里主要是修改 RFPDupeFilter 类的 request_seen() 方法,实现如下:
1 |
def request_seen(self, request): |
首先还是利用 request_fingerprint() 方法获取了 Request 的指纹,然后调用 BloomFilter 的 exists() 方法判定了该指纹是否存在,如果存在,则证明该 Request 是重复的,返回 True,否则调用 BloomFilter 的 insert() 方法将该指纹添加并返回 False,这样就成功利用 BloomFilter 替换了 Scrapy-Redis 的集合去重。 对于 BloomFilter 的初始化定义,我们可以将 init() 方法修改为如下内容:
1 |
def __init__(self, server, key, debug, bit, hash_number): |
其中 bit 和 hash_number 需要使用 from_settings() 方法传递,修改如下:
1 |
@classmethod |
其中常量的定义 DUPEFILTER_DEBUG 和 BLOOMFILTER_BIT 统一定义在 defaults.py 中,默认如下:
1 |
BLOOMFILTER_HASH_NUMBER = 6 |
到此为止我们就成功实现了 BloomFilter 和 Scrapy-Redis 的对接。
4. 本节代码
本节代码地址为:https://github.com/Python3WebSpider/ScrapyRedisBloomFilter。
5. 使用
为了方便使用,本节的代码已经打包成了一个 Python 包并发布到了 PyPi,链接为:https://pypi.python.org/pypi/scrapy-redis-bloomfilter,因此我们以后如果想使用 ScrapyRedisBloomFilter 直接使用就好了,不需要再自己实现一遍。 我们可以直接使用 Pip 来安装,命令如下:
1 |
pip3 install scrapy-redis-bloomfilter |
使用的方法和 Scrapy-Redis 基本相似,在这里说明几个关键配置:
1 |
# 去重类,要使用 BloomFilter 请替换 DUPEFILTER_CLASS |
DUPEFILTER_CLASS 是去重类,如果要使用 BloomFilter 需要将 DUPEFILTER_CLASS 修改为该包的去重类。 BLOOMFILTER_HASH_NUMBER 是 BloomFilter 使用的哈希函数的个数,默认为 6,可以根据去重量级自行修改。 BLOOMFILTER_BIT 即前文所介绍的 BloomFilter 类的 bit 参数,它决定了位数组的位数,如果 BLOOMFILTER_BIT 为 30,那么位数组位数为 2 的 30 次方,将占用 Redis 128MB 的存储空间,去重量级在 1 亿左右,即对应爬取量级 1 亿左右。如果爬取量级在 10 亿、20 亿甚至 100 亿,请务必将此参数对应调高。
6. 测试
在源代码中附有一个测试项目,放在 tests 文件夹,该项目使用了 Scrapy-RedisBloomFilter 来去重,Spider 的实现如下:
1 |
from scrapy import Request, Spider |
在 start_requests() 方法中首先循环 10 次,构造参数为 0-9 的 URL,然后重新循环了 100 次,构造了参数为 0-99 的 URL,那么这里就会包含 10 个重复的 Request,我们运行项目测试一下:
1 |
scrapy crawl test |
可以看到最后的输出结果如下:
1 |
{'bloomfilter/filtered': 10, |
可以看到最后统计的第一行的结果:
1 |
'bloomfilter/filtered': 10, |
这就是 BloomFilter 过滤后的统计结果,可以看到它的过滤个数为 10 个,也就是它成功将重复的 10 个 Reqeust 识别出来了,测试通过。
7. 结语
以上便是 BloomFilter 的原理及对接实现,使用了 BloomFilter 可以大大节省 Redis 内存,在数据量大的情况下推荐使用此方案。




图 14-3 代理池接口
图 14-4 Cookies 池接口 所以接下来我们就需要把 Scrapy 新浪微博项目中的访问链接修改如下:
图 14-5 去重指纹
图 14-6 爬取队列 随着时间的推移,指纹集合会不断增长,爬取队列会动态变化,爬取的数据也会被储存到 MongoDB 数据库中。 至此 Scrapy 分布式的配置已全部完成。
图 14-1 调度架构 如果两个 Scheduler 同时从队列里面取 Request,每个 Scheduler 都有其对应的 Downloader,那么在带宽足够、正常爬取且不考虑队列存取压力的情况下,爬取效率会有什么变化?没错,爬取效率会翻倍。 这样,Scheduler 可以扩展多个,Downloader 也可以扩展多个。而爬取队列 Queue 必须始终为一个,也就是所谓的共享爬取队列。这样才能保证 Scheduer 从队列里调度某个 Request 之后,其他 Scheduler 不会重复调度此 Request,就可以做到多个 Schduler 同步爬取。这就是分布式爬虫的基本雏形,简单调度架构如图 14-2 所示。
图 14-2 调度架构 我们需要做的就是在多台主机上同时运行爬虫任务协同爬取,而协同爬取的前提就是共享爬取队列。这样各台主机就不需要各自维护爬取队列,而是从共享爬取队列存取 Request。但是各台主机还是有各自的 Scheduler 和 Downloader,所以调度和下载功能分别完成。如果不考虑队列存取性能消耗,爬取效率还是会成倍提高。
图 13-32 个人详情页面 我们在页面最上方可以看到她的关注和粉丝数量。我们点击关注,进入到她的关注列表,如图 13-33 所示。
图 13-33 关注列表 我们打开开发者工具,切换到 XHR 过滤器,一直下拉关注列表,即可看到下方会出现很多 Ajax 请求,这些请求就是获取关注列表的 Ajax 请求,如图 13-34 所示。
图 13-34 请求列表 我们打开第一个 Ajax 请求看一下,发现它的链接为:
图 13-35 请求详情
图 13-36 响应结果 请求类型是 GET 类型,返回结果是 JSON 格式,我们将其展开之后即可看到其关注的用户的基本信息。接下来我们只需要构造这个请求的参数。此链接一共有 7 个参数,如图 13-37 所示。
图 13-37 参数信息 其中最主要的参数就是 containerid 和 page。有了这两个参数,我们同样可以获取请求结果。我们可以将接口精简为:
图 13-38 用户信息
图 13-39 微博信息 针对用户信息,我们不仅爬取了其基本信息,还把关注和粉丝列表加到了 follows 和 fans 字段并做了去重操作。针对微博信息,我们成功进行了时间转换处理,同时还保存了微博的图片列表信息。
图 13-32 运行结果 如果出现类似图 13-29 的运行结果,这就证明构建的镜像没有问题。
图 13-30 推送结果 如果我们想在其他的主机上运行这个镜像,主机上装好 Docker 后,可以直接执行如下命令:
图 13-31 运行效果 整个项目爬取完成后,数据就可以存储到指定的数据库中。
图 13-28 输出结果 返回的是一个 JSON 格式的字符串,我们解析它的结构,如下所示:
最后经排查,发现后端接口使用时设定了 



图 13-19 爬取站点
图 13-20 页面内容 这是新闻的列表页,下一步自然就是将列表中的每条新闻详情的链接提取出来。这里直接指定这些链接所在区域即可。查看源代码,所有链接都在 ID 为 left_side 的节点内,具体来说是它内部的 class 为 con_item 的节点,如图 13-21 所示。
图 13-21 列表源码 此处我们可以用 LinkExtractor 的 restrict_xpaths 属性来指定,之后 Spider 就会从这个区域提取所有的超链接并生成 Request。但是,每篇文章的导航中可能还有一些其他的超链接标签,我们只想把需要的新闻链接提取出来。真正的新闻链接路径都是以 article 开头的,我们用一个正则表达式将其匹配出来再赋值给 allow 参数即可。另外,这些链接对应的页面其实就是对应的新闻详情页,而我们需要解析的就是新闻的详情信息,所以此处还需要指定一个回调函数 callback。 到现在我们就可以构造出一个 Rule 了,代码如下所示:
图 13-22 分页源码 但是,下一页节点和其他分页链接区分度不高,要取出此链接我们可以直接用 XPath 的文本匹配方式,所以这里我们直接用 LinkExtractor 的 restrict_xpaths 属性来指定提取的链接即可。另外,我们不需要像新闻详情页一样去提取此分页链接对应的页面详情信息,也就是不需要生成 Item,所以不需要加 callback 参数。另外这下一页的页面如果请求成功了就需要继续像上述情况一样分析,所以它还需要加一个 follow 参数为 True,代表继续跟进匹配分析。其实,follow 参数也可以不加,因为当 callback 为空的时候,follow 默认为 True。此处 Rule 定义为如下所示:
图 13-23 运行效果
图 13-24 详情页面 如果像之前一样提取内容,就直接调用 response 变量的 xpath()、css() 等方法即可。这里 parse_item() 方法的实现如下所示:
图 13-25 输出内容 现在我们就可以成功将每条新闻的信息提取出来。 不过我们发现这种提取方式非常不规整。下面我们再用 Item Loader,通过 add_xpath()、add_css()、add_value() 等方式实现配置化提取。我们可以改写 parse_item(),如下所示:
图 13-26 运行结果 现在我们已经对 Spider 的基础属性实现了可配置化。剩下的解析部分同样需要实现可配置化,原来的解析函数如下所示:
图 13-27 运行结果 运行结果是完全相同的。 我们再回过头看一下 start_urls 的配置。这里 start_urls 只可以配置具体的链接。如果这些链接有 100 个、1000 个,我们总不能将所有的链接全部列出来吧?在某些情况下,start_urls 也需要动态配置。我们将 start_urls 分成两种,一种是直接配置 URL 列表,一种是调用方法生成,它们分别定义为 static 和 dynamic 类型。 本例中的 start_urls 很明显是 static 类型的,所以 start_urls 配置改写如下所示: ```json”start_urls”: {“type”:”static”,”value”: [“
图 13-15 页面截图 翻页操作也成功实现,如图 13-16 所示即为当前页码,和我们传入的页码 page 参数是相同的。
图 13-16 翻页结果 我们只需要在 Spider 里用 SplashRequest 对接 Lua 脚本就好了,如下所示:
图 13-17 运行结果 由于 Splash 和 Scrapy 都支持异步处理,我们可以看到同时会有多个抓取成功的结果。在 Selenium 的对接过程中,每个页面渲染下载是在 Downloader Middleware 里完成的,所以整个过程是阻塞式的。Scrapy 会等待这个过程完成后再继续处理和调度其他请求,这影响了爬取效率。因此使用 Splash 的爬取效率比 Selenium 高很多。 最后我们再看看 MongoDB 的结果,如图 13-18 所示。
图 13-18 存储结果 结果同样正常保存到了 MongoDB 中。
图 13-13 运行结果 再查看一下 MongoDB,结果如图 13-14 所示:
图 13-14 MongoDB 结果 这样我们便成功在 Scrapy 中对接 Selenium 并实现了淘宝商品的抓取。
图 13-6 请求列表 我们查看一个请求的详情,观察返回的数据结构,如图 13-7 所示。
图 13-7 返回结果 返回格式是 JSON。其中 list 字段就是一张张图片的详情信息,包含了 30 张图片的 ID、名称、链接、缩略图等信息。另外观察 Ajax 请求的参数信息,有一个参数 sn 一直在变化,这个参数很明显就是偏移量。当 sn 为 30 时,返回的是前 30 张图片,sn 为 60 时,返回的就是第 31~60 张图片。另外,ch 参数是摄影类别,listtype 是排序方式,temp 参数可以忽略。 所以我们抓取时只需要改变 sn 的数值就好了。 下面我们用 Scrapy 来实现图片的抓取,将图片的信息保存到 MongoDB、MySQL,同时将图片存储到本地。
图 13-8 运行结果 所有请求的状态码都是 200,这就证明图片信息爬取成功了。
图 13-9 输出日志 查看本地 images 文件夹,发现图片都已经成功下载,如图 13-10 所示。
图 13-10 下载结果 查看 MySQL,下载成功的图片信息也已成功保存,如图 13-11 所示。
图 13-11 MySQL 结果 查看 MongoDB,下载成功的图片信息同样已成功保存,如图 13-12 所示。
图 13-12 MongoDB 结果 这样我们就可以成功实现图片的下载并把图片的信息存入数据库了。
图 13-1 Scrapy 架构 它可以分为如下的几个部分。
图 9-10 预装操作系统 推荐安装 CentOS 7 系统。 然后找到远程管理面板 远程连接的用户名和密码,也就是 SSH 远程连接服务器的信息。比如我使用的 IP 和端口是 153.36.65.214:20063,用户名是 root。命令行下输入如下内容:
图 9-11 配置页面 提示成功之后就可以进行拨号了。注意,在拨号之前测试 ping 任何网站都是不通的,因为当前网络还没联通。输入如下拨号命令:
图 9-12 拨号建立连接 断线重播的命令就是二者组合起来,先执行 adsl-stop,再执行 adsl-start。每次拨号,ifconfig 命令观察主机的 IP,发现主机的 IP 一直在变化,网卡名称叫作 ppp0,如图 9-13 所示。
图 9-13 网络设备信息 接下来,我们要做两件事:一是怎样将主机设置为代理服务器,二是怎样实时获取拨号主机的 IP。
图 9-14 运行结果 如果有正常的结果输出,并且 origin 的值为代理 IP 的地址,就证明 TinyProxy 配置成功了。
图 9-15 Hash 结构 如果有多台主机,只需要向 Hash 中添加映射即可。 另外,get() 方法就是从散列表中取出某台主机对应的代理。remove() 方法则是从散列表中移除对应的主机的代理。还有 names()、proxies()、all() 方法则是分别获取散列表中的主机列表、代理列表及所有主机代理映射。count() 方法则是返回当前散列表的大小,也就是可用代理的数目。 最后还有一个比较重要的方法 random(),它随机从散列表中取出一个可用代理,类似前面代理池的思想,确保每个代理都能被取到。 如果要对数据库进行操作,只需要初始化 RedisClient 对象,然后调用它的 set() 或者 remove() 方法,即可对散列表进行设置和删除。
图 9-16 示例输出 首先移除了代理,再进行拨号,拨号完成之后获取新的 IP,代理检测成功之后就设置到 Redis 散列表中,然后等待一段时间再重新进行拨号。 我们添加了多台拨号主机,这样就有多个稳定的定时更新的代理可用了。Redis 散列表会实时更新各台拨号主机的代理,如图 9-17 所示。
图 9-17 Hash 结构 图中所示是四台 ADSL 拨号主机配置并运行后的散列表的内容,表中的代理都是可用的。
图 9-18 主页面 访问 proxies 接口可以获得所有代理列表,如图 9-19 所示。
图 9-19 代理列表 访问 random 接口可以获取随机可用代理,如图 9-20 所示。
图 9-20 随机代理 我们只需将接口部署到服务器上,即可通过 Web 接口获取可用代理,获取方式和代理池类似。
图 13-5 Scrapy Shell 我们可以在命令行模式下输入命令调用对象的一些操作方法,回车之后实时显示结果。这与 Python 的命令行交互模式是类似的。 接下来,演示的实例都将页面的源码作为分析目标,页面源码如下所示:
图 13-2 页面结构 提取的方式可以是 CSS 选择器或 XPath 选择器。在这里我们使用 CSS 选择器进行选择,parse() 方法的改写如下所示:
图 13-3 页面底部 有一个 Next 按钮,查看一下源代码,可以发现它的链接是 /page/2/,实际上全链接就是:
图 13-4 爬取结果 长的 text 已经被处理并追加了省略号,短的 text 保持不变,author 和 tags 也都相应保存。
图 12-26 运行页面 也可以单独运行 pyspider 的某一个组件。 运行 Scheduler 的命令如下所示:
图 12-27 抓取进度 progress 中的 5m、1h、1d 指的是最近 5 分、1 小时、1 天内的请求情况,all 代表所有的请求情况。 蓝色的请求代表等待被执行的任务,绿色的代表成功的任务,黄色的代表请求失败后等待重试的任务,红色的代表失败次数过多而被忽略的任务,从这里我们可以直观看到爬取的进度和请求情况。
图 12-2 运行结果 这样可以启动 pyspider 的所有组件,包括 PhantomJS、ResultWorker、Processer、Fetcher、Scheduler、WebUI,这些都是 pyspider 运行必备的组件。最后一行输出提示 WebUI 运行在 5000 端口上。可以打开浏览器,输入链接
图 12-3 WebUI 页面 此页面便是 pyspider 的 WebUI,我们可以用它来管理项目、编写代码、在线调试、监控任务等。
图 12-4 创建项目 接下来会看到 pyspider 的项目编辑和调试页面,如图 12-5 所示。
图 12-5 调试页面 左侧就是代码的调试页面,点击左侧右上角的 run 单步调试爬虫程序,在左侧下半部分可以预览当前的爬取页面。右侧是代码编辑页面,我们可以直接编辑代码和保存代码,不需要借助于 IDE。 注意右侧,pyspider 已经帮我们生成了一段代码,代码如下所示:
图 12-6 操作示例 左栏左上角会出现当前 run 的配置文件,这里有一个 callback 为 on_start,这说明点击 run 之后实际是执行了 on_start() 方法。在 on_start() 方法中,我们利用 crawl() 方法生成一个爬取请求,那下方 follows 部分的数字 1 就代表了这一个爬取请求。 点击下方的 follows 按钮,即可看到生成的爬取请求的链接。每个链接的右侧还有一个箭头按钮,如图 12-7 所示。
图 12-7 操作示例 点击该箭头,我们就可以对此链接进行爬取,也就是爬取攻略的首页内容,如图 12-8 所示。
图 12-8 爬取结果 上方的 callback 已经变成了 index_page,这就代表当前运行了 index_page() 方法。index_page() 接收到的 response 参数就是刚才生成的第一个爬取请求的 Response 对象。index_page() 方法通过调用 doc() 方法,传入提取所有 a 节点的 CSS 选择器,然后获取 a 节点的属性 href,这样实际上就是获取了第一个爬取页面中的所有链接。然后在 index_page() 方法里遍历了所有链接,同时调用 crawl() 方法,就把这一个个的链接构造成新的爬取请求了。所以最下方 follows 按钮部分有 217 的数字标记,这代表新生成了 217 个爬取请求,同时这些请求的 URL 都呈现在当前页面了。 再点击下方的 web 按钮,即可预览当前爬取结果的页面,如图 12-9 所示。
图 12-9 预览页面 当前看到的页面结果和浏览器看到的几乎是完全一致的,在这里我们可以方便地查看页面请求的结果。 点击 html 按钮即可查看当前页面的源代码,如图 12-10 所示。
图 12-10 页面源码 如果需要分析代码的结构,我们可以直接参考页面源码。 我们刚才在 index_page() 方法中提取了所有的链接并生成了新的爬取请求。但是很明显要爬取的肯定不是所有链接,只需要攻略详情的页面链接就够了,所以我们要修改一下当前 index_page() 里提取链接时的 CSS 选择器。 接下来需要另外一个工具。首先切换到 Web 页面,找到攻略的标题,点击下方的 enable css selector helper,点击标题。这时候我们看到标题外多了一个红框,上方出现了一个 CSS 选择器,这就是当前标题对应的 CSS 选择器,如图 12-11 所示。
图 12-11 CSS 工具 在右侧代码选中要更改的区域,点击左栏的右箭头,此时在上方出现的标题的 CSS 选择器就会被替换到右侧代码中,如图 12-12 所示。
图 12-12 操作结果 这样就成功完成了 CSS 选择器的替换,非常便捷。 重新点击左栏右上角的 run 按钮,即可重新执行 index_page() 方法。此时的 follows 就变成了 10 个,也就是说现在我们提取的只有当前页面的 10 个攻略,如图 12-13 所示。
图 12-13 运行结果 我们现在抓取的只是第一页的内容,还需要抓取后续页面,所以还需要一个爬取链接,即爬取下一页的攻略列表页面。我们再利用 crawl() 方法添加下一页的爬取请求,在 index_page() 方法里面添加如下代码,然后点击 save 保存:
图 12-14 运行结果 现在,索引列表页的解析过程我们就完成了。
图 12-15 运行结果 切换到 Web 页面预览效果,页面下拉之后,头图正文中的一些图片一直显示加载中,如图 12-16 和图 12-17 所示。
图 12-16 预览结果
图 12-17 预览结果 查看源代码,我们没有看到 img 节点,如图 12-18 所示。
图 12-18 源代码 出现此现象的原因是 pyspider 默认发送 HTTP 请求,请求的 HTML 文档本身就不包含 img 节点。但是在浏览器中我们看到了图片,这是因为这张图片是后期经过 JavaScript 出现的。那么,我们该如何获取呢? 幸运的是,pyspider 内部对接了 PhantomJS,那么我们只需要修改一个参数即可。 我们将 index_page() 中生成抓取详情页的请求方法添加一个参数 fetch_type,改写的 index_page() 变为如下内容:
图 12-19 爬取详情 再点击新生成的详情页 Request 的爬取按钮,这时我们便可以看到页面变成了这样子,如图 12-20 所示。
图 12-20 运行结果 图片被成功渲染出来,这就是启用了 PhantomJS 渲染后的结果。只需要添加一个 fetch_type 参数即可,这非常方便。 最后就是将详情页中需要的信息提取出来,提取过程不再赘述。最终 detail_page() 方法改写如下所示:
图 12-21 输出结果 左栏中输出了最终构造的字典信息,这就是一篇攻略的抓取结果。
图 12-22 启动爬虫 在最左侧我们可以定义项目的分组,以方便管理。rate/burst 代表当前的爬取速率,rate 代表 1 秒发出多少个请求,burst 相当于流量控制中的令牌桶算法的令牌数,rate 和 burst 设置的越大,爬取速率越快,当然速率需要考虑本机性能和爬取过快被封的问题。process 中的 5m、1h、1d 指的是最近 5 分、1 小时、1 天内的请求情况,all 代表所有的请求情况。请求由不同颜色表示,蓝色的代表等待被执行的请求,绿色的代表成功的请求,黄色的代表请求失败后等待重试的请求,红色的代表失败次数过多而被忽略的请求,这样可以直观知道爬取的进度和请求情况,如图 12-23 所示。
图 12-23 爬取情况 点击 Active Tasks,即可查看最近请求的详细状况,如图 12-24 所示。
图 12-24 最近请求 点击 Results,即可查看所有的爬取结果,如图 12-25 所示。
图 12-25 爬取结果 点击右上角的按钮,即可获取数据的 JSON、CSV 格式。
图 12-1 pyspider 架构图 Scheduler 发起任务调度,Fetcher 负责抓取网页内容,Processer 负责解析网页内容,然后将新生成的 Request 发给 Scheduler 进行调度,将生成的提取结果输出保存。 pyspider 的任务执行流程的逻辑很清晰,具体过程如下所示。













在开始了解 X-Forward-For 之前,我们先来假设一个场景。你是一名爬虫工程师,现在要爬取目标网站 xxx.com 上面的内容。在编码的时候,你发现单位时间内请求频率过高时会被限制,猜测应该是目标网站针对 IP 地址做了限制。现在你有两种选择:
那么问题来了:
图 forwarded-client-server 由于客户端到代理 1 的请求没有使用代理,所以值为空或短横线。到代理 2 时,中间经过了代理 1,所以值为原始 IP。到服务端时,中间经过了代理 1 和代理2 ,所以值为原始 IP 和代理 1 IP。 上面就是关于 RFC7239 中部分内容的解读。看到这里,想必你已有丝丝头绪,接下来我们再捋一捋。
将代码片段 Forwarded-Test 中用于设置代理服务器 IP 和端口号的字段值改为高匿 IP 及对应的端口号即可,例如:






图 11-12 mitmproxy 运行结果 右下角会出现当前正在监听的端口。 或者启动 mitmdump,它也会监听 8080 端口,命令如下所示:
图 11-13 MitmDump 运行结果 将手机和 PC 连接在同一局域网下,设置代理为当前代理。首先看看 PC 的当前局域网 IP。 Windows 上的命令如下所示:
图 11-14 查看局域网 IP 一般类似 10... 或 172.16.. 或 192.168.1. 这样的 IP 就是当前 PC 的局域网 IP,例如此图中 PC 的 IP 为 192.168.1.28,手机代理设置类似如图 11-15 所示。
图 11-15 代理设置 这样我们就配置好了 mitmproxy 的的代理。
图 11-16 所有请求 这就相当于之前我们在浏览器开发者工具监听到的浏览器请求,在这里我们借助于 mitmproxy 完成。Charles 完全也可以做到。 这里是刚才手机打开百度页面时的所有请求列表,左下角显示的 2/38 代表一共发生了 38 个请求,当前箭头所指的是第二个请求。 每个请求开头都有一个 GET 或 POST,这是各个请求的请求方式。紧接的是请求的 URL。第二行开头的数字就是请求对应的响应状态码,后面是响应内容的类型,如 text/html 代表网页文档、image/gif 代表图片。再往后是响应体的大小和响应的时间。 当前呈现了所有请求和响应的概览,我们可以通过这个页面观察到所有的请求。 如果想查看某个请求的详情,我们可以敲击回车,进入请求的详情页面,如图 11-17 所示。
图 11-17 详情页面 可以看到 Headers 的详细信息,如 Host、Cookies、User-Agent 等。 最上方是一个 Request、Response、Detail 的列表,当前处在 Request 这个选项上。这时我们再点击 TAB 键,即可查看这个请求对应的响应详情,如图 11-18 所示。
图 11-18 响应详情 最上面是响应头的信息,下拉之后我们可以看到响应体的信息。针对当前请求,响应体就是网页的源代码。 这时再敲击 TAB 键,切换到最后一个选项卡 Detail,即可看到当前请求的详细信息,如服务器的 IP 和端口、HTTP 协议版本、客户端的 IP 和端口等,如图 11-19 所示。
图 11-19 详细信息 mitmproxy 还提供了命令行式的编辑功能,我们可以在此页面中重新编辑请求。敲击 e 键即可进入编辑功能,这时它会询问你要编辑哪部分内容,如 Cookies、Query、URL 等,每个选项的第一个字母会高亮显示。敲击要编辑内容名称的首字母即可进入该内容的编辑页面,如敲击 m 即可编辑请求的方式,敲击 q 即可修改 GET 请求参数 Query。 这时我们敲击 q,进入到编辑 Query 的页面。由于没有任何参数,我们可以敲击 a 来增加一行,然后就可以输入参数对应的 Key 和 Value,如图 11-20 所示。
图 11-20 编辑页面 这里我们输入 Key 为 wd,Value 为 NBA。 然后再敲击 esc 键和 q 键,返回之前的页面,再敲击 e 和 p 键修改 Path。和上面一样,敲击 a 增加 Path 的内容,这时我们将 Path 修改为 s,如图 11-21 所示。
图 11-21 编辑页面 再敲击 esc 和 q 键返回,这时我们可以看到最上面的请求链接变成了
图 11-22 请求详情 敲击 a 保存修改,敲击 r 重新发起修改后的请求,即可看到上方请求方式前面多了一个回旋箭头,这说明重新执行了修改后的请求。这时我们再观察响应体内容,即可看到搜索 NBA 的页面结果的源代码,如图 11-23 所示。
图 11-23 响应结果 以上内容便是 mitmproxy 的简单用法。利用 mitmproxy,我们可以观察到手机上的所有请求,还可以对请求进行修改并重新发起。 Fiddler、Charles 也有这个功能,而且它们的图形界面操作更加方便。那么 mitmproxy 的优势何在? mitmproxy 的强大之处体现在它的另一个工具 mitmdump,有了它我们可以直接对接 Python 对请求进行处理。下面我们来看看 mitmdump 的用法。
图 11-24 手机端页面 PC 端控制台输出如图 11-25 所示。
图 11-25 PC 端控制台 手机端返回结果的 Headers 实际上就是请求的 Headers,User-Agent 被修改成了 mitmproxy。PC 端控制台输出了修改后的 Headers 内容,其 User-Agent 的内容正是 mitmproxy。 所以,通过这三行代码我们就可以完成对请求的改写。print() 方法输出结果可以呈现在 PC 端控制台上,可以方便地进行调试。
图 11-26 运行结果 不同的颜色对应不同级别的输出,我们可以将不同的结果合理划分级别输出,以更直观方便地查看调试信息。
图 11-27 输出结果 结果中分别输出了请求链接、请求头、请求 Cookies、请求 Host、请求方法、请求端口、请求协议这些内容。 同时我们还可以对任意属性进行修改,就像最初修改 Headers 一样,直接赋值即可。例如,这里将请求的 URL 修改一下,脚本修改如下所示:
图 11-28 手机端页面 比较有意思的是,浏览器最上方还是呈现百度的 URL,但是页面已经变成了 httpbin.org 的页面了。另外,Cookies 明显还是百度的 Cookies。我们只是用简单的脚本就成功把请求修改为其他的站点。通过这种方式修改和伪造请求就变得轻而易举。 通过这个实例我们知道,有时候 URL 虽然是正确的,但是内容并非是正确的。我们需要进一步提高自己的安全防范意识。 Request 还有很多属性,在此不再一一列举。更多属性可以参考:
图 11-29 PC 端控制台 控制台输出了响应的状态码、响应头、Cookies、响应体这几部分内容。 我们可以通过 response() 方法获取每个请求的响应内容。接下来再进行响应的信息提取和存储,我们就可以成功完成爬取了。
图 11-1 Charles 运行界面 Charles 会一直监听 PC 和手机发生的网络数据包,捕获到的数据包就会显示在左侧,随着时间的推移,捕获的数据包越来越多,左侧列表的内容也会越来越多。 可以看到,图中左侧显示了 Charles 抓取到的请求站点,我们点击任意一个条目便可以查看对应请求的详细信息,其中包括 Request、Response 等内容。 接下来清空 Charles 的抓取结果,点击左侧的扫帚按钮即可清空当前捕获到的所有请求。然后点击第二个监听按钮,确保监听按钮是打开的,这表示 Charles 正在监听 App 的网络数据流,如图 11-2 所示。
图 11-2 监听过程 这时打开手机京东,注意一定要提前设置好 Charles 的代理并配置好 CA 证书,否则没有效果。 打开任意一个商品,如 iPhone,然后打开它的商品评论页面,如图 11-3 所示。
图 11-3 评论页面 不断上拉加载评论,可以看到 Charles 捕获到这个过程中京东 App 内发生的所有网络请求,如图 11-4 所示。
图 11-4 监听结果 左侧列表中会出现一个 api.m.jd.com 链接,而且它在不停闪动,很可能就是当前 App 发出的获取评论数据的请求被 Charles 捕获到了。我们点击将其展开,继续上拉刷新评论。随着上拉的进行,此处又会出现一个个网络请求记录,这时新出现的数据包请求确定就是获取评论的请求。 为了验证其正确性,我们点击查看其中一个条目的详情信息。切换到 Contents 选项卡,这时我们发现一些 JSON 数据,核对一下结果,结果有 commentData 字段,其内容和我们在 App 中看到的评论内容一致,如图 11-5 所示。
图 11-5 Json 数据结果 这时可以确定,此请求对应的接口就是获取商品评论的接口。这样我们就成功捕获到了在上拉刷新的过程中发生的请求和响应内容。
图 11-6 监听结果 这个结果和原本在 Web 端用浏览器开发者工具内捕获到的结果形式是类似的。 接下来点击 Contents 选项卡,查看该请求和响应的详情信息。 上半部分显示的是 Request 的信息,下半部分显示的是 Response 的信息。比如针对 Reqeust,我们切换到 Headers 选项卡即可看到该 Request 的 Headers 信息,针对 Response,我们切换到 JSON TEXT 选项卡即可看到该 Response 的 Body 信息,并且该内容已经被格式化,如图 11-7 所示。
图 11-7 监听结果 由于这个请求是 POST 请求,所以我们还需要关心的就是 POST 的表单信息,切换到 Form 选项卡即可查看,如图 11-8 所示。
图 11-8 监听结果 这样我们就成功抓取 App 中的评论接口的请求和响应,并且可以查看 Response 返回的 JSON 数据。 至于其他 App,我们同样可以使用这样的方式来分析。如果我们可以直接分析得到请求的 URL 和参数的规律,直接用程序模拟即可批量抓取。
图 11-9 编辑页面 我们可以将 Form 中的某个字段移除,比如这里将 partner 字段移除,然后点击 Remove。这时我们已经对原来请求携带的 Form Data 做了修改,然后点击下方的 Execute 按钮即可执行修改后的请求,如图 11-10 所示。
图 11-10 编辑页面 可以发现左侧列表再次出现了接口的请求结果,内容仍然不变,如图 11-11 所示。
图 11-11 重新请求后结果 删除 Form 表单中的 partner 字段并没有带来什么影响,所以这个字段是无关紧要的。 有了这个功能,我们就可以方便地使用 Charles 来做调试,可以通过修改参数、接口等来测试不同请求的响应状态,就可以知道哪些参数是必要的哪些是不必要的,以及参数分别有什么规律,最后得到一个最简单的接口和参数形式以供程序模拟调用使用。
图 10-7 返回数据 但是,这个接口在没有登录的情况下会有请求频率检测。如果一段时间内访问太过频繁,比如打开这个链接,一直不断刷新,则会看到请求频率过高的提示,如图 10-8 所示。
图 10-8 提示页面 如果重新打开一个浏览器窗口,打开
图 10-9 对比页面 图中左侧是登录了账号之后请求接口的结果,右侧是未登录账号请求接口的结果,二者的接口链接是完全一样的。未登录状态无法正常访问,而登录状态可以正常显示。 因此,登录账号可以降低被封禁的概率。 我们可以尝试登录之后再做爬取,被封禁的几率会小很多,但是也不能完全排除被封禁的风险。如果一直用同一个账号频繁请求,那就有可能遇到请求过于频繁而封号的问题。 如果需要做大规模抓取,我们就需要拥有很多账号,每次请求随机选取一个账号,这样就降低了单个账号的访问频率,被封的概率又会大大降低。 那么如何维护多个账号的登录信息呢?这时就需要用到 Cookies 池了。接下来我们看看 Cookies 池的构建方法。
图 10-10 Cookies 池架构 Cookies 池架构的基本模块分为 4 块:存储模块、生成模块、检测模块和接口模块。每个模块的功能如下。
图 10-11 用户名密码 Hash 结构
图 10-12 用户名 Cookies Hash 结构 Hash 的 Key 就是账号,Value 对应着密码或者 Cookies。另外需要注意,由于 Cookies 池需要做到可扩展,存储的账号和 Cookies 不一定单单只有本例中的微博,其他站点同样可以对接此 Cookies 池,所以这里 Hash 的名称可以做二级分类,例如存账号的 Hash 名称可以为 accounts:weibo,Cookies 的 Hash 名称可以为 cookies:weibo。如要扩展知乎的 Cookies 池,我们就可以使用 accounts:zhihu 和 cookies:zhihu,这样比较方便。 好,接下来我们就创建一个存储模块类,用以提供一些 Hash 的基本操作,代码如下:
图 10-13 接口页面 爬虫只需要请求该接口就可以实现随机 Cookies 的获取。
图 10-1 开发者工具设置 点击登录按钮,这时便会看到开发者工具下方显示了各个请求过程,如图 10-2 所示。
图 10-2 请求过程 点击第一个请求,进入其详情页面,如图 10-3 所示。
图 10-3 详情页面 可以看到请求的 URL 为
图 10-4 详情页面 Headers 里面包含了 Cookies、Host、Origin、Referer、User-Agent 等信息。Form Data 包含了 5 个字段,commit 是固定的字符串 Sign in,utf8 是一个勾选字符,authenticity_token 较长,其初步判断是一个 Base64 加密的字符串,login 是登录的用户名,password 是登录的密码。 综上所述,我们现在无法直接构造的内容有 Cookies 和 authenticity_token。下面我们再来探寻一下这两部分内容如何获取。 在登录之前我们会访问到一个登录页面,此页面是通过 GET 形式访问的。输入用户名密码,点击登录按钮,浏览器发送这两部分信息,也就是说 Cookies 和 authenticity_token 一定是在访问登录页的时候设置的。 这时再退出登录,回到登录页,同时清空 Cookies,重新访问登录页,截获发生的请求,如图 10-5 所示。
图 10-5 截获请求 访问登录页面的请求如图所示,Response Headers 有一个 Set-Cookie 字段。这就是设置 Cookies 的过程。 另外,我们发现 Response Headers 没有和 authenticity_token 相关的信息,所以可能 authenticity_token 还隐藏在其他的地方或者是计算出来的。我们再从网页的源码探寻,搜索相关字段,发现源代码里面隐藏着此信息,它是一个隐藏式表单元素,如图 10-6 所示。
图 10-6 表单元素 现在我们已经获取到所有信息,接下来实现模拟登录。
由于这种验证码交互形式比较友好,且安全性、美观度上也会更高,像这种类似的验证码也变得越来越流行。另外不仅仅是「极验」,其他很多验证码服务商也推出了类似的验证码服务,如「网易易盾」等,上图所示的就是「网易易盾」的滑动验证码。 没错,确实这种滑动验证码的出现让很多网站变得更安全。但是做爬虫的可就苦恼了,如果采用自动化的方法来绕过这种滑动验证码,关键部分在于以下两点:
我们想知道这只狗在哪,它的舌头在哪,找到了就把它们框选出来,这就是目标检测。 经过目标检测算法处理之后,我们期望得到的图片是这样的:
可以看到这只狗和它的舌头就被框选出来了,这就完成了一个不错的目标检测。 现在比较流行的目标检测算法有 R-CNN、Fast R-CNN、Faster R-CNN、SSD、YOLO 等,感兴趣同学的可以了解一下,当然看不懂也没有什么影响。 另外再提一个地方,不懂深度学习的同学可以看看,懂的直接跳过下面一段。 我们既然要搭建一个模型来实现一个目标检测算法,那模型怎么知道我们究竟想识别个什么东西?就比如上图,模型咋知道我们想识别的是狗而不是草,是舌头而不是鼻子。这是因为,既然叫深度学习,那得有学习的东西。所以,搭建一个深度学习模型需要训练数据。啥也不告诉模型,模型从哪里去学习?所以,我们得预先有一些标注好位置的图片供模型去学习(训练),比如准备好多张狗的图片和狗的轮廓标注位置,模型在训练过程中会自动学习到图片和标注位置的关系。模型训练好了之后,我们给模型一个没有见过的类似的狗的图,模型也能找出来目标的位置了。 所以,迁移到验证码缺口识别这个任务上来,我们第一步就是给模型提供一些训练数据,训练数据就包括验证码的图片和缺口的位置标注轮廓信息。 好,既然如此,我们第一步就得准备一批验证码数据供标注和训练了。
我们不需要滑轨的部分,只保留验证码本身的图片和上面的两个缺口就行了,下面是我准备的一些验证码图:
我爬了大约上千张吧,越多越好。当然对于今天的任务来说,其实几十上百张已经就够了。
它已经内置了一些深度学习模型,包括图像分类、物体检测、预测分析等等,我们可以直接利用它们来快速搭建属于自己的模型。 在这里我们就切换到「自动学习」的选项卡,创建一个物体检测的项目。
进入项目里面,可以看到最上面会显示三个步骤:
那我们先来第一步——数据标注,这里我把一些验证码的图上传到页面中,在这里我上传了 112 张图:
上传完毕之后我们可以点击每一张图片进行标注了,这个平台提供了非常方便的标注功能,只需要鼠标拖拽个轮廓就完成了,112 张图标注完也就几分钟,标注的时候就框选这么个轮廓就行了,如图所示:
在这里边界需要把整个缺口的图全框选出来,其中上边界和右边界和标注框相切即可,总之确保标注框正好把缺口图框选出来就行,平台会自动保存和记录标注的像素点位置。 标注完一个,它会提示要添加一个名字,我在这里添加的名字叫「边界」,可以随意指定。 等全部标注完毕,点击「保存并返回」按钮即可。
在这里,我们只需要设置一下「最大训练时长」就好了,这么点图片其实几分钟就能训练完了,「最大训练时长」随意填写即可,最小不小于 0.05,填写完了之后就可以点击「开始训练」按钮训练了。 等几分钟,就会训练完成了,可以看到类似如图的页面:
这里显示了模型的各个参数和指标。 是的,你没看错,我们没有写任何代码,只过了几分钟,模型就已经训练完,并且可以部署上线了。
过一会儿, 部署成功之后便可以看到类似这样的界面:
在这里我们可以上传任意的验证码图片进行测试,比如我随意上传一张没有标注过的验证码图,然后它会给我们展示出预测结果,如图所示:
可以看到,它就把缺口的位置检测出来了,同时在右侧显示了具体的像素值和置信度:
AI 在图像识别和文本处理方面的效果尤为突出,且已经应用到人类的生活中,例如人脸识别、对话、车牌识别、城市智慧大脑项目中的目标检测和目标分类等。
接下来,我们将了解图像分类的需求、完成任务的前提条件和任务实践。
目标检测,指的是检测目标在图片中的位置。例如智慧交通项目中,路面监控摄像头拍摄画面中车辆的位置。目标检测涉及两种技术:分类和定位。也就是说先判定图片中是否存在指定的目标,然后还需要确定目标在图片中的位置。
这样的技术将会应用在人脸识别打卡、视频监控警报、停车场、高速收费站和城市智慧交通等项目当中。
简单来说,AI 工程师必须准备很多张不同的图片,并且将一大部分图片中的目标标注出来,然后让计算机提取每张图片中的特征,最后就会形成「认知」。 想一想,你还小的时候,是如何分辨鸭子和鹅的呢?
是不是根据它们的特征进行判断的?
AI 在硬件、软件、数据资料和人才方面都是很费钱的,普通的 IT 工程师也就是学习了解一下,远远达不到产品商用的要求。 普通的中小企业,极少有资质和经济能力吸引高学历且优秀的 AI 工程师,这就导致了资源的聚拢和倾斜。 想要将图像分类技术商用,在让计算机经历「看」、「认识」的步骤并拥有「分辨」能力后,还要将其转换为 Web 服务。
但我只想将人脸识别或者图像分类的功能集成到我的项目当中,就那么困难吗? 我只是一个很小的企业,想要在原来普通的视频监控系统中增加「家人识别」、「陌生人警报」、「火灾警报」和「生物闯入提醒」等功能,没有上述的条件和经济投入,就不能实现了吗? 我好苦恼! 有什么好办法吗?
它为机器学习与深度学习提供海量数据预处理及半自动化标注、大规模分布式 Training、自动化模型生成,及端-边-云模型按需部署能力,帮助用户快速创建和部署模型,管理全周期 AI 工作流。 它为用户提供了以下可选模式:
同时,它将 AI 开发的整个过程都集成了进来。例如数据标注、模型训练、参数优化、服务部署、开放接口等,这就是「全周期 AI 工作流」。
还有,平台上的操作都是可视化的。 这些条件对于想要将 AI 技术应用于产品,但无奈条件不佳的个人开发者和企业提供了机会,这很重要!可以说
想想就美滋滋,太棒了! 赶紧体验一下!
进入到
区域 2 自动学习模式中有图像分类,将鼠标移动到图标上,并点击弹出的「开始体验」按钮。如果是华为云的新用户,网页会提示我们输入访问密钥和私有访问密钥。
没有密钥的开发者可以点击页面给出的链接并按照指引获取密钥,得到两种密钥后将其填入框中,点击「确定」按钮即可。 此时正式进入项目创建流程中,点击「图像分类」中的「创建项目」按钮(华为云为用户准备了对应的教程,很贴心)。
在创建项目的页面中,我们需要填两三项配置。要注意的是,项目是按需计费的,这次我们只是体验,也没有训练和存储太多数据,所以费用很低,大家不用担心。 项目名称可以根据需求设定一个容易记的,案例中我将其设定为 ImageCLF-Test-Pro。在训练数据的存储选择处,点击输入框中的文件夹图标,在弹出的选项卡中新建 obs 桶
并在创建的桶中新建文件夹
最后输入描述,并点击页面右下角的「创建项目」按钮即可。
当然,数据越多、形态越丰富、标注越准确,那么训练结果就会越好,AI 服务的体验就会越好。 这里我准备了一些直升机、坦克和狗的图片,共 45 张。
依次将 3 类图片标注后,左侧图片标注的「未标注」选项卡中的图就会清空,而「已标注」选项卡中可以看到标注好的图片。
这个我们不必理解它的作用,可以按照默认值进行,也可以稍微调整,例如将训练时长的上限改为 0.2。
训练页左侧会显示训练状态,例如初始化、运行中和运行成功/失败等。训练完成后,右侧会给出运行时长、准确率、评估结果和训练参数等信息。 
稍微等待些许时间(本次约 10 分钟)后,页面提示部署完成,同时页面将会分为 3 栏。
左侧 1 区为部署状态和控制。中间 2 区可以在线测试图片分类,右侧 3 区会显示在线测试的结果(包括准确率),右侧 4 区提供了 API 接口,方便我们将其集成到 Web 应用当中。
1 秒中不到,右侧 3 区就会返回本次预测的结果:
返回的预测结果如下:
返回的预测结果如下:
图 9-21 搜索结果 点击搜索后,搜索结果的 URL 中其实有很多无关 GET 请求参数,将无关的参数去掉,只保留 type 和 query 参数,例如
图 9-22 翻页列表 注意,如果没有输入账号登录,那只能看到 10 页的内容,登录之后可以看到 100 页内容,如图 9-23 和图 9-24 所示。
图 9-23 不登录的结果
图 9-24 登录后的结果 如果需要爬取更多内容,就需要登录并使用 Cookies 来爬取。 搜狗微信站点的反爬虫能力很强,如连续刷新,站点就会弹出类似如图 9-25 所示的验证。
图 9-25 验证码页面 网络请求出现了 302 跳转,返回状态码为 302,跳转的链接开头为
图 9-26 代理池运行结果 这样,数据库中留下的 100 分的代理就是针对搜狗微信的可用代理了,如图 9-27 所示。
图 9-27 可用代理列表 同时访问代理接口,接口设置为 5555,访问
图 9-28 代理接口 再定义一个函数来获取随机代理:
图 9-29 运行结果 程序首先调度了第一页结果对应的请求,获取了代理执行此请求,随后得到了 11 个新请求,请求都是 WeixinRequest 类型,将其再加入队列。随后继续调度新加入的请求,也就是文章详情页对应的请求,再执行,得到的就是文章详情对应的提取结果,提取结果是字典类型。 程序循环往复,不断爬取,直至所有结果爬取完毕,程序终止,爬取完成。 爬取结果如图 9-30 所示。
图 9-30 爬取结果 我们可以看到,相关微信文章都已被存储到数据库里了。

很多岗位都要求有逆向或者解决反爬虫的能力
甚至作为优先选择的条件 

声明: 本次活动最终解释权归内容制作方夜幕团队所有。