磁盘缓存
为链接爬虫添加缓存支持
将之前的download函数重构为一个类,将限速功能放到下载函数中,只有在真正的下载时在会触发限速,而在加载时不会触发。
#DownLoad.py
import random
import re
import socket
import urllib2
import urlparse
import time
import datetime
#设置代理服务器
DEFAULT_AGENT='wswp'
#时延变量
DEFAULT_DELAY=5
#爬取的深度
DEFAULT_RETRIES=1
#超时时间
DEFAULT_TIMEOUT=60
class Downloader:
"""一个关于下载网页的类"""
def __init__(self,delay=DEFAULT_DELAY,user_agent=DEFAULT_AGENT,
opener=None,proxies=None,num_retries=DEFAULT_RETRIES,
timeout=DEFAULT_TIMEOUT,cache=None):
#如果超时,远程主机强迫关闭了一个现有的连接
socket.setdefaulttimeout(timeout)
#实例化一个时延对象
self.throttle=Throttle(delay)
#代理服务器
self.user_agent=user_agent
self.proxies=proxies
#设置重试的最大次数
self.num_retries=num_retries
#缓存
self.cache=cache
self.opener=opener
# 在对象作为函数被调用的时候会调用这个方法
def __call__(self,url):
result=None
#检查是否有缓存
if self.cache:
# 检查是否缓存了该URL
try:
# 从cache中加载数据
result = self.cache[url]
# 检查报错,如果url没有就什么都不做
except KeyError:
pass
else:
# 如果以上检测都正确
# 判断重试的次数
# 判断服务器报错,即报错码以5开头,
# 就使result为空并且重新下载
if self.num_retries > 0 and 500 <= result['code'] < 600:
result = None
# 如果result没有定义,就直接下载
if result is None:
# 在下载之前,加上时延,保证安全下载
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, self.num_retries)
if self.cache:
# 向cache中保存结果
self.cache[url] = result
return result['html']
def download(self,url,headers, proxy, num_retries,data=None):
print 'Downloading:', url
request = urllib2.Request(url,data,headers or {})
opener = self.opener or urllib2.build_opener()
if opener:
proxy_params = {urlparse.urlparse(url).scheme: proxy}
opener.add_handler(urllib2.ProxyHandler(proxy_params))
try:
response=urllib2.urlopen(request)
html=response.read()
code=response.code
except Exception as e:
print 'Download:', str(e)
html = None
if hasattr(e, 'code'):
if num_retries > 0 and 500 <= e.code < 600:
return self.download(url,headers,proxy, num_retries - 1,data)
else:
code=None
return {'html':html,'code':code}
# Throttle类记录了每个域名上次访问的时间,如果当前时间距离上次
# 访问的时间小于延迟,则执行睡眠操作。
class Throttle:
def __init__(self,delay):
self.delay=delay
self.domains={}
def wait(self ,url):
#urlparse.urlparse将url地址拆成六个部分,netloc为域名服务器,也可能包含用户信息
domain=urlparse.urlparse(url).netloc
last_accessed=self.domains.get(domain)
if self.delay >0 and last_accessed is not None:
#判断时延时间是否大于现在时间-上一次访问的时间
sleep_secs=self.delay-(datetime.datetime.now()-last_accessed).seconds
if sleep_secs>0:
time.sleep(sleep_secs)
#更新上一次访问时间
self.domains[domain]=datetime.datetime.now()
获取中网页链接
import re
import robotparser
import urlparse
from DownLoad import Downloader
#获取网页中的链接
def link_crawler(seed_url,link_regex=None,max_urls=50,max_depth=1,delay=5,user_agent='wswp',proxies=None,num_retries=1,scrape_callback=None,cache=None):
#存储获取到的url的queue
craw_queue=[seed_url]
#记录url以及这个url的深度的字典
seen={seed_url:0}
#下载了url的个数
num_url=0
#判断该网站是否可以爬取
rp=get_robots(seed_url)
D=Downloader(delay=delay,user_agent=user_agent,proxies=proxies,num_retries=num_retries,cache=cache)
while craw_queue:
#弹出队列顶的url
url=craw_queue.pop()
#获取这个url的深度
depth=seen[url]
#检查这个网页是否可以爬取
if rp.can_fetch(user_agent,url):
html=D(url)
links=[]
if scrape_callback:
links.extend(scrape_callback(url,html) or [])
#判断当前爬取的url的深度是否为规定的最大深度
if depth != max_depth:
#是否存在要匹配的正则格式
if link_regex:
#将匹配到的链接放到links里
links.extend(link for link in get_links(html) if re.search(link_regex,link))
for link in links:
link=normalize(seed_url,link)
#判断是否已经爬取过该链接
if link not in seen:
#将该链接的深度加1,以便后面做判断
seen[link]=depth+1
#判断该链接与首链接域名是否一致,
if same_damain(seed_url,link):
craw_queue.append(link)
#如果爬取链接的数量达到最大值就退出
num_url+=1
if num_url==max_urls:
break
else:
print 'Bilocked by robots.txt',url
def get_robots(url):
#解析robots.txt文件,判断该网站是否可以爬取
rp=robotparser.RobotFileParser()
rp.set_url(urlparse.urljoin(url,'/robot.txt'))
rp.read()
return rp
#获取网页中所有链接,并返回一个列表
def get_links(html):
webpage_regx=re.compile('<a[^>]+href=["\'](.*?)["\']',re.IGNORECASE)
link_list1=webpage_regx.findall(html)
link_list2=[]
for link in link_list1:
if re.search('/view/',link):
link_list2.append(link)
return link_list1
#去掉链接的标示符,并且加上绝对路径
def normalize(seed_url,link):
#如果url包含一个片段标识符,则返回一个没有片段标识符
# 的修改过的url,并且这个片段标识符作为单独的字符串。
link,_=urlparse.urldefrag(link)
return urlparse.urljoin(seed_url,link)
#判断目前爬取的链接和第一个链接域名是否则一致
def same_damain(url1,url2):
return urlparse.urlparse(url1).netloc==urlparse.urlparse(url2).netloc
磁盘缓存
将下载到的网页存到文件系统中,需要将URL安全地映射为跨平台文件名.
操作系统 | 文件系统 | 非法文件名字符 | 文件名最大长度 |
---|---|---|---|
Linux | Ext3/Ext4 | /和\0 | 255字节 |
OS X | HFSN Plus | :和\0 | 255个UTF-16编码字节 |
Windows | NTFS | ,/,?,*,",<,>,和’ | ’ |
为了保证在不同的文件系统文件路径式安全的,就需要限制其只能包含数字,字母和基本符号,并将其他字符替换为下划线.
>>> import re
>>> url='http://example.webscraping.com'
>>> re.sub('[^/0-9a-zA-Z\-.,;_]','_',url)
'http_//example.webscraping.com'
限制文件名以及其符目录的长度在255个字符内
filename='/'.join(segment[:255] for segment in filename.split('/'))
如果URL路径以/结尾,此时斜杠后的空字符串就会成为一个非法文件名,但是如果移除又会造成无法保存其他URL的问题.
- http://example.webscraping.com/index/
- http://example.webscraping.com/index/1
如果希望这两个URL都能保存下来,就以index为目录名,以1作为子路径.对于第一个这种以/结尾的情况,解决方案时添加index.html作为其文件名.
>>> import urlparse
]>>> components=urlparse.urlsplit('http://example.webscraping.com/index/')
>>> print components
SplitResult(scheme='http', netloc='example.webscraping.com', path='/index/', query='', fragment='')
使用分隔模块对上述边界情况添加index.htnl示例代码
>>> path=components.path
>>> if not path:
... path='/index.html'
... elif path.endswith('/'):
... path+='index.html'
>>> filename=components.netloc+path+components.query
>>> filename
'example.webscraping.com/index/'
实现
import re
import urlparse
from datetime import timedelta
import datetime
# datetime.timedelata两个时间之间的时间差
import os
import zlib
import pickle
import shutil
from Link_crawler import link_crawler
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):
#从磁盘中加载数据
#获取这个url在文件系统中的绝对路径
path=self.url_to_path(url)
#如果这个文件路径存在就读取这个文件的数据
if os.path.exists(path):
with open(path,'rb') as fp:
data=fp.read()
if self.compress:
# zlib.compress用于压缩流数据。参数string指定了要压缩的数据流,
# 参数level指定了压缩的级别,它的取值范围是1到9。压缩速度与压缩率成反比,
# 1表示压缩速度最快,而压缩率最低,而9则表示压缩速度最慢但压缩率最高。
data=zlib.decompress(data)
# 把变量从内存中变成可存储或传输,必填参数data必须以二进制可读模式打开
result,timestamp=pickle.loads(data)
#判断文件是否过期
if self.has_expired(timestamp):
raise KeyError(url+'has expired')
#返回读取到的数据
return result
def __setitem__(self, url,result):
"""将url读取的数据保存到磁盘"""
#创建这个url的文件在系统中的路径
path=self.url_to_path(url)
#获取文件的目录名
folder=os.path.dirname(path)
if not os.path.exists(folder):
#如果目录名不存在则创建
os.makedirs(folder)
#将数据和当前时间,返回一个字符串,它包含一个 pickle 格式的对象
data=pickle.dumps((result,datetime.datetime.utcnow()))
if self.compress:
data=zlib.compress(data)
#将压缩后的数据以二进制形式写入文件
with open(path,'wb') as fp:
fp.write(data)
def __delitem__(self, url):
path=self.url_to_path(url)
try:
#删除该url的文件以及目录
os.remove(path)
os.removedirs(os.path.dirname(path))
except OSError:
pass
def url_to_path(self,url):
#给url创建文件系统的路径
#分解url为六个部分
components=urlparse.urlsplit(url)
#获取url的目录路径名
path=components.path
#判断文件路径是否为空
if not path:
path='/index.html'
#判断是否以/结尾,以避免成为非法文件名
elif path.endswith('/'):
path+='index.html'
#合成系统文件名
#netloc:服务器的位置
#path:网页文件在服务器中存放的位置
#query:连接符(&)连接键值对
filename=components.netloc+path+components.query
#保证文件名的可移植性
filename=re.sub('[^/0-9a-zA-Z\-.,;_]','_',filename)
#文件名及父目录不超过255个字节
filename='/'.join(segment[:255] for segment in filename.split('/'))
#添加文件名为绝对路径
return os.path.join(self.cache_dir,filename)
#
def has_expired(self,timestamp):
#判断文件有效期
#datetime.datetime.utcnow代表目前的时刻(世界时间)
#文件的创建时间+有效日期是否小于当前时刻
return datetime.datetime.utcnow() > timestamp +self.expires
def clear(self):
#清除缓存
if os.path.exists(self.cache_dir):
shutil.rmtree(self.cache_dir)
if __name__=='__main__':
import time
start=time.time()
link_crawler('http://example.webscraping.com/', '/view', cache=DiskCache())
print "runing:%s" %(time.time()-start)
第一次爬取
第二次爬取
从运行时间和运行结果来看,第二次没有去下载,而是从磁盘重读取出来,可以看出效率提高了许多.
缺点
- 受限于本地文件系统的限制。
- 一些URl会被映射为相同的文件名。
- 如果文件数量过多的话,会很难实现