3.3 数据库缓存
为了避免磁盘缓存方案的已知限制,下面我们会在现有数据库系统之上创建缓存。爬取时,我们可能需要缓存大量数据,但又无须任何复杂的连接操作,因此我们将选用NoSQL数据库,这种数据库比传统的关系型数据库更易于扩展。在本节中,我们将会选用目前非常流行的MongoDB作为缓存数据库。
3.3.1 NoSQL是什么
NoSQL 全称为Not Only SQL ,是一种相对较新的数据库设计方式。传统的关系模型使用的是固定模式,并将数据分割到各个表中。然而,对于大数据集的情况,数据量太大使其难以存放在单一服务器中,此时就需要扩展到多台服务器。不过,关系模型对于这种扩展的支持并不够好,因为在查询多个表时,数据可能在不同的服务器中。相反,NoSQL数据库通常是无模式的,从设计之初就考虑了跨服务器无缝分片的问题。在NoSQL中,有多种方式可以实现该目标,分别是列数据存储(如HBase)、键值对存储(如Redis)、面向文档的数据库(如MongoDB)以及图形数据库(如Neo4j)。
3.3.2 安装MongoDB
MongoDB可以从https://www.mongodb.org/downloads 下载得到。然后,我们需要使用如下命令额外安装其Python封装库。
pip install pymongo
要想检测安装是否成功,可以使用如下命令在本地启动MongoDB。
$ mongod -dbpath .
然后,在Python中,使用MongoDB的默认端口尝试连接MongoDB。
>>> from pymongo import MongoClient >>> client = MongoClient('localhost', 27017)
3.3.3 MongoDB概述
下面是通过MongoDB存取数据的示例代码。
>>> url = 'http://example.webscraping.com/view/United-Kingdom-239' >>> html = '...' >>> db = client.cache >>> db.webpage.insert({'url': url, 'html': html}) ObjectId('5518c0644e0c87444c12a577') >>> db.webpage.find_one(url=url) {u'_id': ObjectId('5518c0644e0c87444c12a577'), u'html': u'...', u'url': u'http://example.webscraping.com/view/United-Kingdom-239'}
上面的例子存在一个问题,那就是如果我们对相同的URL插入另一条不同的文档时,MongoDB会欣然接受并执行这次插入操作,其执行过程如下所示。
>>> db.webpage.insert({'url': url, 'html': html}) >>> db.webpage.find(url=url).count() 2
此时,同一URL下出现了多条记录,但我们只关心最新存储的那条数据。为了避免重复,我们将ID设置为URL,并执行upsert 操作。该操作表示当记录存在时更新记录,否则插入新记录,其代码如下所示。
>>> db.webpage.update({'_id': url}, {'$set': {'html': html}}, upsert=True) >>> db.webpage.find_one({'_id': url}) {u'_id': u'http://example.webscraping.com/view/ United-Kingdom-239', u'html': u'...'}
现在,当我们尝试向同一URL插入记录时,将会更新其内容,而不是创建冗余的数据,如下面的代码所示。
>>> new_html = '<html></html>' >>> db.webpage.update({'_id': url}, {'$set': {'html': new_ html}}, upsert=True) >>> db.webpage.find_one({'_id': url}) {u'_id': u'http://example.webscraping.com/view/United-Kingdom-239', u'html': u'<html></html>'} >>> db.webpage.find({'_id': url}).count() 1
可以看出,在添加了这条记录之后,虽然HTML的内容更新了,但该URL的记录数仍然是1。
![]() |
MongoDB官方文档可参考`http://docs.mongodb.org/manual/`,在该文档中可以找到上述功能及一些其他功能更详细的介绍。
3.3.4 MongoDB缓存实现
现在我们已经准备好创建基于MongoDB的缓存了,这里使用了和之前的DiskCache 类相同的类接口。
from datetime import datetime, timedelta from pymongo import MongoClient class MongoCache: def __init__(self, client=None, expires=timedelta(days=30)): # if a client object is not passed then try # connecting to mongodb at the default localhost port self.client = MongoClient('localhost', 27017) if client is None else client # create collection to store cached webpages, # which is the equivalent of a table # in a relational database self.db = client.cache # create index to expire cached webpages self.db.webpage.create_index('timestamp', expireAfterSeconds=expires.total_seconds()) def __getitem__(self, url): """Load value at this URL """ record = self.db.webpage.find_one({'_id': url}) if record: return record['result'] else: raise KeyError(url + ' does not exist') def __setitem__(self, url, result): """Save value for this URL """ record = {'result': result, 'timestamp': datetime.utcnow()} self.db.webpage.update({'_id': url}, {'$set': record}, upsert=True)
在上一节讨论如何避免冗余时,你已经见过这里的__getitem__ 和__setitem__ 方法的实现了。此外,我们在构造方法中创建了timestamp 索引。在达到给定时间戳一定秒数之后,MongoDB的这一便捷功能可以自动删除记录。这样我们就无须再像DiskCache 类那样,手工检查记录是否仍然有效了。下面我们使用空的timedelta 对象进行测试,此时记录在创建后就会被立即删除。
>>> cache = MongoCache(expires=timedelta()) >>> result = {'html': '…'} >>> cache[url] = result >>> cache[url] {'html': '…'}
记录还在这里,看起来好像我们的缓存过期机制没能正常运行。但实际上这是MongoDB的运行机制造成的。MongoDB运行了一个后台任务,每分钟检查一次过期记录,所以此时该记录还没有被删除。让我们再等1分钟,就会发现缓存过期机制已经运行成功了。
>>> import time; time.sleep(60) >>> cache[url] Traceback (most recent call last): ... KeyError: 'http://example.webscraping.com/view/United-Kingdom-239 does not exist'
这种机制下,MongoDB缓存无法按照给定时间精确清理过期记录,会存在至多1分钟的延时。不过,由于缓存过期时间通常设定为几周或是几个月,所以这个相对较小的延时不会存在太大问题。
3.3.5 压缩
为了使数据库缓存与之前的磁盘缓存功能一致,我们最后还要添加一个功能:压缩 。其实现方法和磁盘缓存相类似,即序列化数据后使用zlib 库进行压缩,如下面的代码所示。
import pickle import zlib from bson.binary import Binary class MongoCache: def __getitem__(self, url): record = self.db.webpage.find_one({'_id': url}) if record: return pickle.loads(zlib.decompress(record['result'])) else: raise KeyError(url + ' does not exist') def __setitem__(self, url, result): record = { 'result': Binary(zlib.compress(pickle.dumps(result))), 'timestamp': datetime.utcnow() } self.db.webpage.update( {'_id': url}, {'$set': record}, upsert=True)
3.3.6 缓存测试
MongoCache 类的源码可以从https://bitbucket.org/wswp/code/ src/tip/chapter03/mongo_cache.py 获取,和DiskCache 一样,这里我们依然通过执行该脚本测试链接爬虫。
$ time python mongo_cache.py http://example.webscraping.com http://example.webscraping.com/view/Afghanistan-1 ... http://example.webscraping.com/view/Zimbabwe-252 23m40.302s $ time python mongo_cache.py 0.378s
可以看出,加载数据库缓存的时间几乎是加载磁盘缓存的两倍。不过,MongoDB可以让我们免受文件系统的各种限制,还能在下一章介绍的并发爬虫处理中更加高效。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论