缓存,还是不缓存?是一个问题。
如果你需要执行一个大型爬取工作,那么它可能会由于错误或异常被中断,缓存可以帮助你无须重新爬取那些可能已经抓取过的页面。缓存还可以让你在离线时访问这些页面(出于数据分析或开发的目的)。
不过,如果你的最高优先级是获得网站最新和当前的信息,那此时缓存就没有意义。此外,如果你没有计划实现大型或可重复的爬虫,那么可能只需要每次去抓取页面即可。
那如果支持了缓存,多久清空缓存并抓取新页面?后面都会讲解,首先让我们学习如何使用缓存!
import os
import re
from urllib.parse import urlsplit
import json
from Link_Crawler import link_crawler
class DiskCache:
def __init__(self, cache_dir='cache', max_len=255):
self.cache_dir = cache_dir
self.max_len = max_len
def url_to_path(self, url):
""" Return file system path string for given URL"""
components = urlsplit(url)
# append index.html to empty paths
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(seg[:self.max_len] for seg in filename.split('/'))
return os.path.join(self.cache_dir, filename)
def __getitem__(self, url):
"""Load data from disk for given URL"""
path = self.url_to_path(url)
if os.path.exists(path):
return json.load(path)
else:
# URL has not yet been cached
raise KeyError(url + ' does not exist')
def __setitem__(self, url, result):
"""Save data to disk for given url"""
path = self.url_to_path(url)
folder = os.path.dirname(path)
if not os.path.exists(folder):
os.makedirs(folder)
json.dump(result, path)
if __name__ == '__main__':
import time
start = time.time()
link_crawler('http://example.python-scraping.com/', '/view', cache=DiskCache())
print("runing:%s" % (time.time()-start))
从运行时间和运行结果来看,第二次没有去下载,而是从磁盘重读取出来,可以看出效率提高了许多.
缺点
介于磁盘缓存的限制,爬取到的数据量比较大,但又无任何复杂的连接,所以选用NoSQL数据库,这种数据库相比常用的关系型数据库更容易扩展。我们将使用非常流行的键值对存储 Redis 作为我们的缓存。
键值对存储类似于 Python 字典,存储中的每个元素都有一个键和一个值。在设计 DiskCache 时,键值对模型可以很好地解决该问题。Redis 实际上表示 REmote DIctionary Server(远程字典服务器)。
Redis官方不支持Windows系统,只能用微软开源部门移植的版本,笔者下载版本Windows Redis下载,下载解压之后,在其文件夹直接打开cmd窗口,输入redis-server.exe redis.windows.conf
,然后你就可以看到redis的信息了。(已经安装完成)
Python程序想使用,也需要单独安装库pip install redis
,完成之后通过下面代码测试是否安装成功。
import redis
r = redis.StrictRedis(host='localhost', port=6379, db=0)
r.set('test', 'answer')
True
r.get('test')
b'answer'
像上面,可以正常导入,正常存取,即说明安装成功,服务也启动,就可以进行后续的操作了。
set命令:存数据
只是简单地覆盖了之前的值,这对于类似网络爬虫这样的简单存储来说非常合适。
get命令:取数据
从 Redis 存储中接收到的是 bytes 类型,即使我们插入的是字典或字符串。
keys():
keys 方法返回了所有可用键的列表。
delete():
delete 方法可以让我们传递一个(或多个)键并从存储中删除它们
flushdb():
删除所有的键
import json
from datetime import timedelta
from redis import StrictRedis
from Link_Crawler import link_crawler
class RedisCache:
def __init__(self, client=None, expires=timedelta(days=30), encoding='utf-8'):
# if a client object is not passed then try
# connecting to redis at the default localhost port
self.client = StrictRedis(host='localhost', port=6379, db=0) if client is None else client
self.expires = expires
self.encoding = encoding
def __getitem__(self, url):
"""Load value from Redis for the given URL"""
record = self.client.get(url)
if record:
return json.loads(record.decode(self.encoding))
else:
raise KeyError(url + ' does not exist')
def __setitem__(self, url, result):
"""Save value in Redis for the given URL"""
data = bytes(json.dumps(result), self.encoding)
self.client.setex(url, self.expires, data)
if __name__ == '__main__':
import time
start = time.time()
link_crawler('http://example.python-scraping.com/', '/view', cache=RedisCache())
print("runing:%s" % (time.time()-start))
setex 方法,能够使我们在设置键值时附带过期时间。setex 既可以接受 datetime.timedelta,也可以接受以秒为单位的数值。这是一个非常方便的 Redis 功能,可以在指定秒数后自动删除记录。这就意味着我们不再需要像 DiskCache 类那样手工检查记录是否在我们的过期规则内。
通过测试发现,无缓存时,爬取页面需要2.057s,当存在缓存时,爬取相同页面只需要0.3640s,缓存的效果显而易见。