requests实现的一个自定义限速,下载深度,保存到(mongodb或文件夹)的通用队列爬虫详解

**

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队列:

在这里插入图片描述

  1. gecco的队列模型是两级队列模型。分为初始请求队列和派生请求队列。初始请求队列在循环模式下是一个阻塞式的FIFO队列,在非循环模式下是一个非阻塞式的FIFO队列。派生队列是一个非阻塞的剔重的FIFO队列;
  2. 线程首先去初始请求队列按照FIFO原则获取一个请求,如果线程数量大于初始请求队列的数量,多余的线程就会待定新的初始请求入队,因此建议线程数量不要大于初始请求队列的数量;
  3. 对于循环模式loop(true),线程在抓取完成后,会将初始请求重新放入队列;
  4. 多线程只对初始请求队列有效,每个线程会有自己的派生请求队列,因此派生请求队列是在单线程下运行的,爬虫将派生请求放入队列继续抓取,直到没有派生请求;
    线程在抓取完成派生请求后,会继续向初始请求队列获取初始请求
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值