返回介绍

3.3 数据库缓存

发布于 2024-02-05 23:37:18 字数 5953 浏览 0 评论 0 收藏 0

为了避免磁盘缓存方案的已知限制,下面我们会在现有数据库系统之上创建缓存。爬取时,我们可能需要缓存大量数据,但又无须任何复杂的连接操作,因此我们将选用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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文