目录
3.1 为链接爬虫添加缓存支持
要想支持缓存 ,我们需要修改第1章中编写的download函数,使其在要想支持缓存 ,我们需要修改第1章中编写的download函数,使其在URL下载之前进行缓存检查 。另外,我们还需要把限速功能移至函数内部,只有在真正发生下载时才会触发限速 ,而在加载缓存时不会触发 。为了避免每次下载都要传入多个参数,我们借此机会将download函数重构为一个类,这样参数只需在构造方法中设置一次 ,就能在后续下载时多次复用 。下面是支持了缓存功能的代码实现。
class Downloade r :
de f 一_init_ ( s elf, delay= S ,
user_agent= ’ wswp ’ , proxies=None,
num retries=! , cache=N。ne ) :
s elf. throttle = Throttle ( delay)
self . us er_agent = user_agent
s e l f . proxies = proxies
s e l f . num retries = num retries
s e l f . cache = cache
de f_call_( s elf, url ) :
result = None
if self.cache :
try :
result = se lf. cache [ url ]
except KeyError :
#
url is not ava i l able in cache
pass
else:
if self. num retries > 0 and \
500 <= result [ ’ code ’ l < 600 :
#
s erver e rror so ignore result from cache
#
and re-down load
result = N.ne
if result is N.ne :
# result was not loaded from cache
# so still need to download
self . throttle . wait ( ur l )
proxy = random . choice ( s elf . proxi e s) if s e lf. proxies
else None
headers = { ’ User-agent ’ : s el f . user_agent }
result = s el f . download ( url , headers , pr。xy,
self . num retries )
if self . cache :
# s ave result to cache
self . cache [ url ] = result
return result [ ’ html ’ ]
def downl oad (self, url , headers , proxy , num_retr ies ,
data=None ) :
return { ’ html ’ : html ,
前面代码中的Download类有一个比较有意思的部分 ,那就是 _call_
特殊方法 ,在该方法中 我们实现了下载前检查缓存的功能 。该方法首先会检查缓存是否已经定义 。如果已经定义, 则检查之前是否已经缓存了该URL。如果该URL己被缓存 ,则检查之前的下载中是否遇到了服务端错误 。最后 ,如果也没有发生过服务端错误 ,则表明该缓存结果可用 。如果上述检查 中 的 任何一项失败, 都需要正常下载该 URL,然后将得到的结果添加到缓存中 。这里的 download方法和之前的download函数基本一样 ,只是在返回下载的HTML时额外返回了 HTTP态码,以便在缓存中存储错误码 。当然 ,如果你只需要一个简单的下载功能,而不需要限速或缓存的话,可以直接调用 该方法,这样就不会通过 _call_ 方法调用了 。
3.2 磁盘缓存
要想缓存下载结果 ,我们先来尝试最容易想到的方案 ,将下载到的网页存储到文件系统中 。为了 实现该功能 ,我们需要将URL安全地映射为跨平台的文件名 。表 3.1所示为几大主流文件系统的 限制 。
![](https://i-blog.csdnimg.cn/blog_migrate/7eed93851e7ca0050a2d35b58308ce22.png)
为了保证在不同文件系统中 ,我们的文件路径都是安全的 ,就需要限制其只能包含数字 、字母和基本符号 ,并将其他字符替换为下划线 ,其实现代码
>>>import re
>>> url = ’ http:/ /example . web s craping . c。m/de fault /view/
Australia- 1’
>>> re . sub ( ’ [ 《 /0-9a-zA- Z\-]’ ,'_' , url )
'http_//example . webs c raping . com/default /view/Australia- 1 ’
3.2.1 实现
介绍一下了创建基于磁盘的缓存时需要考虑的文件系统限制 ,包括允许使用哪些字符、文件名长度限制 ,以及确保文件和目录的创建位置不同。把URL到文件名的这些映射逻辑结合起来 ,就形成了磁盘缓存的主要部分。
![](https://i-blog.csdnimg.cn/blog_migrate/04d308502c373eb36aa77390d5bf3967.png)
在上面的代码中 ,构造方法传入了 一个用于设定缓存位置的参数 ,然后在urltopath方法中应用了前面讨论的文件名限制 。现在 ,我们还缺少根据文件名存取数据的方法 ,下面的代码实现了这两个缺失的方法 。
在一setitern ( )一中,我们使用 u r l _to _pa th ( ) 方法将 URL 映射为 安全文件名 ,在必要情况下还需要创建父目录 。这里使用的pickle模块会把输入转化为字符串 ,然后保存到磁盘中 。而在_getitem_ ()方法中 ,首先将URL映射为安全文件名 。然后 ,如果文件存在 ,则加载其内容,并执行反序列化,恢复其原始数据类型 :如果文件不存在 ,则说明缓存中还没有该URL的 数据 ,此时会抛出KeyError异常。
3.2.2缓存测试
现在 ,我们通过向爬虫传递cache回调 ,来检验DiskCache类 。该类的完整源代码可以从https : / /bitbucket .org/wswp/code/src/tip/chapter03/diskcache.py获取 。我们可以通过执行如下脚本,使用链接爬虫测试磁盘缓存 。
![](https://i-blog.csdnimg.cn/blog_migrate/7b02b7834ea65c679a8218e7a8bbf2f1.png)
第一次执行该命令时,由于缓存为空,因此网页会被正常下载。但当我们 第二次执行该脚本时,网页加载自缓存中,爬虫应该更快完成执行,其执行 结果如下所示 。
和上面的预期一样,爬取操作很快就完成了。当缓存为空时,我的计算机中的爬虫下载耗时超过23 分钟1 而在第二次全部使用缓存时,该耗时只有0.186秒( 比第一次爬取快了超过7000倍。由于硬件的差异 ,在不同的计算机中的准确执行时间也会有所区别 。不过毋庸置疑的是,磁盘缓存速度更快。
3.2.3节省磁盘空间
为最小化缓存所需的磁盘空间,我们可以对下载得到的HTML文件进行压缩处理。处理的实现方法很简单,只需在保存到磁盘之前使用zlib压缩序列化字符串即可,如下面的代码所示 。
![](https://i-blog.csdnimg.cn/blog_migrate/75a94c5776c5e9f4146ada50a7932d6e.png)
而从磁盘加载后解压的代码如下所示 。
returnpickle.loads(zlib.decompress(fp.read()))
压缩完所有网页之后,缓存大小从 4.4MB下降到2.3MB,而在我的计算机上爬取缓存示例网站的时间是0.212秒,和未压缩时的0.186秒相比只是略有增加。当然,如果你的项目对速度十分敏感的话 ,也可以禁用压缩功能。
3.2.4 清理过期数据
当前版本的磁盘缓存使用键值对的形式在磁盘上保存缓存,未来无论何时 请求都会返回结果。对于缓存网页而言该功能可能不太理想,因为网页内 容随时都有可能发生变化,存储在缓存中的数据存在过期风险。本节中,我 们将为缓存数据添加过期时间,以便爬虫知道何时需要重新下载网页。在缓 存网页时支持存储时间戳的功能也很简单, 如下面的代码所示 。
![](https://i-blog.csdnimg.cn/blog_migrate/df4f0b6bb451a446a6c20b2fe051140d.png)
在构造方法中,我们使用timedelta对象将默认过期时间设置为30天。然后,在_set一方法中,把当前时间戳保存到序列化数据中:而在_get一 方法中,对比当前时间和缓存时间,检查是否过期 为了测试过期时间功能,我们可以将其缩短为5秒,如下所示 。
和预期一样,缓存结果最初是可用的经过5秒的睡眠之后,再次调用同-URL,则会抛出KeyError异常,也就是说缓存下载失效了
3.2.5缺点
基于磁盘的缓存系统比较容易实现,无须安装其他模块,并且在文件管理 器中就能查看结果。但是,该方法存在一个缺点即受制于本地文件系统 的 限制 。本章早些时候,为了将URL映射为安全文件名,我们应用了多种限制,然而这又会引发另一个问题,那就是一些URL会被映射为相同的文件名 。比如,在对如下几个URL进行字符替换之后就会得到相同的文件名 。
![](https://i-blog.csdnimg.cn/blog_migrate/36148f3ad30d24426d6f5bedc5cbc205.png)
这就意味着,如果其中一个URL生成了缓存,其他3个URL也会被认为 已经生成缓存,因为它们映射到了同一个文件名。另外,如果一些长URL只 在255个字符之后存在区别,截断后的版本也会被映射为相同的文件名。这 个问题非常重要,因为U虹的最大长度并没有明确限制尽管在实践中U肚 很少会超过2000个字符,且早期版本的IE浏览器也不支持超过2083个字 符的U虹。避免这些限制 的 一种解决方案是使用URL的哈希值作为文件名尽管该方法可以带来一定改善,但是最终还是会面临许多文件系统具有的一个关键问题,那就是每个卷和每个目录下的文件数量是有限制的。如 果缓存存储在FAT32 文件系统中,每个目录的最大文件数是65535。该限制可以通过将缓存分割到不同目录来避免,但是文件系统可存储的文件总数也是有限制的。我使用的ext4分区目前支持略多于1500万个文件,而一个大型网站往往拥有超过1亿个网页。很遗憾 ,DiskCache方法想要通用的话存在太多限制。要想避免这些问题,我们需要把多个缓存网页合并到一个文件中,并使用类似B+树的算法进行索引。我们并不会自己实现这种算法,而是在下一节中介绍己实现这类算法的数据库 。
3.3 数据库缓存
为了避免磁盘缓存方案的己知限制,下面我们会在现有数据库系统之上创建缓存。爬取时,我们可能需要缓存大量数据,但又无须任何复杂的连接操作,因此我们将选用NoSQL数据库,这种数据库比传统的关系型数据库更易于扩展。在本节中,我们将会选用目前非常流行的MongoDB作为缓存数据库 。
3.3.1 NoSQL 是什么
NoSQL全称为Not
OnlySQL,是一种相对较新的数据库设计方式。传统的关系模型使用的是固定模式,并将数据分割到各个表中。然而,对于大数据集的情况,数据量太大使其难以存放在单一服务器中,此时就需要扩展到多台服务器不过,关系模型对于这种扩展的支持并不够好,因为在查询多 个表时,数据可能在不同的服务器中。相反,NoSQL数据库通常是无模式的,从设计之初就考虑 了跨服务器无缝分片的问题。在NoSQL中,有多种方式可以实现该目标,分别是列数据存储( 如 HBase )、键值对存储( 如 Redis )、面向文档的数据库(如MongoDB)以及图形数库( 如 Neo4j )。
3.3.2 安装 MangoDB
MongoDB可以从https://www.mongodb.org/downloads下载得到。然后,我们需要使用如下命令外安装其Python封装库。
![](https://i-blog.csdnimg.cn/blog_migrate/b31eb8f24c443c83ecb8e45ef1572993.png)
3.3.3 MongoDB 概述
下面是通过 MongoDB 存取数据 的 示例代码 。
上面的例子存在一个问题, 那就是如果我们对相 同 的 URL 插入另一条不 同 的文档时, MongoDB 会欣然接受并执行这次插入操作, 其执行过程如下所示。
此时,同一URL下出现了多条记录,但我们只关心最新存储的那条数据。为了避免重复,我们将 ID设置为URL,并执行upsert操作。该操作表示当记录存在时更新记录,否则插入新记录,其代码如下所示 。
现在,当我们尝试向同一URL插入记录时,将会更新其内容,而不是创建冗余的数据,如下面的代码所示。
可以看出,在添加了这条记录之后,虽然HTML的内容更新了,但该URL的记录数仍然是 1 。
3.3.4 MongoDB 缓存实现
现在我们己经准备好创建基于MongoDB的缓存了,这里使用了和之前的DiskCache类相同的类接 口。
![](https://i-blog.csdnimg.cn/blog_migrate/ec08bd38b617bf71d707e42ba4f1a6fc.png)
3.3.5 压缩
为了使数据库缓存与之前的磁盘缓存功能一致,我们最后还要添加一个功能:压缩。其实现方法和磁盘缓存相类似,即序列化数据后使用 zlib 库进行压缩,如下面的代码所示 。
![](https://i-blog.csdnimg.cn/blog_migrate/ccc68accfc2b72c29019f774c278ab61.png)
3.3.6 缓存测试
MongoCache类的源码可以从https://bitbucket.org/wswp/code/src/tip/chapter03/mongocache.py 获取,和DiskCache一样,这里我们依然通过执行该脚本测试链接爬虫 。
可以看出,加载数据库缓存的时间几乎是加载磁盘缓存的两倍。不过,MongoDB可以让我们免受文件系统的各种限制,还能在下一章介绍的并发爬虫处理中更加高效 。
3.4 本章小结
本章中,我们了解到缓存己下载的网页可以节省时间,并能最小化重新爬取网站所耗费的带宽 。 缓存的主要缺点是会占用磁盘空间,不过我们可以使用压缩的方式减少空间占用。此外,在类似 MongoDB 等现有数据库的基础之上创建缓存,可以避免文件系统的各种限制 。