用python写网络爬虫三:磁盘下载缓存

下载缓存:假设我们对同一个网站进行了多次下载,在百万个网页的情况下是不明智的,所以我们需要缓存,下过一次的不再重复下载,这样我们能够节省很多时间
为链接爬虫添加缓存支持:
在下载之前进行判断,该链接是否含有缓存,只在没有缓存发生下载是触发限速功能,

支持缓存功能的代码实现.png

完整代码地址
为 了支持缓存功能, 链接爬虫的代码也需要进行一些微调 , 包括添
加 cache 参数、 移除限速以及将 download 函数替换为新的类等, 如下面的
代码所示。

def link_crawler(seed_url, link_regex=None, delay=5, max_depth=-1, max_urls=-1, user_agent='wswp', proxies=None, num_retries=1,scrape_callback=None, cache=None):
    '''crawl from the given seed URL following link matched by link_regex'''
    crawl_quene = [seed_url]
    seen = {seed_url: 0}
    num_urls = 0
    rp = get_robots(seed_url)
    D = Downloader(delay=delay, user_agent=user_agent, proxies=proxies, num_retries=num_retries, cache=cache)

    while crawl_quene:
        url = crawl_quene.pop()
        depth = seen[url]
        if rp.can_fetch(user_agent, url):
            html = D(url)
            links = []

磁盘缓存

将下载到的网页存储到文件系统中,实现该功能,需要将URL安全的映射为跨平台的文件名


主流文件系统的限制.png

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

import re 
url = 'http://example.webscraping.com/default/view/Australia-1'
re.sub('[^/0-9a-zA-Z\-.,;_]', '_', url)

需要文件名及其父目录的长度不超过255个字符

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

还有一种边界情况,就是URL以斜杠结尾。这样分割URL后就会造成一个非法的文件名。例如:

       import urlparse
       components=urlparse.urlsplit ('http://example.webscraping.com/index/')
       print componts
       print components.path
       comonents = urlparse.urlsplit(url)
        path = comonents.path
        if not path:
            path = '/index.html'
        elif path.endswith('/'):
            path += 'index.html'
        filename = comonents.netloc+path+comonents.query

实现将URL到文件名的这些映射逻辑结合起来,

  def url_to_path(self, url):
        comonents = urlparse.urlsplit(url)
        path = comonents.path
        if not path:
            path = '/index.html'
        elif path.endswith('/'):
            path += 'index.html'
        filename = comonents.netloc+path+comonents.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)

然后在
url to path 方法中应用 了前面讨论的文件名 限制。 现在, 我们还缺少根
据文件名存取数据的方法, 下面的代码实现了这两个缺失的方法。

  def __getitem__(self, url):
        path = self.url_to_path(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.has_expired(timestamp):
                    raise KeyError(url + 'has expired')
                return result
        else:
            raise KeyError(url + 'doesnot 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)

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

通过测试发现有缓存(第二次)所需要的时间远远少于没有使用缓存(第一次)的时间
节省磁盘空间:
为了节省磁盘空间我们对下载得到的html进行压缩处理,使用zlib模块进行压缩

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

解压

return pickle.loads(zlib.decompress(fp.read()))

清理过期数据:
我们将为缓存数据添加过期时间 , 以便爬虫知道何时需要重新下载网页,。在构造方法中,我们使用timedelta对象将默认过期时间设置为30天,在set方法中把当前时间戳保存在序列化数据中,在get方法中对比当前时间和缓存时间,检查是否过期。完整代码

class DiskCache:

    def __init__(self, cache_dir='cache', expires=timedelta(days=30), compress=True):
        self.cache_dir = cache_dir
        self.expires = expires
        self.compress = compress

    def __getitem__(self, url):
        path = self.url_to_path(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.has_expired(timestamp):
                    raise KeyError(url + 'has expired')
                return result
        else:
            raise KeyError(url + 'doesnot 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)
        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):
        path = self._key_path(url)
        try:
            os.remove(path)
            os.removedirs(os.path.dirname(path))
        except OSError:
            pass

    def url_to_path(self, url):
        comonents = urlparse.urlsplit(url)
        path = comonents.path
        if not path:
            path = '/index.html'
        elif path.endswith('/'):
            path += 'index.html'
        filename = comonents.netloc+path+comonents.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 has_expired(self, timestamp):
        return datetime.utcnow() > timestamp+self.expires

    def clear(self):
        if os.path.exists(self.cache_dir):
            shutil.rmtree(self.cache_dir)
if __name__ == '__main__':    link_crawler('http://example.webscraping.com/', '/(index|view)', cache=DiskCache())

用磁盘缓存的缺点
由于受制于文件系统的限制,之前我们将URL映射为安全文件名,然而这样又会引发一些问题:- 有些URL会被映射为相同的文件名。比如URL:.../count.asp?a+b
,.../count.asp?a*b
。- URL截断255个字符的文件名也可能相同。因为URL可以超过2000下字符。
使用URL哈希值为文件名可以带来一定的改善。这样也有一些问题:- 每个卷和每个目录下的文件数量是有限制的。FAT32文件系统每个目录的最大文件数65535,但可以分割到不同目录下。- 文件系统可存储的文件总数也是有限的。ext4分区目前支持略多于1500万个文件,而一个大型网站往往拥有超过1亿个网页。
要想避免这些问题,我们需要把多个缓存网页合并到一个文件中,并使用类似B+树的算法进行索引。但我们不会自己实现这种算法,而是在下一节中介绍已实现这类算法的数据库

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容