主要代码:
import random
import requests
from fake_useragent import UserAgent
from retrying import retry #重置下载
import hashlib #信息摘要算法 md5
import queue #队列
import re #正则
from urllib import robotparser #解析网站的robots.txt文件
from urllib.parse import urlparse,urljoin,urldefrag #解析url
from threading import Thread #多线程
from datetime import datetime
import time
import mongo_cache
MAX_DEP = 2 #定义爬虫爬取深度
def get_robots(url):
"""
解析robots.txt 文件
:param url:
:return:
"""
rp = robotparser.RobotFileParser()
rp.set_url(urljoin(url,'robots.txt'))
rp.read()
return rp
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/" + gen_html_name(url_str) + ".html"
with open(file_path,"wb") as f:
f.write(html_content)
def gen_html_name(url_str):
#获取域名后面的部分 urlparse('http://www.baidu.com/a/s/d').path '/a/s/d'
path = urlparse(url_str).path
#按照/切割 array:数组
path_array = path.split('/')
#取出最后一个
return path_array[len(path_array)-1]
#extractor:抽取,提取
def extractor_url_lists(html_content):
"""
抽取网页中的其他链接
:param html_content:
:return:
"""
#复习
# ^:[]外面是以什么开头的意思,在[]里面时是 非 的意思
# [^>] a href 之间不能有 > ,不是>的都可以匹配
#["\'] " ' 都能匹配,\ 转义
# (.*?) 匹配任意个任意字符 ? :取消贪婪匹配 ():把匹配的内容获取出来 分组
#regex:正则
url_regex = re.compile('<a[^>]+href=["\'](.*?)["\']',re.IGNORECASE)
return url_regex.findall(html_content)
class CrawlerCommon(Thread):
"""
实现一个通用爬虫,涵盖基本的爬虫功能
"""
def __init__(self,init_url):
#py2.7的写法
# super(CrawlerCommon,self).__init__()
#py3.x 的写法 强制调用父类函数
super().__init__()
__ua = UserAgent() #随机User-Agent
self.seed_url = init_url #初始爬取的种子网址
self.crawler_queue = queue.Queue() #使用不同的队列会造成BFS和DFS的效果
self.crawler_queue.put(init_url) #将种子网址放入队列
self.visited = {init_url : 0} #初始化爬取深度为0 visited :访问
self.rp = get_robots(init_url) #初始化robots解析器
self.headers = {"User-Agent":__ua.random} #生成一个随机的User-Agent
self.link_regex = '(index|view)' #抽取网址的过滤条件 link:链接
#跳到限流器__init__
self.throttle = Throttle(3.0) #下载限流器间隔5秒
self.mcache = mongo_cache.MongoCache() #初始化 mongo_cache
# self.time_sleep = 2
#attempt:尝试
@retry(stop_max_attempt_number=3)
def retry_download(self,url_str,data,method,proxies):
"""
retry:重试
使用装饰器的重试下载类
:param url_str:
:param data:
:param method:
: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)
assert result.status_code == 200 #此处为断言
return result.content
#dir() 看到类内部的实现方法
def download(self,url_str,data=None,method="GET",proxies={}):
"""
真正的下载类
:param url_str:
:param data:
: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:
print(e.message)
result = None
return result
def nomalize(self,url_str):
"""
补全下载链接
:param url_str:
:return:
"""
#打散网址 urldefrag:截取URL中#前面的网址
real_url,_ = urldefrag(url_str)
#拼接路径
return urljoin(self.seed_url,real_url)
def save_result(self,html_content,url_str):
"""
将结果存入数据库,存入前检查内容是否存在
:param html_content: 下载的二进制内容
:param url_str: 下载网页的url
:return:
"""
if url_str not in self.mcache:
self.mcache[url_str] = html_content
else:
data_from_mongo = self.mcache[url_str]
#初始化md5算法
md5_func_mongo = hashlib.md5()
md5_func_download = hashlib.md5()
#生成数据库记录的md5摘要
md5_func_mongo.update[data_from_mongo]
mongo_md5_str = md5_func_mongo.hexdigest()
md5_func_download.update(html_content)
download_md5_str = md5_func_download.hexdigest()
if download_md5_str != mongo_md5_str:
self.mcache[url_str] = html_content
#高内聚 低耦合
def run(self):
"""
进行网页爬取的主要方法
:return:
"""
#判断队列是否为空
while not self.crawler_queue.empty():
#将网址从队列中取出来
url_str = self.crawler_queue.get()
#检测robots.txt文件规则
# 改为if True: 直接下载
if self.rp.can_fetch(self.headers["User-Agent"],url_str):
self.throttle.wait_url(url_str)
depth = self.visited[url_str]
if depth < MAX_DEP:
#下载链接
html_content = self.download(url_str)
#存储链接
if html_content is not None:
#存库
self.mcache[url_str] = html_content
save_url(html_content,url_str)
else:
continue
print(self.mcache[url_str])
#筛选出页面所有链接
url_list = extractor_url_lists(html_content.decode('utf8'))
#筛选出需要爬去的连接 self.link_refex
filter_urls = [link for link in url_list if re.search('/(html)',link)]
for url in filter_urls:
#不全连接
real_url = self.nomalize(url)
#判断连接是否访问过
if real_url not in self.visited:
self.visited[real_url] = depth + 1
self.crawler_queue.put(real_url)
else:
print("robots.txt 禁止下载:",url_str)
#随机代理
class RandomProxy(object):
"""
随机代理
"""
def __init__(self):
self.proxies = {}
self.headers = {
"User-Agent":"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0"
}
def crawl_proxies(self):
"""
抓取生成代理
:return:
"""
self.proxies.append('114.223.163.70')
self.proxies.append('125.40.79.66')
def verify_proxies(self):
"""
校验每一个代理是否可用
:return:
"""
invalid_ip = {}
for ip_str in self.proxies:
proxies = {"http": ip_str}
r = requests.get("", proxies=proxies, headers=self.headers)
if r.status_code == 200:
continue
else:
invalid_ip.append(ip_str)
for remove_ip in invalid_ip:
self.proxies.remove(remove_ip)
def get_one_proxy(self):
return random.choice(self.proxies)
class Throttle(object):
"""
下载限流器 throttle:限流 time.sleep()
"""
def __init__(self,delay):
#domains:域名
self.domains = {}
#两次下载间隔时间
self.delay = delay
def wait_url(self,url_str):
#获取到爬取的网址的域名 net location :网络位置
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加上随机偏移量
#记录上次爬取到这次的时间间隔 interval:间隔 seconds:转化为秒
sleep_interval = self.delay - (datetime.now() - last_accessed).seconds
#如果时间间隔大于0休眠,否则直接下载
if sleep_interval > 0:
#设置一个随机的偏移量
time.sleep(sleep_interval)
#记录爬取这个链接的时间戳 域名为key,当前时间为value 存到domains字典中
self.domains[domain_url] = datetime.now()
if __name__ == '__main__':
#1,调用器构造方法
crawler = CrawlerCommon("https://www.runoob.com/html/html5-intro.html")
#调用run()方法
crawler.start()
连接数据库代码:
import pickle
import zlib
from datetime import datetime,timedelta
from pymongo import MongoClient
from bson.binary import Binary
class MongoCache(object):
"""
数据库缓存
"""
#timedelta 时间间隔
def __init__(self,client=None,expires=timedelta(days=30)):
self.client = MongoClient("localhost",27017)
self.db = self.client.cache
#加速查找,设置索引,设置超时时间 timestamp:时间戳
# 如果达到 expireAfterSeconds 设置的时间,mongodb会把超时数据自动删除
self.db.webpage.create_index('timestamp',expireAfterSeconds=expires.total_seconds())
#__setitem__ : 每当属性被赋值时调用该方法,不能在该方法内赋值,会造成死循环
def __setitem__(self, key, value):
#三步压缩
record = {"result":Binary(zlib.compress(pickle.dumps(value))),"timestamp":datetime.utcnow()}
#upsert:不存在插入,存在更新 $set:强制覆盖原始数据,mongodb的内置函数(带$)
self.db.webpage.update({"_id":key},{'$set':record},upsert=True)
#__getitem__:当访问不存在的属性时会调用该方法
def __getitem__(self, item):
#根据id 以item作为关键字(例如:url:http://www.baidu.com)查找相关网页
record = self.db.webpage.find_one({"_id":item})
if record:
#解压缩,反序列化
return pickle.loads(zlib.decompress(record["result"]))
else:
#模拟找不到关键字,抛出异常
raise KeyError(item + "does not exist")
#当使用 in ,not in 对象时调用
def __contains__(self, item):
try:
#执行 __getitem__ 方法
self[item]
except KeyError:
#捕获到keyError 异常,说明没找到相关数据,参考3行抛出异常的条件
return False
else:
#找到相应数据说明数据库包含下载内容
return True
def clear(self):
#清空缓存库
self.db.webpage.drop()
mo = MongoCache()