WSWP(用 python写爬虫) 笔记四:实现缓存功能

前面已经重构好了链接获取、数据获取模块,现在开始实现具体的缓存功能。

磁盘缓存

缓存下载结果,先来尝试最容易想到的方案,将下载到的网页内容存储到文件系统中。为了实现该功能,需要将url安全的映射为跨平台的文件名。三大主流文件系统的文件名限制如下:

操作系统文件系统非法文件名字符文件名最大长度
LinuxExt3/Ext4/ 和 \0255字节
OS XHFS Plus(已经更新为AFS): 和 \0255个UTF-16编码单元
WindowsNTFS\、/、:、*、”、>、<和|255个字符

为了保证在不同的操作系统中,文件路径都是合法的,需要限制其只能包含数字、字母和基本符号,其他的非法字符替换为下划线,完整实现代码如下:

# diskCache.py

    def urlToPath(self, url):
        """
        Create file system path for this url
        :param url:
        :return:
        """
        components = urllib.parse.urlparse(url)

        # when empty path set to /index.html
        path = components.path
        if not path:
            path = '/index.html'
        elif path.endswith('/'):
            path += '/index.html'

        filename = components.netloc + path + components.query

        # Replace invalid characters
        filename = re.sub('[^/0-9A-Za-z\-.,;_]','_', filename)

        # restrict maxinum number of characters
        filename = '/'.join(segment[:255] for segment in filename.split('/'))

        return os.path.join(self.cacheDir, filename)

上面的实现代码中,考虑了一种边界情况,url可能会以斜(’/’)结尾,此时通过urlib.parse.urlparse解析后, path为 ‘/’,属于非法字符串,解决的办法是添加index.html作为其文件名。同样地,当path为空时也进行相同的操作。同时为了节省磁盘空间,先使用pickle对数据进行序列化,然后在存储数据之前先对数据使用zlib进行压缩处理。从此盘中加载时反过来先解压再通过反序列化读取即可。完整代码如下:

# diskCache.py

import os
import re
import urllib.parse
import shutil
import zlib
import hashlib
from datetime import datetime, timedelta

try:
    import cPickle as pickle
except ImportError:
    import pickle

class DiskCache:

    def __init__(self, cacheDir='cache', expires=timedelta(days=30),compress=True):
        """
        :param cacheDir: 缓存存放目录
        :param expires: 缓存过期时间
        :param compress: 是否压缩
        """
        self.cacheDir = cacheDir
        self.expires = expires
        self.compress = compress

    def __getitem__(self,url):
        """
        Load data from disk for this URL
        :param url:
        :return:
        """
        path = self.urlToPath(url)
        if os.path.exists(path):
            with open(path, 'rb') as fp:
                data = fp.read()
                if self.compress:
                    data = zlib.decompress(data)
                result, timestamp = pickle.loads(data)
                if self.hasExpired(timestamp):
                    raise KeyError(url + ' has expired')
                return result
        else:
            # url has not yet been cached
            raise KeyError(url + ' does not exist')

    def __setitem__(self, url, result):
        """
        Save data to disk for this url
        :param url:
        :param result:
        :return:
        """
        path = self.urlToPath(url)
        folder = os.path.dirname(path)
        if not os.path.exists(folder):
            os.makedirs(folder)

        data = pickle.dumps((result, datetime.utcnow()))
        if self.compress:
            data = zlib.compress(data)
        with open(path, 'wb') as fp:
            fp.write(data)

    def __delitem__(self, url):
        """
        Remove the value at this key and any empty parent sub-directories
        :param url:
        :return:
        """
        path = self._keyPath(url)
        try:
            os.remove(path)
            os.removedirs(os.path.dirname(path))
        except OSError:
            pass

    def urlToPath(self, url):
        """
        Create file system path for this url
        :param url:
        :return:
        """
        components = urllib.parse.urlparse(url)

        # when empty path set to /index.html
        path = components.path
        if not path:
            path = '/index.html'
        elif path.endswith('/'):
            path += 'index.html'

        filename = components.netloc + path + components.query

        # Replace invalid characters
        filename = re.sub('[^/0-9A-Za-z\-.,;_]','_', filename)

        # restrict maxinum number of characters
        filename = '/'.join(segment[:255] for segment in filename.split('/'))

        return os.path.join(self.cacheDir, filename)

    def hasExpired(self, timestamp):
        """
        Return whether this timestamp has expired
        :param timestamp:
        :return:
        """
        return datetime.utcnow() > timestamp + self.expires

    def clear(self):
        """
        Remove all the cached values
        :return:
        """
        if os.path.exists(self.cacheDir):
            shutil.rmtree(self.cacheDir)

上面的代码中,不仅为数据添加了过期时间,还实现清理全部数据或者是根据url来清理特定的数据的功能。

缺点
1. 存在一些url会被映射为相同的文件名。比如:

更优的方式:数据库缓存

为了避免磁盘缓存方案的已知限制,接下来将会在现有的数据库系统上创建缓存。爬取时可能需要缓存大量数据,但又不需要任何复杂的连接操作,因此将选用NoSQL数据库,这种数据库比传统的关系型数据库更易于扩展。在此我选用MongoDB作为缓存数据库。

先从官网https://www.mongodb.org/downloads下载安装mongodb客户端,然后通过pip下载额外的Python封装库。

pip install pymonogo

MongoDB教程可以看这里mongoDB 入门

为了避免在对相同的url插入时出现多条记录,将ID设置为url,并执行upsert操作,当记录存在时更新记录,否则插入新纪录。同时也需要对数据进行压缩处理以减少数据大小。
实现代码如下:

try:
    import cPickle as pickle
except ImportError:
    import pickle
import zlib
from datetime import datetime, timedelta
from pymongo import MongoClient
from bson.binary import Binary

class MongoCache:

    def __init__(self, client=None, expires=timedelta(days=30)):
        self.client = MongoClient('localhost', 27017) if client is None else client
        self.db = self.client.cache
        self.db.webpage.create_index('timestamp', expireAfterSeconds=expires.total_seconds())

    def __contains__(self, url):
        try:
            self[url]
        except KeyError:
            return False
        else:
            return True

    def __getitem__(self, url):
        """
        Load value at this url
        :param url:
        :return:
        """
        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):
        """
        Save value for this url
        :param url:
        :param result:
        :return:
        """
        record = {'result':Binary(zlib.compress(pickle.dumps(result))), 'timestamp': datetime.utcnow()}
        self.db.webpage.update({'_id':url}, {'$set':record}, upsert=True)

    def clear(self):
        self.db.webpage.drop()

到此文件数据缓存的功能已经实现了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值