Python爬虫读书笔记——下载缓存(6)

磁盘缓存

几大主流文件系统的限制

操作系统 文件系统 非法文件名字符 文件名最大长度
Linux Ext3/Ext4 / and \0

255字节

OS X HFS Plus : and \0 255个UTF-16编码单元
Windows NTFS \、/、?、:、*、"、>、< and | 255个字符

为了保证在不同的文件系统中,我们的文件路径都是安全的,就需要限制其只能包含数字、字母和基本符号,并将其他字符替换为下划线。代码如下:

>>>import re

>>>url='http://...'

>>>re.sub('[^/0-9a-zA-Z\-.,;_]','_'.url)

'http_//...'

此外,文件名及其父目录的长度需要限制在255个字符内,代码如下:

>>>filename='/'.join(segment[:255] for segment in filename.split('/'))

还有一种边界情况需要考虑,那就是URL路径可能会以斜杠 (/) 结尾,此时斜杠后面的空字符串就会成为一个非法的文件名。但是,如果移除这个 斜杠,使用其父字符串作为文件名,又会造成无法保存其他URL 的问题。考虑下面这两个URL:

http://example.webscraping.com/index/

http://example.webscraping. com/index/1

如果我们希望这两个URL都能保存下来,就需要以 index 作为目录名,以1作为子路径。 对于像第一个URL 路径这样以斜杠结尾的情况, 这里使用 的解决方案是添加index.html 作为其文件名。同样地,当URL路径为空时也进行相同的操作。为了解析URL, 我们需要使用 urlparse.urlsplit () 函数,将URL分割成几个部分。

>>>import urlparse

>>>components=urlparse.urlsplit('http://example.webscraping.com/index/')

>>>print components

SplitResult(scheme='http',netloc='example.webscraping.com',path='/index/',query='',fragment='')

>>>print components.path

'/index'

该函数提供了解析和处理URL 的便捷接口。下面是使用该模块对上述边界情况添加 index.html 的示例代码。 

>>>path=components.path

>>>if not path:

>>>path='/index.html'

>>>elif path.endwith('/'):

>>>path+='index.html'

>>>filename=components.netloc+path+components.query

>>>filename

'example.webscraping.com/index/index.html')

实现

把URL到文件名的这些映射逻辑结合起来,就形成了磁盘缓存的主要部分。下面是DiskCache类的初始实现代码。

import os
import re
import urlparse

class DiskCache:
    def __init__(self,cache_dir='cache'):
        self.cache_dir=cache_dir
        self.max_length=max_length
    def url_to_path(self,url):
        #给这个URL创造文件路径
        components=urlparse.urlsplit(url)
        #增加index.html给这个空路径
        path=components.path
        if not path:
            path='/index.html'
        elif path.endwith('/')
            path+='index.html'
        filename=components.netloc+path+components.query
        #替换非法字符
        filename=re.sub('[^/0-9a-zA-Z\-.,;_]','_',filename)
        #限制最大字符数
        filename='/'.join(segment[:255] for segment in filename.split('/'))
        return os.path.join(self.cache_dir,filename)

    def __getitem__(self,url):
        #从磁盘中下载数据在这个链接中
        path=self.url_to_path(url)
        if os.path.exists(path)
            with open(path,'rb') as fp:
                return pickle.load(fp)
        else:
            #链接没有被存储
            raise KeyError(url+'does not exist')
    def __setitem__(self,url,result):
        #在这个链接中保存数据到磁盘中
        path=self.url_to_path(url)
        folder=os.path.dirname(path)
        if not os.path.exists(folder):
            os.makedirs(folder)
        with open(path,'wb') as fp:
            fp.write(pickle.dumps(result))

在上面的代码中,构造方法传入了一个用于设定缓存位置的参数,然后在url_to_path方法中应用了文件名的限制。

在__setitem__()中,我们使用url_to_path()方法将URL映射为安全文件名,在必要的情况下还需要创建父目录。这里使用的 pickle 模块会把输入转化为字符串,然后保存到磁盘中。 而在__getitem__()方法中,首先将URL 映射为安全文件名。然后,如果文件存在,则加载其内容,并执行反序列化, 恢复其原始数据类型: 如果文件不存在,则说明缓存中还没有该 URL 的数据,此时会抛出 KeyError异常。

缓存测试

通过爬虫传递cache回调,来检验DiskCache类。

完整代码需要上外网,所以暂时贴不上来。

节省磁盘空间

为了最小化缓存所需的磁盘空间,我们可以对下载得到的HTML文件进行压缩处理。处理方法,只需保存到磁盘之前使用zlib压缩序列化字符串即可:

fp.write(zlib.compress(pickle.dumps(read)))

压缩磁盘空间,会略微增加项目速度。

清理过期数据

当前版本的磁盘缓存使用键值对的形式在磁盘上保存缓存,未来无论何时请求都会返回结果。对于缓存网页而言, 该功能可能不太理想, 因为网页内容随时都有可能发生变化, 存储在缓存中的数据存在过期风险。 接下来,我们将为缓存数据添加过期时间,以便爬虫知道何时需要重新下载网页。 

import os
import re
import urlparse
from datetime import datetime,timedelta

class DiskCache:
    def __init__(self,cache_dir='cache',expires=timedelta(days=30)):
        self.cache_dir=cache_dir
        self.max_length=max_length
        self.expires=expires
    def url_to_path(self,url):
        #给这个URL创造文件路径
        components=urlparse.urlsplit(url)
        #增加index.html给这个空路径
        path=components.path
        if not path:
            path='/index.html'
        elif path.endwith('/')
            path+='index.html'
        filename=components.netloc+path+components.query
        #替换非法字符
        filename=re.sub('[^/0-9a-zA-Z\-.,;_]','_',filename)
        #限制最大字符数
        filename='/'.join(segment[:255] for segment in filename.split('/'))
        return os.path.join(self.cache_dir,filename)

    def __getitem__(self,url):
        #从磁盘中下载数据在这个链接中
        path=self.url_to_path(url)
        if os.path.exists(path)
            with open(path,'rb') as fp:
                result,timestamp=pickle.loads(zlib.decompress(fp.read()))
                if self.has_expired(timestamp):
                    raise KeyError(url+'has expired')
                return result
        else:
            #链接没有被存储
            raise KeyError(url+'does not exist')
    def __setitem__(self,url,result):
        #在这个链接中保存数据到磁盘中
        path=self.url_to_path(url)
        folder=os.path.dirname(path)
        if not os.path.exists(folder):
            os.makedirs(folder)
        timestamp=datetime.utcnow()
        data=pickle.dumps((result,timestamp))
        with open(path,'wb') as fp:
            fp.write(zlib.compress(data))

    def has_expired(self,timestamp):
        #返回这个时间戳是否过期
        return datetime.utcnow()>timemap+self.expires

在构造方法中,我们使用 timedelta对象将默认过期时间设置为30天。 然后,在__set__一方法中,把当前时间戳保存到序列化数据中:而在__get__方法中,对比当前时间和缓存时间,检查是否过期。 为了测试过期时间功能,我们可以将其缩短为5秒 

Python爬虫读书笔记——下载缓存(6)_第1张图片

缓存结果最初是可用的,经过5s的睡眠之后,再次调用同一URL。则会抛出KeyError异常,也就是缓存下载失效了。

缺点

磁盘的缓存系统比较容易实现,无需安装其他模块,并且在文件管理器中就能查看结果。

但是它受制于本地文件系统的限制。比如,为了将URL映射为安全文件名,应用了多种限制,但是一些URL会被映射成为相同的文件名。在对下面几个URL进行字符替换之后,就会得到相同的文件名。

http://example.com/?a+b

http://example.com/?a*b

http://example.com/?a=b

http://example.com/?a!b

这就意味着,如果其中一个 URL 生成了缓存, 其他3个URL也会被认为已经生成缓存, 因为它们映射到了同一个文件名。 另外, 如果一些长URL 只 在255 个字符之后存在区别,截断后的版本也会被映射为相同的文件名。 这个问题非常重要, 因为URL的最大长度并没有明确限制。 尽管在实践中URL很少会超过2000个字符, 并且早期版本的 IE浏览器也不支持超过2083 个字符的URL。

避免这些限制的一种解决方案是使用 URL 的哈希值作为文件名。尽管该方法可以带来一定改善,但是最终还是会面临许多文件系统具有的一个关键问题, 那就是每个卷和每个目录下的文件数量是有限制的。 如果缓存存储在 FAT32文件系统中,每个目录的最大文件数是65535。 该限制可以通过将缓存分割到不同目录来避免, 但是文件系统可存储的文件总数也是有限制的。 我使用的 ext4 分区目前支持略多于1500万个文件, 而一个大型网站往往拥有超过1 亿个网页。 很遗憾, DiskCache 方法想要通用的话存在太多限制。要想避免这些问题, 我们需要把多个缓存网页合并到一个文件中, 并使用类似 B+树的算法进行索引。我们并不会自己实现这种算法的数据库。

数据库缓存

为了避免磁盘缓存方案的己知限制,下面我们会在现有数据库系统之上创建缓存。 爬取时, 我们可能需要缓存大量数据,但又无须任何复杂的连接操作, 因此我们将选用 NoSQL数据库, 这种数据库比传统的关系型数据库更易于扩展。在本节中, 我们将会选用目前非常流行的 MongoDB 作为缓存数据库。(真的是对这个数据库一点儿都不了解,咨询了同事,他说这个数据很简单!所以打算简单的了解一下,不过,数据库缓存可以用到其他的数据库吗?菜鸟教程里关于python有关于MySQL的数据库,所以我觉得这个数据库也是可以使用的吧)。

NoSQL

NoSQL全称Not Only SQL,是一种相对较新的数据库设计方式。 传统的关系模型使用的是固定模式, 并将数据分割到各个表中 。然而, 对于大数据集的情况, 数据量太大使其难以存放在单一服务器中, 此时就需要扩展到多台服务器。不过,关系模型对于这种扩展的支持并不够好,因为在查询多个表时,数据可能在不同的服务器中。相反,NoSQL 数据库通常是无模式的,从设计之初就考虑了跨服务器无缝分片的问题。 在 NoSQL 中,有多种方式可 以实现该目标,分别是列数据存储 (如 HBase)、键值对存储 (如Redis)、面向文档的数据库 (如 MongoDB)以及图形数据库(如Neo4j)。

安装MongoDB

Python爬虫读书笔记——下载缓存(6)_第2张图片

这一部分暂时先跳过,我会把MongoDB这个数据库搞清楚后来写。


 

你可能感兴趣的:(爬虫)