【Python爬虫3】在下载的本地缓存做爬虫

本文涉及的产品
云数据库 MongoDB,通用型 2核4GB
简介: 下载缓存1为链接爬虫添加缓存支持2磁盘缓存1用磁盘缓存的实现2缓存测试3节省磁盘空间4清理过期数据5用磁盘缓存的缺点3数据库缓存1NoSQL是什么2安装MongoDB3MongoDB概述4MongoDB缓存实现5压缩存储6缓存测试7MongoDB缓存完整代码下载缓存上篇文章,我们学习了如何提取网页中的数据,以及将提取结果存到表格中。

下载缓存

上篇文章,我们学习了如何提取网页中的数据,以及将提取结果存到表格中。如果我们还想提取另一字段,则需要重新再下载整个网页,这对我们这个小型的示例网站问题不大,但对于数百万个网页的网站而言来说就要消耗几个星期的时间。所以,我们可以先对网页进行缓存,就使得每个网页只下载一次。

1为链接爬虫添加缓存支持

  • 我们将downloader重构一类,这样参数只需在构造方法中设置一次,就能在后续多次复用,在URL下载之前进行缓存检查,并把限速功能移到函数内部。
  • 在Downloader类的call特殊方法实现了下载前先检查缓存,如果已经定义该URL缓存则再检查下载中是否遇到了服务端错误,如果都没问题表明缓存结果可用,否则都需要正常下载该URL存到缓存中。
  • downloader方法返回添加了HTTP状态码,以便缓存中存储错误机校验。如果不需要限速或缓存的话,你可以直接调用该方法,这样就不会通过call方法调用了。

class Downloader:
    def __init__(self, delay=5, user_agent='Wu_Being', proxies=None, num_retries=1, cache=None):
        self.throttle = Throttle(delay)
        self.user_agent = user_agent
        self.proxies = proxies
        self.num_retries = num_retries
        self.cache = cache

    def __call__(self, url):
        result = None
        if self.cache:
            try:
                result = self.cache[url]
            except KeyError:
                # url is not available in cache 
                pass
            else:
                if self.num_retries > 0 and 500 <= result['code'] < 600:
                    # server error so ignore result from cache and re-download
                    result = None
        if result is None:
            # result was not loaded from cache so still need to download
            self.throttle.wait(url)
            proxy = random.choice(self.proxies) if self.proxies else None
            headers = {'User-agent': self.user_agent}
            result = self.download(url, headers, proxy=proxy, num_retries=self.num_retries)
            if self.cache:
                # save result to cache
                self.cache[url] = result
        return result['html']

    def download(self, url, headers, proxy, num_retries, data=None):
        print 'Downloading:', url
    ...
        return {'html': html, 'code': code}

class Throttle:
    def __init__(self, delay):
    ...
    def wait(self, url):
    ...

为了支持缓存功能,链接爬虫代码也需用一些微调,包括添加cache参数、移除限速以及将download函数替换为新的类。

from downloader import Downloader

def link_crawler(... cache=None):
    crawl_queue = [seed_url]
    seen = {seed_url: 0}
    # track how many URL's have been downloaded
    num_urls = 0
    rp = get_robots(seed_url)
    #cache.clear()          ###############################
    D = Downloader(delay=delay, user_agent=user_agent, proxies=proxies, num_retries=num_retries, cache=cache)

    while crawl_queue:
        url = crawl_queue.pop()
        depth = seen[url]
        # check url passes robots.txt restrictions
        if rp.can_fetch(user_agent, url):
            html = D(url)               ###def __call__(self, url):
            links = []
    ...

def normalize(seed_url, link):
    ...
def same_domain(url1, url2):
    ...
def get_robots(url):
    ...
def get_links(html):
    ...
"""
if __name__ == '__main__':
    link_crawler('http://example.webscraping.com', '/(index|view)', delay=0, num_retries=1, user_agent='BadCrawler')
    link_crawler('http://example.webscraping.com', '/(index|view)', delay=0, num_retries=1, max_depth=1, user_agent='GoodCrawler')
"""

现在,这个支持缓存的网络爬虫的基本架构已经准备好了,下面就要开始构建实际的缓存功能了。

2磁盘缓存

操作系统 文件系统 非法文件名字符 文件名最大长度
Linux Ext3/Ext4 /\0 255个字节
OS X HFS Plus :\0 255个UTF-16编码单元
Windows NTFS \/?:*"><和` `

为了保证在不同文件系统中,我们的文件路径都是安全的,就需要把除数字、字母和基本符号的其他字符替换为下划线。

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

此外,文件名及其目录长度需要限制在255个字符以内。

>>> filename=re.sub('[^/0-9a-zA-Z\-,.;_ ]','_',url)
>>> filename='/'.join(segment[:255] for segment in filename.split('/'))
>>> print filename
http_//example.webscraping.com/default/view/australia-1
>>> print '#'.join(segment[:5] for segment in filename.split('/'))
http_##examp#defau#view#austr
>>> 

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

对于第一个URL可以在后面添加index.html作为文件名,所以可以把index作为目录名,1为子目录名,index.html为文件名。

>>> import urlparse
>>> components=urlparse.urlsplit('http://exmaple.scraping.com/index/')
>>> print components
SplitResult(scheme='http', netloc='exmaple.scraping.com', path='/index/', query='', fragment='')
>>> print components.path
/index/
>>> path=components.path
>>> if not path:
...     path='/index.html'
... elif path.endswith('/'):
...     path+='index.html'
... 
>>> filename=components.netloc+path+components.query
>>> filename
'exmaple.scraping.com/index/index.html'
>>> 

2.1用磁盘缓存的实现

现在可以把URL到目录和文件名完整映射逻辑结合起来,就形成了磁盘缓存的主要部分。该构造方法传入了用于设定缓存位置的参数,然后在url_to_path方法中应用了前面讨论的文件名限制。

from link_crawler import link_crawler

class DiskCache:

    def __init__(self, cache_dir='cache', ...):
        """
        cache_dir: the root level folder for the cache
        """
        self.cache_dir = cache_dir
    ...

    def url_to_path(self, url):
        """Create file system path for this URL
        """
        components = urlparse.urlsplit(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 maximum number of characters
        filename = '/'.join(segment[:255] for segment in filename.split('/'))
        return os.path.join(self.cache_dir, filename) #拼接当前目录和文件名为完整目录

    def __getitem__(self, url):
        ...
    def __setitem__(self, url, result):
        ...
    def __delitem__(self, url):
        ...
    def has_expired(self, timestamp):
        ...
    def clear(self):
    ...

if __name__ == '__main__':
    link_crawler('http://example.webscraping.com/', '/(index|view)', cache=DiskCache())

现在我们还缺少根据文件名存取数据的方法,就是Downloader类result=cache[url]cache[url]=result的接口方法:__getitem__()__setitem__()两个特殊方法。

import pickle

class DiskCache:

    def __init__(self, cache_dir='cache', expires=timedelta(days=30), compress=True):
    ...    
    def url_to_path(self, url):
    ...
    def __getitem__(self, url):
    ...
    def __setitem__(self, url, result):
        """Save data to disk for this url
        """
        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))

__setitem__()中,我们使用url_to_path()方法将URL映射为安全文件名,在必要情况下还需要创建目录。这里使用的pickle模块会把输入转化为字符串(序列化),然后保存到磁盘中。

import pickle

class DiskCache:

    def __init__(self, cache_dir='cache', expires=timedelta(days=30), compress=True):
    ...    
    def url_to_path(self, url):
    ...
    def __getitem__(self, url):
        """Load data from disk for this URL
        """
        path = self.url_to_path(url)
        if os.path.exists(path):
            with open(path, 'rb') as fp:
                return pickle.loads(fp.read())
        else:
            # URL has not yet been cached
            raise KeyError(url + ' does not exist')

    def __setitem__(self, url, result):
    ...

__getitem__()中,还是先用url_to_path()方法将URL映射为安全文件名。然后检查文件是否存在,如果存在则加载内容,并执行反序列化,恢复其原始数据类型;如果不存在,则说明缓存中还没有该URL的数据,此时会抛出KeyError异常。

2.2缓存测试

可以在python命令前加time计时。我们可以发现,如果是在本地服务器的网站,当缓存为空时爬虫实际耗时0m58.710s,第二次运行全部从缓存读取花了0m0.221s,快了265多倍。如果是爬取远程服务器的网站的数据时,将会耗更多时间。

wu_being@ubuntukylin64:~/GitHub/WebScrapingWithPython/3.下载缓存$ time python 2disk_cache_Nozip127.py 
Downloading: http://127.0.0.1:8000/places/
Downloading: http://127.0.0.1:8000/places/default/index/1
...
Downloading: http://127.0.0.1:8000/places/default/view/Afghanistan-1
real    0m58.710s
user    0m0.684s
sys 0m0.120s
wu_being@ubuntukylin64:~/GitHub/WebScrapingWithPython/3.下载缓存$ time python 2disk_cache_Nozip127.py 

real    0m0.221s
user    0m0.204s
sys 0m0.012s

2.3节省磁盘空间

为节省缓存占用空间,我们可以对下载的HTML文件进行压缩处理,使用zlib压缩序列化字符串即可。

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

从磁盘加载后解压的代码如下:

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

压缩所有网页之后,缓存占用大小2.8 MB下降到821.2 KB,耗时略有增加。

wu_being@ubuntukylin64:~/GitHub/WebScrapingWithPython/3.下载缓存$ time python 2disk_cache.py 
Downloading: http://127.0.0.1:8000/places/
Downloading: http://127.0.0.1:8000/places/default/index/1
...
Downloading: http://127.0.0.1:8000/places/default/view/Afghanistan-1

real    1m0.011s
user    0m0.800s
sys 0m0.104s
wu_being@ubuntukylin64:~/GitHub/WebScrapingWithPython/3.下载缓存$ 
wu_being@ubuntukylin64:~/GitHub/WebScrapingWithPython/3.下载缓存$ time python 2disk_cache.py 

real    0m0.252s
user    0m0.228s
sys 0m0.020s
wu_being@ubuntukylin64:~/GitHub/WebScrapingWithPython/3.下载缓存$ 

2.4清理过期数据

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

from datetime import datetime, timedelta

class DiskCache:

    def __init__(self, cache_dir='cache', expires=timedelta(days=30), compress=True):
        """
        cache_dir: the root level folder for the cache
        expires: timedelta of amount of time before a cache entry is considered expired
        compress: whether to compress data in the cache
        """
        self.cache_dir = cache_dir
        self.expires = expires
        self.compress = compress

    def __getitem__(self, url):
        """Load data from disk for this 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:
            # URL has not yet been cached
            raise KeyError(url + ' does not exist')

    def __setitem__(self, url, result):
        """Save data to disk for this url
        """
        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 has_expired(self, timestamp):
        """Return whether this timestamp has expired
        """
        return datetime.utcnow() > timestamp + self.expires

为了测试时间功能,我们可以将其缩短为5秒,如下操作:

    """
    Dictionary interface that stores cached 
    values in the file system rather than in memory.
    The file path is formed from an md5 hash of the key.
    """
>>> from disk_cache import DiskCache
>>> cache=DiskCache()
>>> url='http://www.baidu.com'
>>> result={'html':'<html>...','code':200}
>>> cache[url]=result
>>> cache[url]
{'code': 200, 'html': '<html>...'}
>>> cache[url]['html']==result['html']
True
>>> 
>>> from datetime import timedelta
>>> cache2=DiskCache(expires=timedelta(seconds=5))
>>> url2='https://www.baidu.sss'
>>> result2={'html':'<html>..ss.','code':500}
>>> cache2[url2]=result2
>>> cache2[url2]
{'code': 200, 'html': '<html>...'}
>>> cache2[url2]
{'code': 200, 'html': '<html>...'}
>>> cache2[url2]
{'code': 200, 'html': '<html>...'}
>>> cache2[url2]
{'code': 200, 'html': '<html>...'}
>>> cache2[url2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "disk_cache.py", line 57, in __getitem__
    raise KeyError(url + ' has expired')
KeyError: 'http://www.baidu.com has expired'
>>> cache2.clear()

2.5用磁盘缓存的缺点

由于受制于文件系统的限制,之前我们将URL映射为安全文件名,然而这样又会引发一些问题:
- 有些URL会被映射为相同的文件名。比如URL:.../count.asp?a+b,.../count.asp?a*b
- URL截断255个字符的文件名也可能相同。因为URL可以超过2000下字符。

使用URL哈希值为文件名可以带来一定的改善。这样也有一些问题:
- 每个卷和每个目录下的文件数量是有限制的。FAT32文件系统每个目录的最大文件数65535,但可以分割到不同目录下。
- 文件系统可存储的文件总数也是有限的。ext4分区目前支持略多于1500万个文件,而一个大型网站往往拥有超过1亿个网页。

要想避免这些问题,我们需要把多个缓存网页合并到一个文件中,并使用类似B+树的算法进行索引。但我们不会自己实现这种算法,而是在下一节中介绍已实现这类算法的数据库。

3数据库缓存

爬取时,我们可能需要缓存大量数据,但又无须任何复杂的连接操作,因此我们将选用NoSQL数据库,这种数据库比传统的关系型数据库更容易扩展。在本节中,我们将选用目前非常流行的MongoDB作为缓存数据库。

3.1NoSQL是什么

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

- 列数据存储(如HBase);
- 键值对存储(如Redis);
- 图形数据库(如Neo4j);
- 面向文档的数据库(如MongoDB)。

3.2安装MongoDB

MongoDB可以从https://www.mongodb.org/downloads 下载。然后安装其Python封装库:

pip install pymongo

检测安装是否成功,在本地启动MongoDB服务器:

wu_being@ubuntukylin64:~/GitHub/WebScrapingWithPython/3.下载缓存$ mongod -dbpath MongoD
2017-01-17T21:20:46.224+0800 [initandlisten] MongoDB starting : pid=1978 port=27017 dbpath=MongoD 64-bit host=ubuntukylin64
2017-01-17T21:20:46.224+0800 [initandlisten] db version v2.6.10
2017-01-17T21:20:46.224+0800 [initandlisten] git version: nogitversion
2017-01-17T21:20:46.225+0800 [initandlisten] OpenSSL version: OpenSSL 1.0.2g  1 Mar 2016
2017-01-17T21:20:46.225+0800 [initandlisten] build info: Linux lgw01-12 3.19.0-25-generic #26~14.04.1-Ubuntu SMP Fri Jul 24 21:16:20 UTC 2015 x86_64 BOOST_LIB_VERSION=1_58
2017-01-17T21:20:46.225+0800 [initandlisten] allocator: tcmalloc
2017-01-17T21:20:46.225+0800 [initandlisten] options: { storage: { dbPath: "MongoD" } }
2017-01-17T21:20:46.269+0800 [initandlisten] journal dir=MongoD/journal
2017-01-17T21:20:46.270+0800 [initandlisten] recover : no journal files present, no recovery needed
2017-01-17T21:20:49.126+0800 [initandlisten] preallocateIsFaster=true 33.72
2017-01-17T21:20:51.932+0800 [initandlisten] preallocateIsFaster=true 32.7
2017-01-17T21:20:55.729+0800 [initandlisten] preallocateIsFaster=true 32.36
2017-01-17T21:20:55.730+0800 [initandlisten] preallocateIsFaster check took 9.459 secs
2017-01-17T21:20:55.730+0800 [initandlisten] preallocating a journal file MongoD/journal/prealloc.0
2017-01-17T21:20:58.042+0800 [initandlisten]        File Preallocator Progress: 608174080/1073741824    56%
2017-01-17T21:21:03.290+0800 [initandlisten]        File Preallocator Progress: 744488960/1073741824    69%
2017-01-17T21:21:08.043+0800 [initandlisten]        File Preallocator Progress: 954204160/1073741824    88%
2017-01-17T21:21:18.347+0800 [initandlisten] preallocating a journal file MongoD/journal/prealloc.1
2017-01-17T21:21:21.166+0800 [initandlisten]        File Preallocator Progress: 639631360/1073741824    59%
2017-01-17T21:21:26.328+0800 [initandlisten]        File Preallocator Progress: 754974720/1073741824    70%
...

然后,在Python中,使用MongoDB的默认端口尝试连接MongoDB:

>>> from pymongo import MongoClient
>>> client=MongoClient('localhost',27017)

3.3MongoDB概述

下面是MongoDB示例代码:

>>> from pymongo import MongoClient
>>> client=MongoClient('localhost',27017)
>>> url='http://www.baidu.com/view/China-47'
>>> html='...<html>...'
>>> db=client.cache
>>> db.webpage.insert({'url':url,'html':html})
ObjectId('587e2cb26b00c10b956e0be9')
>>> db.webpage.find_one({'url':url})
{u'url': u'http://www.baidu.com/view/China-47', u'_id': ObjectId('587e2cb26b00c10b956e0be9'), u'html': u'...<html>...'}
>>> db.webpage.find({'url':url})
<pymongo.cursor.Cursor object at 0x7fcde0ca60d0>
>>> db.webpage.find({'url':url}).count()
1

当插入同一条记录时,MongoDB会欣然接受并执行这次操作,但通过查找发现记录没更新。

>>> db.webpage.insert({'url':url,'html':html})
ObjectId('587e2d546b00c10b956e0bea')
>>> db.webpage.find({'url':url}).count()
2
>>> db.webpage.find_one({'url':url})
{u'url': u'http://www.baidu.com/view/China-47', u'_id': ObjectId('587e2cb26b00c10b956e0be9'), u'html': u'...<html>...'}

为了存储最新的记录,并避免重复记录,我们将ID设置为URL,并执行upsert操作。该操作表示当记录存在时则更新记录,否则插入新记录。

>>> 
>>> new_html='<...>...'
>>> db.webpage.update({'_id':url},{'$set':{'html':new_html}},upsert=True)
{'updatedExisting': True, u'nModified': 1, u'ok': 1, u'n': 1}
>>> db.webpage.find_one({'_id':url})
{u'_id': u'http://www.baidu.com/view/China-47', u'html': u'<...>...'}
>>> db.webpage.find({'_id':url}).count()
1
>>> db.webpage.update({'_id':url},{'$set':{'html':new_html}},upsert=True)
{'updatedExisting': True, u'nModified': 0, u'ok': 1, u'n': 1}
>>> db.webpage.find({'_id':url}).count()
1
>>> 

MongoDB官方文档:http://docs.mongodb.org/manual/

3.4MongoDB缓存实现

现在我们已经准备好创建基于MongoDB的缓存了,这里使用了和之前的DiskCache类相同的接口。我们在下面构造方法中创建了timestamp索引,在达到给定的时间戳之后,MongoDB的这一便捷功能可以自动过期删除记录。

import pickle
from datetime import datetime, timedelta
from pymongo import MongoClient

class MongoCache:
    def __init__(self, client=None, expires=timedelta(days=30)):
        """
        client: mongo database client
        expires: timedelta of amount of time before a cache entry is considered expired
        """
        # 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 = self.client.cache
        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)

下面我们来测试一下这个MongoCache类,我们用默认0时间间隔timedelta()对象进行测试,此时记录创建后应该会马上会被删除,但实际却没有。这是因为MongoDB运行机制造成的,MongoDB后台运行了一个每分钟检查一次过期记录的任务。所以我们可以再等一分钟,就会发现缓存过期机制已经运行成功了。

>>> from mongo_cache import MongoCache
>>> from datetime import timedelta
>>> cache=MongoCache(expires=timedelta())
>>> result={'html':'.....'}
>>> cache[url]=result
>>> cache[url]
{'html': '.....'}
>>> cache[url]
{'html': '.....'}
>>> import time
>>> import time;time.sleep(60)
>>> cache[url]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "mongo_cache.py", line 62, in __getitem__
    raise KeyError(url + ' does not exist')
KeyError: 'http://www.baidu.com/view/China-47 does not exist'
>>> 

3.5压缩存储

import pickle
import zlib
from bson.binary import Binary

class MongoCache:
    def __getitem__(self, url):
        """Load value at this URL
        """
        record = self.db.webpage.find_one({'_id': url})
        if record:
            #return record['result']
            return pickle.loads(zlib.decompress(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()}
        record = {'result': Binary(zlib.compress(pickle.dumps(result))), 'timestamp': datetime.utcnow()}
        self.db.webpage.update({'_id': url}, {'$set': record}, upsert=True)

3.6缓存测试

wu_being@ubuntukylin64:~/GitHub/WebScrapingWithPython/3.下载缓存$ time python 3mongo_cache.py 
Downloading: http://127.0.0.1:8000/places/
Downloading: http://127.0.0.1:8000/places/default/index/1
Downloading: http://127.0.0.1:8000/places/default/index/2
...
Downloading: http://127.0.0.1:8000/places/default/view/Algeria-4
Downloading: http://127.0.0.1:8000/places/default/view/Albania-3
Downloading: http://127.0.0.1:8000/places/default/view/Aland-Islands-2
Downloading: http://127.0.0.1:8000/places/default/view/Afghanistan-1

real    0m59.239s
user    0m1.164s
sys 0m0.108s


wu_being@ubuntukylin64:~/GitHub/WebScrapingWithPython/3.下载缓存$ time python 3mongo_cache.py 

real    0m0.695s
user    0m0.408s
sys 0m0.044s

可以看出,用数据库缓存的读取时间是磁盘缓存的两倍,但成功地避免了磁盘缓存的缺点。

3.7MongoDB缓存完整代码

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

from link_crawler import link_crawler


class MongoCache:
    """
    Wrapper around MongoDB to cache downloads

    >>> cache = MongoCache()
    >>> cache.clear()
    >>> url = 'http://example.webscraping.com'
    >>> result = {'html': '...'}
    >>> cache[url] = result
    >>> cache[url]['html'] == result['html']
    True
    >>> cache = MongoCache(expires=timedelta())
    >>> cache[url] = result
    >>> # every 60 seconds is purged http://docs.mongodb.org/manual/core/index-ttl/
    >>> import time; time.sleep(60)
    >>> cache[url] 
    Traceback (most recent call last):
     ...
    KeyError: 'http://example.webscraping.com does not exist'
    """
    def __init__(self, client=None, expires=timedelta(days=30)):
        """
        client: mongo database client
        expires: timedelta of amount of time before a cache entry is considered expired
        """
        # 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 = self.client.cache
        self.db.webpage.create_index('timestamp100s', expireAfterSeconds=expires.total_seconds())       #timestamp

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

    def __getitem__(self, url):
        """Load value at this URL
        """
        record = self.db.webpage.find_one({'_id': url})
        if record:
            #return record['result']
            return pickle.loads(zlib.decompress(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()}
        record = {'result': Binary(zlib.compress(pickle.dumps(result))), 'timestamp100s': datetime.utcnow()}    #timestamp
        self.db.webpage.update({'_id': url}, {'$set': record}, upsert=True)


    def clear(self):
        self.db.webpage.drop()
        print 'drop() successful'



if __name__ == '__main__':  
    #link_crawler('http://example.webscraping.com/', '/(index|view)', cache=MongoCache())
    #link_crawler('http://127.0.0.1:8000/places/', '/places/default/(index|view)/', cache=MongoCache())
    link_crawler('http://127.0.0.1:8000/places/', '/places/default/(index|view)/', cache=MongoCache(expires=timedelta(seconds=100)))

Wu_Being 博客声明:本人博客欢迎转载,请标明博客原文和原链接!谢谢!
《【Python爬虫3】在下载的本地缓存做爬虫》http://blog.csdn.net/u014134180/article/details/55506984

Wu_Being 吴兵博客接受赞助费二维码

如果你看完这篇博文,觉得对你有帮助,并且愿意付赞助费,那么我会更有动力写下去。

相关实践学习
MongoDB数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。 &nbsp; 相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
目录
相关文章
|
11天前
|
数据采集 存储 API
网络爬虫与数据采集:使用Python自动化获取网页数据
【4月更文挑战第12天】本文介绍了Python网络爬虫的基础知识,包括网络爬虫概念(请求网页、解析、存储数据和处理异常)和Python常用的爬虫库requests(发送HTTP请求)与BeautifulSoup(解析HTML)。通过基本流程示例展示了如何导入库、发送请求、解析网页、提取数据、存储数据及处理异常。还提到了Python爬虫的实际应用,如获取新闻数据和商品信息。
|
15天前
|
数据采集 Python
【python】爬虫-西安医学院-校长信箱
本文以西安医学院-校长信箱为基础来展示爬虫案例。来介绍python爬虫。
【python】爬虫-西安医学院-校长信箱
|
25天前
|
缓存 NoSQL 关系型数据库
在Python Web开发过程中:数据库与缓存,MySQL和NoSQL数据库的主要差异是什么?
MySQL是关系型DB,依赖预定义的表格结构,适合结构化数据和复杂查询,但扩展性有限。NoSQL提供灵活的非结构化数据存储(如JSON),无统一查询语言,但能横向扩展,适用于大规模、高并发场景。选择取决于应用需求和扩展策略。
114 1
|
21天前
|
数据采集 安全 Python
python并发编程:Python实现生产者消费者爬虫
python并发编程:Python实现生产者消费者爬虫
24 0
python并发编程:Python实现生产者消费者爬虫
|
1天前
|
数据采集 存储 JSON
Python爬虫面试:requests、BeautifulSoup与Scrapy详解
【4月更文挑战第19天】本文聚焦于Python爬虫面试中的核心库——requests、BeautifulSoup和Scrapy。讲解了它们的常见问题、易错点及应对策略。对于requests,强调了异常处理、代理设置和请求重试;BeautifulSoup部分提到选择器使用、动态内容处理和解析效率优化;而Scrapy则关注项目架构、数据存储和分布式爬虫。通过实例代码,帮助读者深化理解并提升面试表现。
9 0
|
4天前
|
数据采集 JavaScript 前端开发
使用Python打造爬虫程序之破茧而出:Python爬虫遭遇反爬虫机制及应对策略
【4月更文挑战第19天】本文探讨了Python爬虫应对反爬虫机制的策略。常见的反爬虫机制包括User-Agent检测、IP限制、动态加载内容、验证码验证和Cookie跟踪。应对策略包括设置合理User-Agent、使用代理IP、处理动态加载内容、验证码识别及维护Cookie。此外,还提到高级策略如降低请求频率、模拟人类行为、分布式爬虫和学习网站规则。开发者需不断学习新策略,同时遵守规则和法律法规,确保爬虫的稳定性和合法性。
|
11天前
|
开发者 Python
Python中使用`requests`库进行文件上传与下载的技术详解
【4月更文挑战第12天】在Python的网络编程中,文件上传和下载是常见的需求。`requests`库作为一个强大且易用的HTTP客户端,为我们提供了简便的文件上传和下载功能。本文将详细介绍如何在Python中使用`requests`库进行文件上传和下载。
|
14天前
|
缓存 Python
python本地缓存cacheout
python本地缓存cacheout
|
14天前
|
数据采集 自然语言处理 搜索推荐
一篇博客带你领略学习Python的第三方库---如何获取和安装第三方库,关于三种常见第三方库的下载和讲解(pyinstall库,jieba库,wordcloud库),更多第三方库的分类介绍
一篇博客带你领略学习Python的第三方库---如何获取和安装第三方库,关于三种常见第三方库的下载和讲解(pyinstall库,jieba库,wordcloud库),更多第三方库的分类介绍
|
16天前
|
数据采集 存储 前端开发
Python爬虫如何快速入门
写了几篇网络爬虫的博文后,有网友留言问Python爬虫如何入门?今天就来了解一下什么是爬虫,如何快速的上手Python爬虫。
18 0

热门文章

最新文章