目录
3.1 为链接爬虫添加缓存支持
3.2 磁盘缓存
3.2.1 实现
3.2.2缓存测试
3.2.3节省磁盘空间
3.2.4 清理过期数据
3.2.5缺点
3.3 数据库缓存
3.3.1 NoSQL 是什么
3.3.2 安装 MangoDB
3.3.3 MongoDB 概述
3.3.4 MongoDB 缓存实现
3.3.5 压缩
3.3.6 缓存测试
3.4 本章小结
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所示为几大主流文件系统的 限制 。
为了保证在不同文件系统中 ,我们的文件路径都是安全的 ,就需要限制其只能包含数字 、字母和基本符号 ,并将其他字符替换为下划线 ,其实现代码
>>>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到文件名的这些映射逻辑结合起来 ,就形成了磁盘缓存的主要部分。
在上面的代码中 ,构造方法传入了 一个用于设定缓存位置的参数 ,然后在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获取 。我们可以通过执行如下脚本,使用链接爬虫测试磁盘缓存 。
第一次执行该命令时,由于缓存为空,因此网页会被正常下载。但当我们 第二次执行该脚本时,网页加载自缓存中,爬虫应该更快完成执行,其执行 结果如下所示 。
和上面的预期一样,爬取操作很快就完成了。当缓存为空时,我的计算机中的爬虫下载耗时超过23 分钟1 而在第二次全部使用缓存时,该耗时只有0.186秒( 比第一次爬取快了超过7000倍。由于硬件的差异 ,在不同的计算机中的准确执行时间也会有所区别 。不过毋庸置疑的是,磁盘缓存速度更快。
3.2.3节省磁盘空间
为最小化缓存所需的磁盘空间,我们可以对下载得到的HTML文件进行压缩处理。处理的实现方法很简单,只需在保存到磁盘之前使用zlib压缩序列化字符串即可,如下面的代码所示 。
而从磁盘加载后解压的代码如下所示 。
returnpickle.loads(zlib.decompress(fp.read()))
压缩完所有网页之后,缓存大小从 4.4MB下降到2.3MB,而在我的计算机上爬取缓存示例网站的时间是0.212秒,和未压缩时的0.186秒相比只是略有增加。当然,如果你的项目对速度十分敏感的话 ,也可以禁用压缩功能。
3.2.4 清理过期数据
当前版本的磁盘缓存使用键值对的形式在磁盘上保存缓存,未来无论何时 请求都会返回结果。对于缓存网页而言该功能可能不太理想,因为网页内 容随时都有可能发生变化,存储在缓存中的数据存在过期风险。本节中,我 们将为缓存数据添加过期时间,以便爬虫知道何时需要重新下载网页。在缓 存网页时支持存储时间戳的功能也很简单, 如下面的代码所示 。
在构造方法中,我们使用timedelta对象将默认过期时间设置为30天。然后,在_set一方法中,把当前时间戳保存到序列化数据中:而在_get一 方法中,对比当前时间和缓存时间,检查是否过期 为了测试过期时间功能,我们可以将其缩短为5秒,如下所示 。
和预期一样,缓存结果最初是可用的经过5秒的睡眠之后,再次调用同-URL,则会抛出KeyError异常,也就是说缓存下载失效了
3.2.5缺点
基于磁盘的缓存系统比较容易实现,无须安装其他模块,并且在文件管理 器中就能查看结果。但是,该方法存在一个缺点即受制于本地文件系统 的 限制 。本章早些时候,为了将URL映射为安全文件名,我们应用了多种限制,然而这又会引发另一个问题,那就是一些URL会被映射为相同的文件名 。比如,在对如下几个URL进行字符替换之后就会得到相同的文件名 。
这就意味着,如果其中一个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封装库。
3.3.3 MongoDB 概述
下面是通过 MongoDB 存取数据 的 示例代码 。
上面的例子存在一个问题, 那就是如果我们对相 同 的 URL 插入另一条不 同 的文档时, MongoDB 会欣然接受并执行这次插入操作, 其执行过程如下所示。
此时,同一URL下出现了多条记录,但我们只关心最新存储的那条数据。为了避免重复,我们将 ID设置为URL,并执行upsert操作。该操作表示当记录存在时更新记录,否则插入新记录,其代码如下所示 。
现在,当我们尝试向同一URL插入记录时,将会更新其内容,而不是创建冗余的数据,如下面的代码所示。
可以看出,在添加了这条记录之后,虽然HTML的内容更新了,但该URL的记录数仍然是 1 。
3.3.4 MongoDB 缓存实现
现在我们己经准备好创建基于MongoDB的缓存了,这里使用了和之前的DiskCache类相同的类接 口。
3.3.5 压缩
为了使数据库缓存与之前的磁盘缓存功能一致,我们最后还要添加一个功能:压缩。其实现方法和磁盘缓存相类似,即序列化数据后使用 zlib 库进行压缩,如下面的代码所示 。
3.3.6 缓存测试
MongoCache类的源码可以从https://bitbucket.org/wswp/code/src/tip/chapter03/mongocache.py 获取,和DiskCache一样,这里我们依然通过执行该脚本测试链接爬虫 。
可以看出,加载数据库缓存的时间几乎是加载磁盘缓存的两倍。不过,MongoDB可以让我们免受文件系统的各种限制,还能在下一章介绍的并发爬虫处理中更加高效 。
3.4 本章小结
本章中,我们了解到缓存己下载的网页可以节省时间,并能最小化重新爬取网站所耗费的带宽 。 缓存的主要缺点是会占用磁盘空间,不过我们可以使用压缩的方式减少空间占用。此外,在类似 MongoDB 等现有数据库的基础之上创建缓存,可以避免文件系统的各种限制 。