爬虫框架开发之框架功能升级

框架功能升级
介绍
前面的内容,我们实现了一个功能相对完善的爬虫框架,但是还不够强大,对于一些功能比如分布式,断点续爬等功能任然没有实现,那么接下来在框架功能升级这一部分,我们继续来完善框架

内容
分布式爬虫的支持
增量爬虫的设计支持
断点续爬的设计支持

3.4.1框架升级 – 分布式爬虫设计原理及其实现
目标
理解分布式爬虫的原理
理解如何通过redis构建一个队列
完成代理的重构,实现分布式

  1. 分布式爬虫原理
    分布式爬虫设计原理:

多台服务器同时抓取数据,请求和指纹存储在同一个redis中

  1. 实现方案
    利用redis实现队列

注意pickle模块的使用:如果将对象存入redis中,需要先将其序列化为二进制数据,取出后反序列化就可以再得到原始对象
接口定义一致性:利用redis使用一个Queue,使其接口同python的内置队列接口一致,可以实现无缝转换

scrapy_plus/queue.py

import time
import pickle

import redis
from six.moves import queue as BaseQueue

redis队列默认配置

REDIS_QUEUE_NAME = ‘request_queue’
REDIS_QUEUE_HOST = ‘localhost’
REDIS_QUEUE_PORT = 6379
REDIS_QUEUE_DB = 10

利用redis实现一个Queue,使其接口同python的内置队列接口一致,可以实现无缝转换

class Queue(object):
“”"
A Queue like message built over redis
“”"

Empty = BaseQueue.Empty
Full = BaseQueue.Full
max_timeout = 0.3

def __init__(self, maxsize=0, name=REDIS_QUEUE_NAME, host=REDIS_QUEUE_HOST, port=REDIS_QUEUE_PORT, db=REDIS_QUEUE_DB,
            lazy_limit=True, password=None):
    """
    Constructor for RedisQueue
    maxsize:    an integer that sets the upperbound limit on the number of
                items that can be placed in the queue.
    lazy_limit: redis queue is shared via instance, a lazy size limit is used
                for better performance.
    """
    self.name = name
    self.redis = redis.StrictRedis(host=host, port=port, db=db, password=password)
    self.maxsize = maxsize
    self.lazy_limit = lazy_limit
    self.last_qsize = 0

def qsize(self):
    self.last_qsize = self.redis.llen(self.name)
    return self.last_qsize

def empty(self):
    if self.qsize() == 0:
        return True
    else:
        return False

def full(self):
    if self.maxsize and self.qsize() >= self.maxsize:
        return True
    else:
        return False

def put_nowait(self, obj):
    if self.lazy_limit and self.last_qsize < self.maxsize:
        pass
    elif self.full():
        raise self.Full
    self.last_qsize = self.redis.rpush(self.name, pickle.dumps(obj))
    return True

def put(self, obj, block=True, timeout=None):
    if not block:
        return self.put_nowait(obj)

    start_time = time.time()
    while True:
        try:
            return self.put_nowait(obj)
        except self.Full:
            if timeout:
                lasted = time.time() - start_time
                if timeout > lasted:
                    time.sleep(min(self.max_timeout, timeout - lasted))
                else:
                    raise
            else:
                time.sleep(self.max_timeout)

def get_nowait(self):
    ret = self.redis.lpop(self.name)
    if ret is None:
        raise self.Empty
    return pickle.loads(ret)

def get(self, block=True, timeout=None):
    if not block:
        return self.get_nowait()

    start_time = time.time()
    while True:
        try:
            return self.get_nowait()
        except self.Empty:
            if timeout:
                lasted = time.time() - start_time
                if timeout > lasted:
                    time.sleep(min(self.max_timeout, timeout - lasted))
                else:
                    raise
            else:
                time.sleep(self.max_timeout)

2.通过配置文件选择是否启用分布式:

项目文件夹/settings.py

设置调度器的内容是否要持久化

量个值:True和False

如果是True,那么就是使用分布式,同时也是基于请求的增量式爬虫

如果是False, 不会说那个redis队列,会使用python的set存储指纹和请求

SCHEDULER_PERSIST = False
3. 修改调度器实现请求对象的持久化
#scrapy_plus/core/scheduler.py
from six.moves.queue import Queue
from scrapy_plus.utils.queue import Queue as ReidsQueue
from scrapy_plus.conf.settings import SCHEDULER_PERSIST

class Scheduler:

def __init__(self,collector):

    self._filter_container = set()
    if SCHEDULER_PERSIST: #如果使用分布式或者是持久化,使用redis的队列
        self.queue = ReidsQueue()

    else:
        self.queue = Queue()

    self.repeate_request_num = 0

需要实现分布式,除了请求对象需要存储在redis中,还需要对请求进行去重,包括自动退出判断问题(分布式的话通常不需要自动退出)

  1. 利用Redis的集合类型实现去重
    如果分布式中请求去重的去重容器各个服务器用的不是同一个,那么就无法达到去重的目的,因此这里同样的需要使用redis来实现去重容器,也就是把所有的去重指纹都存储在redis中

实现一个自定义的set:

scrapy_plus/set.py

import redis
from scrapy_plus.conf import settings

class BaseFilterContainer(object):

def add_fp(self, fp):
    '''往去重容器添加一个指纹'''
    pass

def exists(self, fp):
    '''判断指纹是否在去重容器中'''
    pass

class NoramlFilterContainer(BaseFilterContainer):
‘’‘利用python的集合类型’’’

def __init__(self):
    self._filter_container = set()

def add_fp(self, fp):
    ''''''
    self._filter_container.add(fp)

def exists(self, fp):
    '''判断指纹是否在去重容器中'''
    if fp in self._filter_container:
        return True
    else:
        return False

class RedisFilterContainer(BaseFilterContainer):

REDIS_SET_NAME = settings.REDIS_SET_NAME
REDIS_SET_HOST = settings.REDIS_SET_HOST
REDIS_SET_PORT = settings.REDIS_SET_PORT
REDIS_SET_DB = settings.REDIS_SET_DB

def __init__(self):
    self._redis = redis.StrictRedis(host=self.REDIS_SET_HOST, port=self.REDIS_SET_PORT ,db=self.REDIS_SET_DB)
    self._name = self.REDIS_SET_NAME

def add_fp(self, fp):
    '''往去重容器添加一个指纹'''
    self._redis.sadd(self._name, fp)

def exists(self, fp):
    '''判断指纹是否在去重容器中'''
    return self._redis.sismember(self._name, fp)

在调度器中使用这个set.py, 使得分布式模式下的去重功能正常运作

scrapy_plus/core/scheduler.py

from hashlib import sha1

import w3lib.url
from six.moves.queue import Queue
from scrapy_plus.conf import settings
from scrapy_plus.queue import Queue as RedisQueue
from scrapy_plus.set import NoramlFilterContainer, RedisFilterContainer
from scrapy_plus.utils.log import logger

class Scheduler(object):
‘’’
1. 缓存请求对象(Request),并为下载器提供请求对象,实现请求的调度
2. 对请求对象进行去重判断
‘’’

def __init__(self,collector):

    if SCHEDULER_PERSIST: #如果使用分布式或者是持久化,使用redis的队列
        self.queue = ReidsQueue()
        self._filter_container = RedisFilterContainer() #使用redis作为python的去重的容器
    else: #如果不适用分布式或者持久化,使用python的set作为去重的容器
        self.queue = Queue()
        self._filter_container = NoramlFilterContainer()
    self.repeate_request_num = 0

def _filter_request(self, request):
    # 去重容器:存储已经发过的请求的特征 url    选用集合类型:set()
    # 利用请求的url method data  求出一个指纹  利用sha1

    request.fp = self._gen_fp(request) #把指纹作为request的一个属性
    if not self._filter_container.exists(request.fp):
        self._filter_container.add_fp(request.fp)
        # logger.info("添加新的请求:<%s>" % request.url)
        return True
    else:
        logger.info("发现重复的请求:<%s>" % request.url)
        self.repeate_request_num +=1
        return False
  1. 程序结束的条件
    在之前的单机版本的代码中,通过:总的响应+总的重复数>=总的请求来判断程序结束,但是在分布式的版本那种,每个服务器的请求数量和响应数量不在相同

因为每个服务器存入队列的请求,和成功发送的请求中间可能很多请求被其他的服务器发送了,导致数量不一致,所以可以把总的请求,总的响应,总的重复等信息记录在redis中,那么所有的服务端修改的数据的位置是同一个redis中的内容,所有的服务端判断退出的时候也是通过比较同一个redis中的这些数据来决定

此时,在utils中新建stats_collector.py文件,来实现对各种数量的统计,包括总的请求数量,总的响应数量,总的重复数量

import redis
from scrapy_plus.conf.settings import REDIS_QUEUE_NAME, REDIS_QUEUE_HOST, REDIS_QUEUE_PORT, REDIS_QUEUE_DB

redis队列默认配置

REDIS_QUEUE_NAME = ‘request_queue’

REDIS_QUEUE_HOST = ‘localhost’

REDIS_QUEUE_PORT = 6379

REDIS_QUEUE_DB = 10

class StatsCollector(object):

def __init__(self, spider_names=[], host=REDIS_QUEUE_HOST, port=REDIS_QUEUE_PORT, \
             db=REDIS_QUEUE_DB, password=None):

    self.redis = redis.StrictRedis(host=host, port=port, db=db, password=password)
    #存储请求数量的键
    self.request_nums_key = "_".join(spider_names) + "_request_nums"
    #存储响应数量的键
    self.response_nums_key = "_".join(spider_names) + "_response_nums"
    #存储重复请求的键
    self.repeat_request_nums_key = "_".join(spider_names) + "_repeat_request_nums"
    #存储start_request数量的键
    self.start_request_nums_key = "_".join(spider_names) + "_start_request_nums"

def incr(self, key):
    '''给键对应的值增加1,不存在会自动创建,并且值为1,'''
    self.redis.incr(key)

def get(self, key):
    '''获取键对应的值,不存在是为0,存在则获取并转化为int类型'''
    ret = self.redis.get(key)
    if not ret:
        ret = 0
    else:
        ret = int(ret)
    return ret

def clear(self):
    '''程序结束后清空所有的值'''
    self.redis.delete(self.request_nums_key, self.response_nums_key, \
                      self.repeat_request_nums_key, self.start_request_nums_key)

@property
def request_nums(self):
    '''获取请求数量'''
    return self.get(self.request_nums_key)

@property
def response_nums(self):
    '''获取响应数量'''
    return self.get(self.response_nums_key)

@property
def repeat_request_nums(self):
    '''获取重复请求数量'''
    return self.get(self.repeat_request_nums_key)

@property
def start_request_nums(self):
    '''获取start_request数量'''
    return self.get(self.start_request_nums_key)

修改engine.py

coding=utf-8

import datetime
import time
import importlib
from scrapy_plus.conf.settings import SPIDERS, PIPELINES,
SPIDER_MIDDLEWARES, DOWNLOADER_MIDDLEWARES, MAX_ASYNC_NUMBER,ASYNC_TYPE

if ASYNC_TYPE == “coroutine”:
from gevent.monkey import patch_all
patch_all()
from gevent.pool import Pool
elif ASYNC_TYPE == “thread”:
from multiprocessing.dummy import Pool

from .downloader import Downloader
from .scheduler import Scheduler

from scrapy_plus.http.request import Request
from scrapy_plus.utils.log import logger
from scrapy_plus.utils.stats_collector import StatsCollector

class Engine:
def init(self):
self.spiders = self._auto_import_instances(SPIDERS, isspider=True)
self.downloader = Downloader()
self.pipelines = self._auto_import_instances(PIPELINES)
self.collector = StatsCollector(self.spiders)
self.scheduler = Scheduler(self.collector)
self.downloader_mids = self._auto_import_instances(DOWNLOADER_MIDDLEWARES)
self.spider_mids = self._auto_import_instances(SPIDER_MIDDLEWARES)
# self.total_request_num = 0
# self.total_response_num = 0
self.pool = Pool(4)
self.is_running = False

def _auto_import_instances(self, path=[], isspider=False):
    instances = {} if isspider else []
    for p in path:
        model_name = p.rsplit(".", 1)[0]
        cls_name = p.rsplit(".", 1)[-1]
        model = importlib.import_module(model_name)
        cls = getattr(model, cls_name)
        if isspider:
            instances[cls.name] = cls()
        else:
            instances.append(cls())
    return instances

def start(self):
    t_start = datetime.datetime.now()
    logger.info("爬虫开始启动:{}".format(t_start))
    logger.info("爬虫运行模式:{}".format(ASYNC_TYPE))
    logger.info("最大并发数:{}".format(MAX_ASYNC_NUMBER))
    logger.info("启动的爬虫有:{}".format(list(self.spiders.keys())))
    logger.info("启动的下载中间件有:\n{}".format(DOWNLOADER_MIDDLEWARES))
    logger.info("启动的爬虫中间件有:\n{}".format(SPIDER_MIDDLEWARES))
    logger.info("启动的管道有:\n{}".format(PIPELINES))
    self._start_engine()
    t_end = datetime.datetime.now()
    logger.info("爬虫结束:{}".format(t_end))
    logger.info("耗时:%s" % (t_end - t_start).total_seconds())
    # logger.info("一共获取了请求:{}个".format(self.total_request_num))
    # logger.info("重复的请求:{}个".format(self.scheduler.repeate_request_num))
    # logger.info("成功的请求:{}个".format(self.total_response_num))
    logger.info("一共获取了请求:{}个".format(self.collector.request_nums))
    logger.info("重复的请求:{}个".format(self.collector.repeat_request_nums))
    logger.info("成功的请求:{}个".format(self.collector.response_nums))
    self.collector.clear()

def _start_request_callback(self,temp):
    self.collector.incr(self.collector.start_request_nums_key)

def _start_request(self):
    def _func(spider_name,spider):
        for start_request in spider.start_requests():
            for spider_mid in self.spider_mids:
                start_request = spider_mid.process_request(start_request)
            start_request.spider_name = spider_name
            self.scheduler.add_request(start_request)
            #使用collector进行数据的收集
            # self.total_request_num += 1
            self.collector.incr(self.collector.request_nums_key)

    for spider_name, spider in self.spiders.items():
        self.pool.apply_async(_func,args=(spider_name,spider),callback=self._start_request_callback)

def _execute_request_response_item(self):
    request = self.scheduler.get_request()
    if request is None:
        return

    spider = self.spiders[request.spider_name]

    for downloader_mid in self.downloader_mids:
        request = downloader_mid.process_request(request)
    response = self.downloader.get_response(request)
    response.meta = request.meta
    for downloader_mid in self.downloader_mids:
        response = downloader_mid.process_response(response)
    for spider_mid in self.spider_mids:
        response = spider_mid.process_response(response)

    parse = getattr(spider, request.parse)

    for ret in parse(response):
        if isinstance(ret, Request):
            ret.spider_name = request.spider_name
            for spider_mid in self.spider_mids:
                ret = spider_mid.process_request(ret)
            self.scheduler.add_request(ret)
            #使用collector进行数据的收集
            # self.total_request_num += 1
            self.collector.incr(self.collector.request_nums_key)
        else:
            for pipeline in self.pipelines:
                pipeline.process_item(ret, spider)
    #使用collector进行数据的收集
    # self.total_response_num += 1
    self.collector.incr(self.collector.response_nums_key)

def _callback(self, temp):
    if self.is_running:
        self.pool.apply_async(self._execute_request_response_item, callback=self._callback)

def _start_engine(self):
    # spider中的start_url开始启动
    self.is_running = True
    self.pool.apply_async(self._start_request)
    for i in range(MAX_ASYNC_NUMBER):
        self.pool.apply_async(self._execute_request_response_item, callback=self._callback)

    while True:
        time.sleep(0.001)

        # if self.total_response_num+self.scheduler.repeate_request_num >= self.total_request_num
        #当start_request的执行数量和爬虫的数量相同的时候
        if self.collector.response_nums + self.collector.repeat_request_nums >= self.collector.request_nums:
                self.is_running = False
                break

小结
本小结重点
分布式的实现原理
使用redis实现队列中,put和put_nowait以及get和get_nowait的区别
完成代码的重构,实现分布式

3.4.2框架升级 – 增量爬虫设计原理及其实现
目标
理解增量式爬虫的原理
完成增量式爬虫的实现

  1. 增量爬虫设计原理
    增量抓取,意即针对某个站点的数据抓取,当网站的新增数据或者该站点的数据发生了变化后,自动地抓取它新增的或者变化后的数据

设计原理:

增量爬虫设计原理

1.1 实现关闭请求去重
为Request对象增加属性filter

# scrapy/http/reqeust.py
'''封装Request对象'''


class Request(object):
    '''请求对象,设置请求信息'''

def __init__(self, url, method='GET', headers=None, params=None, data=None, filter=True):
    self.url = url    # 请求地址
    self.method = method    # 请求方法
    self.headers = headers    # 请求头
    self.params = params    # 请求参数
    self.data = data    # 请求体
    self.filter = filter    # 是否进行去重,默认是True

修改调度器,进行判断

# scrapy_plus/core/scheduler.py
class Scheduler(object):

......

def add_request(self, request):
    '''添加请求对象'''

    # 先判断是否要去重
    if request.filter is False:
        self.queue.put(request)
        logger.info("添加请求成功<disable去重>[%s %s]" % (request.method, request.url))
        self.total_request_number += 1  # 统计请求总数
        return # 必须return

    # 添加请求对象前,先进性去重判断
    fp = self._gen_fp(request)
    if not self.filter_request(fp, request):    # 如果指纹不存在,那么添加该请求
        self.queue.put(request)
        logger.info("添加请求成功[%s %s]"%(request.method, request.url))
        self._filter_container.add_fp(fp)     # 添加完请求后,将指纹也记录下来
        self.total_request_number += 1    # 统计请求总数
    else:
        logger.info("发现重复的请求 [%s %s]" % (request.method, request.url))
        self.repeat_request_number += 1

......

1.2 实现无限发起请求:
新增爬虫抓取:新浪滚动新闻

在start_reqeusts中改成无限循环,并设置对应请求为非去重模式。(注意)

spiders/baidu.py

import time

from scrapy_plus.core.spider import Spider
from scrapy_plus.http.request import Request
from scrapy_plus.item import Item
import js2py

class SinaGunDong(Spider):

name = "sina_gundong"

def start_requests(self):
    while True:
        # 需要发起这个请求,才能获取到列表页数据,并且返回的是一个js语句
        url = "http://roll.news.sina.com.cn/interface/rollnews_ch_out_interface.php?col=89&spec=&type=&ch=&k=&offset_page=0&offset_num=0&num=120&asc=&page=1&r=0.5559616678192825"
        yield Request(url, parse='parse', filter=False)
        time.sleep(10)     # 每10秒发起一次请求

def parse(self, response):
    '''响应体数据是js代码'''
    # 使用js2py模块,执行js代码,获取数据
    ret = js2py.eval_js(response.body.decode("gbk"))    # 对网站分析发现,数据编码格式是gbk的,因此需要先进行解码
    yield Item(ret.list)

但由于框架调用start_requests方法时同步,如果设置为死循环后,那么位于之后的爬虫的start_requests方法就不会被调用,因此需要在调用每个爬虫的start_reqeusts时设置为异步的

scrapy_plus/core/engine.py

class Engine(object):

......

def _start_requests(self):
    '''向调度器添加初始请求'''
    # 1. 爬虫模块发出初始请求
    # for spider_name, spider in self.spiders.items():
    #     for start_request in spider.start_requests():
    #         # 2. 把初始请求添加给调度器
    #         # 利用爬虫中间件预处理请求对象
    #         for spider_mid in self.spider_mids:
    #             start_request = spider_mid.process_request(start_request)
    #         start_request.spider_name = spider_name    #为请求对象绑定它所属的爬虫的名称
    #         self.scheduler.add_request(start_request)

    def _func(spider_name, spider):
        for start_request in spider.start_requests():
            # 2. 把初始请求添加给调度器
            # 利用爬虫中间件预处理请求对象
            for spider_mid in self.spider_mids:
                start_request = spider_mid.process_request(start_request)
            start_request.spider_name = spider_name    #为请求对象绑定它所属的爬虫的名称
            self.scheduler.add_request(start_request)
    # 1. 爬虫模块发出初始请求
    for spider_name, spider in self.spiders.items():
        self.pool.apply_async(_func, args=(spider_name, spider))    # 把执行每个爬虫的start_requests方法,设置为异步的

......

让程序的主线程在,多个start_reqeusts方法都没执行完毕前,不要进行退出判断,避免退出过早:

scrapy_plus/core/engine.py

class Engine(object):
‘’’
负责驱动各大组件,通过调用各自对外提供的API接口,实现它们之间的交互和协作
提供整个框架的启动入口
‘’’
def init(self):

    self.finshed_start_requests_number = 0

......

def _callback_total_finshed_start_requests_number(self, temp):
    '''记录完成的start_requests的数量'''
    self.finshed_start_requests_number += 1

def _start_requests(self):

    ......

    # 让主线程在这里阻塞
    while True:
        time.sleep(0.001)    # 节省cpu消耗
        # self.pool.apply_async(self._execute_request_response_item)    # 发起一个请求,处理一个响应
        # 设置退出循环的条件:
        # 当处理完的响应数等于总的请求数时,退出循环:
        if self.finshed_start_requests_number == len(self.spiders):    # 判断是否所有爬虫的start_requests是否都执行完毕,
            # 如果都执行完毕,才应该应该进行退出判断
            if self.total_response_number == self.scheduler.total_request_number and self.total_response_number != 0:
                self.running = False    # 设为Flase, 让子线程满足判断条件,不再执行递归循环,然后退出
                break
    logger.info("主线程循环已经退出")
    self.pool.close()   # 意味着无法再向pool添加任务,,无法在调用apply_async  apply
    self.pool.join()   #

小结
本小结重点
理解增量式爬虫的内涵
理解增量式爬虫的具体实现方法
完成代码重构达到增量式爬虫的目的

3.4.3框架升级 – 断点续爬设计原理及其实现
目标
理解断点续爬的内涵
理解分布式爬虫中请求丢失的情况
理解使用备份队列保留请求的过程
完成代码的重构,解决请求丢失的请求

  1. 断点续爬设计分析
    断点续爬设计原理介绍: 断点续爬设计原理

只实现持久化存储队列完成断点续爬可能出现的问题: 只实现持久化存储队列完成断点续爬可能出现的问题

现有断点续爬方案的问题解决方案分析一: 现有断点续爬方案的问题解决方案分析一

现有断点续爬方案的问题解决方案分析二: 现有断点续爬方案的问题解决方案分析二

  1. 断点续爬无丢失方案的实现
    断点续爬无丢失的实现方案分析: 断点续爬无丢失的实现方案

断点续爬无丢失的代码实现:

添加备份容器:利用redis的hash类型类对每一个请求对象进行存储
为Request对象设置重试次数属性
在调度器的get_request方法中实现响应的逻辑判断
实现delete_request方法:从备份中删除对应的Reqeust对象
实现add_lost_request方法
在引擎中调用这些方法,完成断点续爬无丢失需求

# scrapy_plus/redis_hash.py
'''实现一个对redis哈希类型的操作封装'''
import redis
import pickle

from scrapy_plus.http.request import Request
from scrapy_plus.conf import settings


class RedisBackupRequest(object):
    '''利用hash类型,存储每一个请求对象,key是指纹,值就是请求对象'''

REDIS_BACKUP_NAME = settings.REDIS_BACKUP_NAME
REDIS_BACKUP_HOST = settings.REDIS_BACKUP_HOST
REDIS_BACKUP_PORT = settings.REDIS_BACKUP_PORT
REDIS_BACKUP_DB = settings.REDIS_BACKUP_DB


def __init__(self):
    self._redis = redis.StrictRedis(host=self.REDIS_BACKUP_HOST, port=self.REDIS_BACKUP_PORT ,db=self.REDIS_BACKUP_DB)
    self._name = self.REDIS_BACKUP_NAME

# 增删改查
def save_request(self, fp, request):
    '''将请求对象备份到redis的hash中'''
    bytes_data = pickle.dumps(request)
    self._redis.hset(self._name, fp, bytes_data)

def delete_request(self, fp):
    '''根据请求的指纹,将其删除'''
    self._redis.hdel(self._name, fp)

def update_request(self, fp, request):
    '''更新已有的fp'''
    self.save_request(fp, request)

def get_requests(self):
    '''返回全部的请求对象'''
    for fp, bytes_request in self._redis.hscan_iter(self._name):
        request = pickle.loads(bytes_request)
        yield request

为Request对象增加重试次数属性:

class Request(object):
‘’‘框架内置请求对象,设置请求信息’’’

  def __init__(self, url, method='GET', headers=None, params=None, data=None, parse='parse', filter=True, meta=None):
      self.url = url    # 请求地址
      self.method = method    # 请求方法
      self.headers = headers    # 请求头
      self.params = params    # 请求参数
      self.data = data    # 请求体
      self.parse = parse    # 指明它的解析函数, 默认是parse方法
      self.filter = filter  # 是否进行去重,默认是True
      self.retry_time = 0    # 重试次数
      self.meta = meta

修改调度器,实现对应的逻辑以及方法:

# scrapy_plus/core/scheduler.py
  ......
  from scrapy_plus.redis_hash import RedisBackupRequest
  ......

  class Scheduler(object):
      '''
      缓存请求对象(Request),并为下载器提供请求对象,实现请求的调度
      对请求对象进行去重判断
      '''
      def __init__(self,collector):

      if SCHEDULER_PERSIST: #如果使用分布式或者是持久化,使用redis的队列
          self.queue = ReidsQueue()
          self._filter_container = RedisFilterContainer()
      else:
          self.queue = Queue()
          self._filter_container = NoramlFilterContainer()
      self.collector = collector

  def add_reqeust(self, request):
      '''存储request对象进入队列
      return: None
      '''
      # 先判断是否要去重
      if request.filter is False:
          self.queue.put(request)
          logger.info("添加请求成功<disable去重>[%s %s]" % (request.method, request.url))
          self.total_request_number += 1  # 统计请求总数
          return # 必须return

      # 判断去重,如果重复,就不添加,否则才添加
      fp = self._gen_fp(request)
      if not self.filter_request(fp, request):
          # 往队列添加请求
          logger.info("添加请求成功[%s %s]"%(request.method.upper(), request.url))
          self.queue.put(request)
          if settings.ROLE in ['master', 'slave']:
              self._backup_request.save_request(fp, request)   # 对请求进行备份
          # 如果是新的请求,那么就添加进去重容器,表示请求已经添加到了队列中
          self._filter_container.add_fp(fp)

          self.total_request_number += 1
      else:
          self.repeat_request_number += 1

  def get_request(self):
      '''从队列取出一个请求对象
      return: Request Object
      '''
      try:
          request = self.queue.get(False)
      except:
          return None
      else:
          if request.filter is True and settings.ROLE in ['master', 'slave']:  # 先判断 是否需要进行去重
              # 判断重试次数是否超过规定
              fp = self._gen_fp(request)
              if request.retry_time >= settings.MAX_RETRY_TIMES:
                  self._backup_request.delete_request(fp)    # 如果超过,那么直接删除
                  logger.warnning("出现异常请求,且超过最大尝试的次数:[%s]%s"%(request.method, request.url))
              request.retry_time += 1   # 重试次数+1

              self._backup_request.update_request(fp, request)  # 并更新到备份中
          return request

  def delete_request(self, request):
      '''根据请求从备份删除对应的请求对象'''
      if settings.ROLE in ['master', 'slave']:
          fp = self._gen_fp(request)
          self._backup_request.delete_request(fp)

  def add_lost_reqeusts(self):
      '''将丢失的请求对象再添加到队列中'''
      # 从备份容器取出来,放到队列中
      if settings.ROLE in ['master', 'slave']:
          for request in self._backup_request.get_requests():
              self.queue.put(request)
  ......

小结
本小结重点
理解断点续爬的内涵
理解分布式爬虫中请求丢失的情况
理解使用备份队列保留请求的过程
完成代码的重构,解决请求丢失的请求

3.5新浪滚动新闻资讯实时采集
完善 新浪滚动新闻爬虫的数据采集

要求:

存储文章的标题、作者、发布时间、正文、正文中的图片链接、文章链接、文章所属分类
根据网站的实时更新(周期1分钟)进行采集
时间格式保存为"yyyy-mm-dd HH:MM:SS"
存储到mysql数据库
代码实现如下:

新浪滚动的爬虫文件:

# spiders/sina_gundong.py
import time

from scrapy_plus.core.spider import Spider
from scrapy_plus.http.request import Request
from scrapy_plus.item import Item
import js2py


class SinaGunDong(Spider):

name = "sina_gundong"

headers = {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Cookie": "UOR=www.google.com,www.sina.com.cn,; SGUID=1520816292777_83076650; SINAGLOBAL=211.103.136.242_1520816292.736990; SUB=_2AkMt-V_2f8NxqwJRmPEQy2vmZYx_zwjEieKbpa4tJRMyHRl-yD83qnIJtRB6BnlxGSLw2fy6O04cZUKTsCZUeiiFEsZE; SUBP=0033WrSXqPxfM72-Ws9jqgMF55529P9D9WhpFUZmqbYYLueonGrZIL2c; U_TRS1=0000001a.e268c0.5aaa0d39.35b0731a; lxlrttp=1521688012; Apache=223.72.62.219_1522208561.132697; ULV=1522208952476:6:6:3:223.72.62.219_1522208561.132697:1522208561158; U_TRS2=000000db.81c2323e.5abca69b.ad269c11; ArtiFSize=14; rotatecount=1; hqEtagMode=1",
    # "Host": "roll.news.sina.com.cn",   这里host必须禁用掉
    "Pragma": "no-cache",
    "Referer": "http://roll.news.sina.com.cn/s/channel.php?ch=01",
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
}

def start_requests(self):
    while True:
        # 需要发起这个请求,才能获取到列表页数据,并且返回的是一个js语句
        url = "http://roll.news.sina.com.cn/interface/rollnews_ch_out_interface.php?col=89&spec=&type=&ch=&k=&offset_page=0&offset_num=0&num=120&asc=&page=1&r=0.5559616678192825"
        yield Request(url, parse='parse', filter=False)
        time.sleep(60)     # 每60秒发起一次请求

def parse(self, response):
    '''响应体数据是js代码'''
    # 使用js2py模块,执行js代码,获取数据
    ret = js2py.eval_js(response.body.decode("gbk"))    # 对网站分析发现,数据编码格式是gbk的,因此需要先进行解码
    for news in ret.list:    #
        yield Request(news["url"], headers=self.headers, parse='parse_detail', meta={"type": news["channel"]["title"]})

def parse_detail(self, response):
    response.body = response.body.decode("utf-8")    # 部分页面无法正确解码,因此在这里手动进行解码操作
    title = response.xpath("//h1[@class='main-title']/text()")[0]
    pub_date = response.xpath("//span[@class='date']/text()")[0]
    try:
        author = response.xpath("//div[@class='date-source']//a/text()")[0]    # 由于作者的提取,有两种格式,因此这里使用一个异常捕获来进行判断
    except IndexError:
        author = response.xpath("//div[@class='date-source']//span[contains(@class,'source')]/text()")[0]
    content = response.xpath("//div[@class='article']//text()")    # 多个  每一个代表一段
    image_links = response.xpath("//div[@class='article']//img/@src")    # 图片链接有多个

    yield Item({
        "content": content,    # 正文
        "image_links":image_links,    # 图片链接
        "title": title,    # 标题
        "pub_date":pub_date,    # 发布日期
        "author": author,    # 作者
        "url": response.url,    # 文章链接
        "type": response.request.meta["type"],    # 文章所属分类
    }
)

项目中新建db.py

# 项目文件夹下db.py
# 依赖:sqlalchemy  pymysql
from sqlalchemy import Column,Integer,Text,DateTime, String
from sqlalchemy.ext.declarative import declarative_base

# 创建对象的基类:
Base = declarative_base()


class Model(Base):
    __tablename__ = 'sina_news'

id = Column(Integer, primary_key=True, autoincrement=True)    # 主键id
title = Column(String(100), nullable=False)    # 标题
author = Column(String(20), nullable=False)    # 作者
pub_date = Column(DateTime, nullable=False)    # 发布时间
content = Column(Text, nullable=False)    # 正文
image_links = Column(Text, nullable=False)    # 图片链接
url = Column(String(500), nullable=False)    # 文章链接
type = Column(String(6), nullable=False)    # 文章分类
news_tag = Column(String(40), nullable=False)    # 文章去重标记

管道文件:

# 项目下管道文件 pipelines.py

import json
from datetime import datetime
from hashlib import sha1

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from scrapy_plus.core.scheduler import utf8_string

from db import Base, Model


class Pipeline(object):
    '''数据入库前的清洗和格式化处理'''

def process_item(self, item, spider):
    item.data['pub_date'] = datetime.strptime(item.data['pub_date'], '%Y年%m月%d日 %H:%M') # 将时间格式进行一个处理,转换为datetime类型
    item.data['content'] = [i for i in item.data['content'] if i.strip()]    # 去掉content中的空白字符
    item.data['content'] = "\n\n".join(item.data["content"])
    item.data['image_links'] = json.dumps(item.data['image_links'])  # 列表转换为json字符串
    # 数据去重标识生成:利用标题、作者、文章链接生成唯一key
    s1 = sha1()
    s1.update(utf8_string(item.data['title']))
    s1.update(utf8_string(item.data['author']))
    s1.update(utf8_string(item.data['url']))
    item.data['news_tag'] = s1.hexdigest()    # 数据去重标识
    return item


class MysqlPipeline(object):

def __init__(self):
    # 建立数据库链接
    self.conn = create_engine("mysql+pymysql://root:ryoma@127.0.0.1/test3?charset=utf8")
    Base.metadata.create_all(bind=self.conn)    # 创建表,如果有,就不在创建

def _get_session(self):
    # 创建session对象
    Session = sessionmaker(bind=self.conn)
    return Session()

def process_item(self, item, spider):
    session = self._get_session()   # 获取session
    # 先判断news_tag是否已经存在:如果存在,代表数据是重复的,否则才插入
    if not session.query(Model).filter_by(news_tag=item.data['news_tag']).all():
        obj = Model(**item.data)    # 创建模型类对象
        session.add(obj)    # 插入数据
        session.commit()    # 提交
    session.close() # 关闭session
    return item

项目配置文件:

# 更改默认的配置
DEFAULT_LOG_FILENAME = '滚动新闻采集.log'    # 默认日志文件名称


SPIDERS = [
    "spiders.sina.SinaGunDong"
]

PIPELINES = [
    "pipelines.Pipeline",
    "pipelines.MysqlPipeline"
]

SPIDER_MIDS = [
]

DOWNLOADER_MIDS = [
]

# 控制最大并发数
MAX_ASYNC_NUMBER = 1

# 异步模式  thread, coroutine
ASYNC_TYPE = 'thread'

'''分布式配置'''

# 执行角色
# None 代表非分布式,发起初始请求(_start_requests), 处理请求(_execute_request_response_item)
# master代表主,只负责发起初始请求(_start_requests),并维护请求队列
# slave代表从,只负责处理请求(_execute_request_response_item)
# ROLE = 'master'
# ROLE = 'slave'
ROLE = None

# 最大重试次数
MAX_RETRY_TIMES = 3

# redis 队列的配置
REDIS_QUEUE_NAME = 'request_queue'
REDIS_QUEUE_HOST = 'localhost'
REDIS_QUEUE_PORT = 6379
REDIS_QUEUE_DB = 10

# reids 集合配置
REDIS_SET_NAME = 'filter_container'
REDIS_SET_HOST = 'localhost'
REDIS_SET_PORT = 6379
REDIS_SET_DB = 10

# 利用redis进行请求备份 的配置
REDIS_BACKUP_NAME = 'request_backup'
REDIS_BACKUP_HOST = 'localhost'
REDIS_BACKUP_PORT = 6379
REDIS_BACKUP_DB = 10
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值