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

来源:互联网 发布:揭阳有没有学淘宝培训 编辑:程序博客网 时间:2024/06/05 16:08

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

磁盘缓存

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

操作系统 文件系统 非法文件名字符 文件名最大长度 Linux Ext3/Ext4 / 和 \0 255字节 OS X HFS Plus(已经更新为AFS) : 和 \0 255个UTF-16编码单元 Windows NTFS \、/、:、*、”、>、<和| 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.pyimport osimport reimport urllib.parseimport shutilimport zlibimport hashlibfrom datetime import datetime, timedeltatry:    import cPickle as pickleexcept ImportError:    import pickleclass 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会被映射为相同的文件名。比如:

  • http://example.com/?a+b
  • http://example.com/?a*b
  • http://example.com/?a=b

    避免这种缺陷的方式是通过使用url的哈希值作为文件名。

    1. 但是文件系统中每个卷和每个目录下的文件数量是有限,如果下载的文件过多,则会出现存储错误。

更优的方式:数据库缓存

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

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

pip install pymonogo

MongoDB教程可以看这里mongoDB 入门。

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

try:    import cPickle as pickleexcept ImportError:    import pickleimport zlibfrom datetime import datetime, timedeltafrom pymongo import MongoClientfrom bson.binary import Binaryclass 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()

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

原创粉丝点击