**
1,自定义mongo_cache包
**
代码如下:
import pickle
import zlib
from datetime import timedelta, datetime
from pymongo import MongoClient
from bson.binary import Binary
class MongoCache:
def __init__(self,client=None,expires=timedelta(30)):
self.client = MongoClient('localhost',27017)
self.db = self.client.cache
# 设置索引,设置超时时间,到设置的超时的时间会自动删除
self.db.webpage.create_index('timestamp',expireAfterSeconds=expires.total_seconds())
def __setitem__(self, key, value):
# 压缩数据,设置时间戳
record = {'result':Binary(zlib.compress(pickle.dumps(value))),'timestamp':datetime.utcnow()}
# 使用update,upsert表示没有就插入
self.db.webpage.update({'_id':key},{'$set':record},upsert=True)
def __getitem__(self,item):
# 根据_id以itme为关键字相关网页的内容
record = self.db.webpage.find_one({'_id':item})
if record:
return pickle.loads(zlib.decompress(record['result']))
else:
# 找不到破抛出异常
raise KeyError(item + "does not exist")
def __contains__(self, item):
try:
self[item]#这里会调用__getitem__方法
except KeyError:
return False#捕获到keyErrory异常说明没有找到相关数据
else:
return True#找到相应数据说明数据库包含下载内容
def clear(self):
self.db.webpage.drop()
if __name__ == '__main__':
cache = MongoCache()
#以下是测试代码:
# a = cache1['http://www.runoob.com/mongodb/mongodb-limit-skip.html']
#保存
# with open('cache.html', 'w+',encoding='utf-8')as fp:
# fp.write(a.decode('utf-8'))
# 删除cache库
# cache.clear()
2,使用自己定义的mongo_cache进行对菜鸟教程相关网站的爬取
代码如下:
import requests
#伪造用户代理
from fake_useragent import UserAgent
#失败时重试下载
from retrying import retry
#实现信息摘要算法
import hashlib
#解析网站robots.txt文件(robots.txt如:https://www.bilibili.com/robots.txt)
from urllib import robotparser
#解析url内容
from urllib.parse import urldefrag, urljoin, urlparse
import re
#队列
import queue
from datetime import datetime
import time
import random
#多线程
from threading import Thread
#这里时自己定义的mongo_cache,如果配置环境里下载了可能会出错,可以进行将mongo_cache文件名更改
import mongo_cache
#定义爬虫爬取的深度
MAX_DEP = 2
def extractor_url_lists(html_content):
"""
抽取网页中其他链接
:param html_content: 网页内容
:return: 返回该页面所有网址
"""
url_regex = re.compile('<a[^>]+href=["\'](.*?)["\']', re.IGNORECASE)
#'<a[^>]+href=["\'](.*?)["\']'中[^>]表示匹配到非>,中["\']表示可以“也可以‘
return url_regex.findall(html_content)
#匹配所有符合的url
def get_robots(url):
"""
解析robots.txt文件
:param url: 初始化的时候传递的种子链接
:return:
"""
rp = robotparser.RobotFileParser() # 创建一个解析robots.txt文件的对象
robots_url = "http://" + urlparse(url).netloc # 从种子链接中提取出域名,加上http拼接成完整的域名
#urlparse(https://www.bilibili.com).netloc=“www.bilibili.com”
rp.set_url(urljoin(robots_url, 'robots.txt')) # rp.set_url(urljoin(url,'robots.txt')) #拼接成完整的robots.txt的url
#urljoin('https://www.bilibili.com', 'robots.txt')=‘https://www.bilibili.com/robots.txt’
rp.read()
# rp.can_fetch("http://www.baidu.com",user-agent)
return rp # 返回一个解析robots.txt的对象(对象中包含url属性)
def save_url(html_content, url_str):
"""
存储下载内容
:param html_content:网页的源码
:param url_str:下载的链接
:return:
"""
md5 = hashlib.md5()
md5.update(html_content)
# file_path = "./download/" +md5.hexdigest() + ".html"
file_path = "./download/" + get_html_name(url_str)
#找到同级目录下的download文件然后调用 get_html_name此函数得到爬取网页的url中域名后的名子为文件名
with open(file_path, 'wb') as f:
f.write(html_content)
def get_html_name(url_str):
path = urlparse(url_str).path # 从爬取网页的url中获取域名后的path名
path_array = path.split('/') # 因为path第一个字符为/,所以切割选择第二个元素作为文件名
return path_array[len(path_array) - 1]
class CrawlerCommon(Thread):
"""
通用下载爬虫
"""
def __init__(self, init_url):
"""
初始化方法
"""
super(CrawlerCommon, self).__init__()
__ua = UserAgent()
# 使用fake-useragent随机生成一个访问头
self.headers = {"User-Agent": __ua.random}
# 设置最初抓取的种子网站,定义的方法里面有init_url的参数
self.seed_url = init_url
# 使用不同的队列会造成BFS和DFS的效果
# 使用先进先出队列产生广度优先效果,使用先进后出(栈)产生深度优先效果
self.crawler_queue = queue.Queue() # 创建一个队列,用来存放待爬取的链接
self.crawler_queue.put(init_url) # 将种子网站放入初始队列
#
self.rp = get_robots(init_url) # 获取robots.txt文件的url
self.link_regex = '(index|view)' #链接上面的正则表达式
self.throttle = Throttle(3.0) # 限制下载的时间间隔为3秒
self.visited = {init_url: 0} #表示爬去的此url为第0层
@retry(stop_max_attempt_number=3)
#使用retry当爬取失败时,在最多重新进行爬去3次
def retry_download(self, url_str, data, method, proxies):
"""
通过装饰器封装重试下载模块,最多重试三次
:param url_str: 下载网页的最终地址
:param data: Post传输数据
:param method: 下载方法GET或POST
:param proxies: 代理服务器
:return: 下载结果
"""
if method == "POST":
result = requests.post(url_str, data=data, headers=self.headers, proxies=proxies)
else:
result = requests.get(url_str, headers=self.headers, timeout=3,
proxies=proxies) # timeout设置超过规定秒数之后不再等待响应,超时会报异常
assert result.status_code == 200 # 使用断言判断下载状态,成功则返回结果,失败抛出异常
return result.content
def download(self, url_str, data=None, method="GET", proxies={}):
"""
真正的下载类,代理模式
:param url_str:下载的链接
:param data:post需要传输的数据
:param method:请求方法
:param proxies:代理
:return:下载的结果
"""
print("download url is:", url_str)
try:
result = self.retry_download(url_str, data, method, proxies)
#调用重新下载的函数
except Exception as e: # 异常处理尽量使用具体的异常
result = None
return result
def nomalize(self, url_str):
"""
补全下载链接
实现一个类方法的时候,要注意类方法是否使用了当前类的属性或其他方法
如果没有使用说明和当前类没有直接关系,最好独立出来,当做工具方法
:param url_str:
:return:
"""
# urldefrag处理网址#后的转发(去掉网址后面的#号)
real_url, _ = urldefrag(url_str) # 网站内的相对网址(没有域名)
# real_url, _=urldefrag('https://www.bilibili.com/?name=zxf#name')则real_url= 'https://www.bilibili.com/?name=zxf'
return urljoin(self.seed_url, real_url) # 将下载地址拼接上网站前缀,将域名后的相对网址拼接到域名之后
def run(self):
"""
进行网页爬取的主要方法
:return:
"""
while not self.crawler_queue.empty(): # 当队列不为空时(能取出网址,在进行操作)
url_str = self.crawler_queue.get() # 获取队列的第一条(第一条在初始化时保存为种子链接)
# 检测robots.txt规则(是否被禁止访问)
if self.rp.can_fetch(self.headers["User-Agent"], url_str):
#上面的get_robots中可以rp.can_fetch("http://www.baidu.com",user-agent)在此中因为上面方法中定义了self.headers = {"User-Agent": __ua.random}所以if里面这样写
self.throttle.wait_url(url_str) # 通过下载限流器判断爬取该页面是否需要等待
depth = self.visited[url_str] # 获取爬取的url的深度,当爬取到第二层深度的时候,depth就会更新为1
if depth < MAX_DEP:
# 下载链接
html_content = self.download(url_str)
# 存储链接
if html_content is not None:
# self.mcache[url_str] = html_content # 保存到mongodb数据库
save_url(html_content, url_str) # 将页面源码和完整的链接传到保存文件的函数中
# 筛选出下一深度页面的链接
url_list = extractor_url_lists(
str(html_content, encoding='utf-8')) # 正则表达式需要匹配字符串,但是内容是字节流,所以需要先把内容编码
# 筛选需要爬取的链接
filter_urls = [link for link in url_list if
re.search('/(mongodb)', link)] # 使用正则表达式匹配网址中含有/mongodb的网址
for url in filter_urls:
# 补全链接
real_url = self.nomalize(url) # 将每一个抽取到的相对网址补全为http:// + 域名 + 相对网址的格式
# 判断链接是否访问过
if real_url not in self.visited:
# 将每一个页面中的下一层url链接的depth都加一,这样每一层都会对应一个depth
self.visited[real_url] = depth + 1
self.crawler_queue.put(real_url) # 将所有抽取出来的链接添加到队列中待爬取
else:
print("robots.txt 禁止下载:", url_str)
class Throttle:
"""
下载限流器
"""
def __init__(self, delay):
# 保存每个爬取过的链接与对应爬取时间的时间戳
self.domains = {} # 可以放到mongodb数据库中(在最后一句)
self.delay = delay # 两次下载间隔时间
#delay与上面CrawlerCommon类中self.throttle = Throttle(3.0) 中的3.0对应
def wait_url(self, url_str):
# 以netloc为基础进行休眠
domain_url = urlparse(url_str).netloc # 获取到爬取的链接的域名
last_accessed = self.domains.get(domain_url) # 获取上次爬取链接的时间戳(时间戳与域名对应,爬取该域名的网站之后更新为最新的时间戳)
# 爬取的条件为上次爬取的时间戳不为空(上次爬取过,如果没有爬取则把这个域名和当前时间戳保存到字典)
if self.delay > 0 and last_accessed is not None:
# 计算当前时间和上次访问时间间隔
# sleep_interval加上随机偏移量
sleep_interval = self.delay - (datetime.now() - last_accessed).seconds # 记录上次爬取到这次的时间间隔
# 如果时间间隔尚未达到规定的时间间隔,则需要等待
if sleep_interval > 0:
time.sleep(sleep_interval + round(random.uniform(1, 3), 1)) # 设置一个随机的偏移量(最后的一个1表示保留一位小数)
self.domains[domain_url] = datetime.now() # 记录爬取这个链接的时间戳
if __name__ == '__main__':
crawler = CrawlerCommon('http://www.runoob.com/mongodb/mongodb-intro.html')
crawler.run()
关于 queue队列:
- gecco的队列模型是两级队列模型。分为初始请求队列和派生请求队列。初始请求队列在循环模式下是一个阻塞式的FIFO队列,在非循环模式下是一个非阻塞式的FIFO队列。派生队列是一个非阻塞的剔重的FIFO队列;
- 线程首先去初始请求队列按照FIFO原则获取一个请求,如果线程数量大于初始请求队列的数量,多余的线程就会待定新的初始请求入队,因此建议线程数量不要大于初始请求队列的数量;
- 对于循环模式loop(true),线程在抓取完成后,会将初始请求重新放入队列;
- 多线程只对初始请求队列有效,每个线程会有自己的派生请求队列,因此派生请求队列是在单线程下运行的,爬虫将派生请求放入队列继续抓取,直到没有派生请求;
线程在抓取完成派生请求后,会继续向初始请求队列获取初始请求