继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Python爬虫实战-使用Scrapy框架爬取土巴兔(三)

拉风的咖菲猫
关注TA
已关注
手记 348
粉丝 44
获赞 212

通过上一篇文章Python爬虫实战-使用Scrapy框架爬取土巴兔(二)我们创建了工程目录与完成了基本配置。接下来就要开始做中间件的编写。


    该篇文章主要讲Scrapy中自定义中间件(MIDDLEWARES)的作用与代码编写。

    一.下载器中间件(Downloader Middleware)

    1.下载器中间件的作用

    下载器中间件是介于Scrapy的request/response处理的钩子框架。 是用于全局修改Scrapy request和response的一个轻量、底层的系统。

    2.下载器中间件的配置

    要使用自定义的下载器中间件,需要加入到settings.py文件的DOWNLOADER_MIDDLEWARES配置中:

    DOWNLOADER_MIDDLEWARES = {    'msic.scrapy.middlewares.CustomUserAgentMiddleware': 2,    'tubatu.middlewares.RedirectionMiddleware': 998,
    }if USE_PROXY:
        DOWNLOADER_MIDDLEWARES['msic.scrapy.middlewares.CustomHttpProxyMiddleware'] = 1
        DOWNLOADER_MIDDLEWARES['msic.scrapy.middlewares.CatchExceptionMiddleware'] = 999

    该配置是一个字典类型,键为中间件类的路径,值为其中间件的顺序,值越小越先被调用。

    3.下载器中间件的编写

    爬取土巴兔网站时,土巴兔对IP会有限制,如果一段时间内同一ip访问请求过多。服务端的http响应会直接返回503的error code。所以我们必须编写一些下载器中间件来应对这样的限制。同时我们也需要自己建立一个ip代理池来为爬虫中的http请求维护可用的代理ip。

    • CustomUserAgentMiddleware:在每个http请求的请求头中添加User-Agent。

    • CustomHttpProxyMiddleware:在每个http请求的请求头中添加代理ip,使得Scrapy的下载器在下载网页数据时都是通过代理IP来下载。

    • CatchExceptionMiddleware:如果一个http请求出错或失败了,那么在ip代理池中添加一次该代理ip失败的记录。

    import randomfrom msic.common import log, agentsfrom msic.proxy.proxy_pool import proxy_poolclass CatchExceptionMiddleware(object):
        def process_response(self, request, response, spider):
            if response.status < 200 or response.status >= 400:            try:
                    proxy_pool.add_failed_time(request.meta['proxy'].replace('http://', ''))            except KeyError:                pass
            return response    def process_exception(self, request, exception, spider):
            try:
                proxy_pool.add_failed_time(request.meta['proxy'].replace('http://', ''))        except Exception:            passclass CustomHttpProxyMiddleware(object):
        def process_request(self, request, spider):
            try:
                request.meta['proxy'] = "http://%s" % proxy_pool.random_choice_proxy()        except Exception as e:
                log.error(e)class CustomUserAgentMiddleware(object):
        def process_request(self, request, spider):
            agent = random.choice(agents.AGENTS_ALL)
            request.headers['User-Agent'] = agent
    • RedirectionMiddleware:作用主要是添加一种容错机制,如果我们的代理全部都失效了,http请求数据频繁返回503错误,那么我们直接关闭scrapy,不再去做爬取的动作了。

    from scrapy import Spiderclass RedirectionMiddleware(object):
        ERROR_COUNT = 0
    
        def process_response(self, request, response, spider: Spider):
            if response.status == 302 or response.status == 503:
                self.ERROR_COUNT += 1
                print('错误次数%s' % self.ERROR_COUNT)            if self.ERROR_COUNT > 100:
                    spider.close(spider, 'http status error')        return response    def process_exception(self, request, exception, spider):
            pass

    下载器中间件主要是覆写三个回调方法:

    • process_request(request, spider):当每个request通过下载中间件时,该方法被调用。

    • process_response(request, response, spider):当每个request返回response通过下载中间件时,该方法被调用。

    • process_exception(request, exception, spider):当下载处理器或 process_request()抛出异常(包括 IgnoreRequest 异常)时,该方法被调用。

    二.Spider中间件(Spider Middleware)

    1.Spider中间件的作用

    Spider中间件是介入到Scrapy的spider处理机制的钩子框架,通过它来处理发送给Spiders的response及spider产生的item和request。

    2.Spider中间件的配置

    要使用自定义的下载器中间件,需要加入到settings.py文件的SPIDER_MIDDLEWARES配置中:

    SPIDER_MIDDLEWARES = {    'myproject.middlewares.CustomSpiderMiddleware': 543,
    }

    该配置同样是一个字典类型,键为中间件类的路径,值为其中间件的顺序,值越小越先被调用。

    由于工程中没有使用到,所以不做过多说明,有疑问可以参考官方文档

    三.IP代理池

    不光是土巴兔,很多网站都会都爬虫做自己的限制。限制在一定时间内访问请求过多的IP,所以我们不得不使用ip代理池来保证我们的爬虫能够长时间运作。
    代理池的运行机制:
    我们在爬虫启动时先去获取最新的ip代理。并将ip都存入到数据库中。所以我们在数据库中会存一份ip列表如图1。在scpray在爬取网站数据时http请求可以根据自定义的算法将数据库中可靠的ip做为代理,不能使用本机的ip直接访问目标网站,否则爬取了一段时间你的ip就被封了。

    https://img.mukewang.com/5d58d2940001c54a08920529.png

    图1


    如果你的代理ip质量足够稳定,那么你可能不用在这上面多花心思。但如果我们用的代理ip不够稳定,那么我们就要强化我们的代理池。


    • 首先,获取代理池获取ip是一个定时任务,每过一段时间检查数据库中ip数量是否小于预设的最少ip数,如果小于则自动抓取ip,保证代理池中ip充足。

    • 其次,如果使用某个代理ip在http请求过程中请求失败了,那么在数据库中标记一次它的失败。当某个ip失败次数到预设的最大失败次数时,就把该ip从数据库中删除。每次删除ip都要检查,如果删除ip后数据库中ip数小于预设的最小ip数量就需要再次抓取ip来补充代理池。

    • 最后,我们也要开启一个定时任务来运行代理池ip自检机制,这个自己机制就是通过代理ip来ping数据量很小的网站。如果ping失败了则直接删除该ip。

    下面放上关键代码
    代理IP实体类:

    from msic.common import utilsclass Proxy(object):
        def __init__(self):        self.ip = ''
            self.response_speed = -1
            self.validity = False        self.origin = ''
            self.create_time = ''
            self.update_time = ''
            self.failed_count = 0
    
        @staticmethod    def create(ip, origin):
            proxy = Proxy()
            proxy.ip = ip
            proxy.origin = origin
            proxy.create_time = utils.get_utc_time()  # 格式: 2017-03-18T06:16:26.887Z
            proxy.update_time = proxy.create_time
            proxy.failed_count = 0
            proxy.response_speed = -1
            proxy.validity = False        return proxy

    启动爬虫任务

    def start(self):        #抓取ip代理任务
            self.crawl_proxy_task(False)        def task():            self.check_ip_availability_task()
                schedule = Scheduler()            # 代理池IP自检,60分钟运行一次
                schedule.every(60).minutes.do(self.check_ip_availability_task)            while True:
                    schedule.run_pending()
                    time.sleep(1)
    
            thread = threading.Thread(target=task)
            thread.start()

    抓取IP代理

    def crawl_proxy_task(self, check_num: bool = True):    if check_num:
            count = self.collection.count()        #如果数据库中IP数大于最小ip数则不抓取
            if count > MIN_PROXY_COUNT:
                return
        utils.log("开始抓取代理")    #具体抓取逻辑
        proxy_list = proxy_strategy.crawl_proxy()
        utils.log("开始保存")    for proxy in proxy_list:
            if not self.collection.find_one({'ip': proxy.ip}):            self.collection.insert_one(proxy.__dict__)
                utils.log('保存了:' + proxy.ip)
        utils.log("保存结束")

    代理池自检

    def check_ip_availability_task(self):
        #redis获取上次自检时间,如果未达到设定时间则不在检查
        last_check_time = self.redis_client.get(REDIS_KEY_LAST_CHECK_IP_TIME)
        now_time = datetime.utcnow().timestamp()    if last_check_time is not None and (now_time - float(last_check_time)) < (TASK_INTERVAL * 60):        return
        self.redis_client.set(REDIS_KEY_LAST_CHECK_IP_TIME, now_time)
    
        proxy_list = self.collection.find()    for proxy in proxy_list:
            ip = proxy['ip']
            start_time = time.time()
            response = utils.http_request('http://lwons.com/wx', timeout=10)
            is_success = response.status_code == 200
            response.close()        if not is_success:            #如果请求失败,直接删除IP
                try:
                    self.collection.delete_one({'ip': ip})            except:                pass
                utils.log('Check ip %s FAILED' % ip)        else:            #如果请求成功,在数据库中记录该ip最后响应的时间,下次取ip时优先取出使用
                elapsed = round(time.time() - start_time, 4)            try:
                    self.collection.update_one({'ip': ip},
                                               {"$set": {'update_time': utils.get_utc_time(), 'response_speed': elapsed, 'validity': True}})            except:                pass
                utils.log('Check ip %s SUCCESS' % ip)

    http请求失败,在数据库中处理请求失败的IP

    def add_failed_time(self, ip):
        proxy = self.collection.find_one({'ip': ip})    if proxy is not None:
            failed_count = proxy['failed_count'] + 1
            utils.log("ip: %s 失败次数+1 已失败次数%s次" % (ip, failed_count))        if failed_count <= FAILED_COUNT_BORDER:            #如果未达到最大失败次数,则在数据库中添加一次失败
                try:
                    self.collection.update_one({'ip': ip}, {"$set": {'update_time': utils.get_utc_time(), 'failed_count': failed_count}})            except:                pass
            else:            #达到最大失败次数,则在数据库中删除
                try:
                    self.collection.delete_one({'ip': ip})            except:                pass
        #检查数据库中IP是否足够
        self.crawl_proxy_task()

    Scrapy的中间件在取出代理池中IP时,优先取出失败次数少,最近http响应成功的有效IP

    def random_choice_proxy(self) -> str:
        proxy = self.collection.find().sort(
            [("failed_count", pymongo.ASCENDING), ("validity", pymongo.DESCENDING), ("response_speed", pymongo.ASCENDING),
             ("update_time", pymongo.DESCENDING)])    return proxy[0]['ip']

    最后

    爬虫的下载中间件和代理池都已创建完毕,接下来就要编写具体的爬取规则Python爬虫实战-使用Scrapy框架爬取土巴兔(四)

    附:

    详细的项目工程在Github中,如果觉得还不错的话记得Star哦。



    作者:imflyn
    链接:https://www.jianshu.com/p/d0462dc6a7e0


    打开App,阅读手记
    0人推荐
    发表评论
    随时随地看视频慕课网APP