前面已经重构好了链接获取、数据获取模块,现在开始实现具体的缓存功能。
磁盘缓存
缓存下载结果,先来尝试最容易想到的方案,将下载到的网页内容存储到文件系统中。为了实现该功能,需要将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.py
import os
import re
import urllib.parse
import shutil
import zlib
import hashlib
from datetime import datetime, timedelta
try:
import cPickle as pickle
except ImportError:
import pickle
class 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
-
避免这种缺陷的方式是通过使用url的哈希值作为文件名。
- 但是文件系统中每个卷和每个目录下的文件数量是有限,如果下载的文件过多,则会出现存储错误。
更优的方式:数据库缓存
为了避免磁盘缓存方案的已知限制,接下来将会在现有的数据库系统上创建缓存。爬取时可能需要缓存大量数据,但又不需要任何复杂的连接操作,因此将选用NoSQL数据库,这种数据库比传统的关系型数据库更易于扩展。在此我选用MongoDB作为缓存数据库。
先从官网https://www.mongodb.org/downloads下载安装mongodb客户端,然后通过pip下载额外的Python封装库。
pip install pymonogo
MongoDB教程可以看这里mongoDB 入门。
为了避免在对相同的url插入时出现多条记录,将ID设置为url,并执行upsert操作,当记录存在时更新记录,否则插入新纪录。同时也需要对数据进行压缩处理以减少数据大小。
实现代码如下:
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
class 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()
到此文件数据缓存的功能已经实现了。