前面已经实现了一个具有数据爬取的爬虫。如果新增需求还要再抓取另一个字段,比如前面的爬虫中要求增加一个国旗图片的url,又该怎么做呢?想要抓取新增的字段,需要重新下载整个需要爬取的网站。对于小型网站来说,可能不算特别大的问题,但是对于那些百万级数据的网站而言,重新下载可能需要耗费很长一段时间。因此,对已爬取的网页进行缓存的方案可以让每个网页只下载一次。
为链接爬虫添加缓存支持
修改上一个爬虫中的downloader.py中的download函数,使其在开始下载网页之前先进性缓存检查,还需要把限速的功能移植到函数内部,这样只有在发生真正下载的时候才会触发限速,而通过加载缓存则不会触发。为了避免每次下载都需要传入多个参数,将download函数重构为一个类,这样参数只需要在构造方法中设置一次,就能在后续下载中多次复用。代码如下:
# downloader.py
import urllib.request
import urllib.parse
import urllib.error
from .Throttle import Throttle
import socket
import random
DEFAULT_AGENT = 'wswp'
DEFAULT_DELAY = 5
DEFAULT_RETRIES = 1
DEFAULT_TIMEOUT = 60
class Downloader:
def __init__(self,delay=DEFAULT_DELAY,userAgent=DEFAULT_AGENT,proxies=None,numRetries=DEFAULT_RETRIES, timeOut=DEFAULT_TIMEOUT, opener=None,cache=None):
socket.setdefaulttimeout(timeOut)
self.throttle = Throttle(delay)
self.userAgent = userAgent
self.proxies = proxies
self.numRetries = numRetries
self.opener = None
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.numRetries >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.userAgent}
result = self.download(url, headers, proxy=proxy,numRetries=self.numRetries)
if self.cache:
# save result to cache
self.cache[url] = result
return result['html']
def download(self, url, headers, proxy, numRetries, data=None):
print("正在下载:", url)
request = urllib.request.Request(url, data, headers)
opener = self.opener or urllib.request.build_opener()
if proxy:
proxyParams = {urllib.parse.urlparse(url).scheme: proxy}
opener.add_handler(urllib.request.ProxyHandler(proxyParams))
try:
response = opener.open(request)
html = response.read()
code = response.code
except urllib.error.URLError as e:
print('下载错误:', e.reason)
html = ''
if hasattr(e, 'code'):
code = e.code
if numRetries > 0 and 500 <= code < 600:
return self.download(url, headers, proxy, numRetries - 1, data)
else:
code = None
return {'html':html,'code':code}
上面的代码中Downloader类中有一个比较有意思的部分,那就是__call__特殊方法,在该方法中实现了下载前检查缓存的功能。首先会检查缓存是否已经定义,如果已经定义,则检查之前是否已经缓存了该url,如果已经缓存,则检查之前的下载中是否遇到了服务器端错误。如果检测到上述检查中的任何一项失败,都需要重新下载该网页,然后将缓存结构添加到缓存中。这里的download方法参数和之间的download函数基本一致,只是在返回下载的html的时候多加了一个状态码,该方法也可以直接调用。proxies是一个代理列表,通过随机获取的方式来绕过一定的反爬虫机制。
对于cache类,可以通过调用result=cache[url]从cache中加载数据,并通过cache[url] = resutl向cache中保存结果。这种便捷的接口写法也是Python中字典数据类型的使用方式。为了支持该接口,cache类需要定义__getitem__()和__setitem__()这两个特殊的类方法。
除此之外,为了支持缓存功能,链接爬取模块的代码也要进行一些微调,包括添加cache参数、移除限速以及将download函数替换为新的类等。代码如下所示:
# downloader.py
def linkCrawler(seedUrl, linkRegex=None, delay=5, maxDepth=-1, maxUrls=-1, headers=None, userAgent='wswp', proxies=None, numRetries=1, scrapeCallBack=None,cache=None):
"""
Crawl from the given seed URL following links matched by linkRegex
:param seedUrl: 起始url
:param linkRegx: 链接匹配的正则表达式
:param delay: 延迟时间
:param maxDepth: 最深的层次
:param maxUrls: 最多的url数量
:param headers: http请求头
:param userAgent: http头中的userAgent选项
:param proxy: 代理地址
:param numRetries: 重新下载次数
:return:
"""
crawlQueue = deque([seedUrl])
seen = { seedUrl:0}
numUrls = 0
rp = getRobots(seedUrl)
Down = Downloader(delay=delay,userAgent=userAgent,proxies=proxies,numRetries=numRetries,cache=cache)
while crawlQueue:
url = crawlQueue.pop()
if rp.can_fetch(userAgent, url):
html = Down(url)
links = []
if scrapeCallBack:
links.extend(scrapeCallBack(url, html) or [])
depth = seen[url]
if depth != maxDepth:
if linkRegex:
links.extend(link for link in getLinks(html) if re.match(linkRegex, link))
for link in links:
link = normalize(seedUrl, link)
if link not in seen:
seen[link] = depth + 1
if sameDomain(seedUrl, link):
crawlQueue.append(link)
numUrls += 1
if numUrls == maxUrls:
break
else:
print('Blocked by robots.txt',url)
到目前为止,这个网络爬虫的基本架构已经搭建好了,接下来就要开始构建实际的缓存了。