Spider

目前提供以下 spider

  • LocalSpider:本地爬虫
  • DistributiveSpider:分布式爬虫
  • CycleSpider:周期爬虫(根据需求可继承本地、分布式)
  • JumpSpider:跳转爬虫(基于内存队列,用于频繁的验证码、登录的确认更新 cookie)

LocalSpider

本地爬虫,不支持分布式

创建

注意:在项目目录下执行

palp create -s baidu 1

结构

"""
Create on 2022-12-12 16:21:10.159221
----------
@summary:
----------
@author:
"""
import palp


class BaiduSpider(palp.LocalSpider):
    spider_name = "baidu"   # 自定义的名字
    spider_domains = []  # 允许通过的域名,默认不限制
    spider_settings = None  # 字典形式或导入形式的设置

    def start_requests(self) -> None:
        """
        起始函数

        :return:
        """
        yield palp.RequestGet("https://www.baidu.com")

    def parse(self, request, response) -> None:
        """
        解析函数

        :param request:
        :param response:
        :return:
        """
        print(response)


if __name__ == '__main__':
    BaiduSpider().start()

DistributiveSpider

注意:

  • 分布式爬虫,需要在设置中增加 redis 连接
  • 分布式爬虫默认为优先级队列,本身具有去重的功能,如果有大量必要的重复请求,修改 settings.REQUEST_QUEUE_MODE = 1

创建

palp create -s baidu 2

结构

和普通的爬虫,只是继承类不同,无成本切换

"""
Create on 2022-12-12 16:21:10.159221
----------
@summary:
----------
@author:
"""
import palp


class BaiduSpider(palp.DistributiveSpider):
    spider_name = "baidu"   # 自定义的名字
    spider_domains = []  # 允许通过的域名,默认不限制
    spider_settings = None  # 字典形式或导入形式的设置

    def start_requests(self) -> None:
        """
        起始函数

        :return:
        """
        yield palp.RequestGet("https://www.baidu.com")

    def parse(self, request, response) -> None:
        """
        解析函数

        :param request:
        :param response:
        :return:
        """
        print(response)


if __name__ == '__main__':
    BaiduSpider().start()

borrow_request、recycle_request

目前框架是根据以上两个函数实现 cookie_jar 共享功能(可自定义)

  • borrow_request:决定在什么时候借用共享的参数
  • recycle_request:决定在执行完毕时,共享什么参数

在处理批次任务时,可能一次会有很多任务,对每个任务都去重新请求,获取 cookie 的话(指的是登录、验证码等获取必要 cookie)将会是灾难

所以这里解决 cookie 复用,当然要做好 cookie 失效的判断,并重新登录(可在 middleware 判断,并直接返回含有 jumpspider 的请求)

注意:该机制在任何时候都会生效,即你发出去的请求,只要没有 cookie 就会被设置

这里说另外一个逻辑

假设需求是开始需要获取 cookie,但是我们不能每次都通过 JumpSpider 获取
上面的逻辑不适用,因为这里开始时是直接返回的的 JumpSpider
所以这里可以判断一下,能借到 cookie 就借,借不到就通过 JumpSpider 获取

recycle_data = self.queue_borrow.get(block=False)
if recycle_data:
    if 'cookie_jar' in recycle_data:
        yield palp.RequestGet(
            url=url,
            params=params,
            headers=headers,
            meta={'name': name, 'task': task},
            keep_cookie=True,
            cookie_jar=recycle_data['cookie_jar']
        )
else:
    yield palp.RequestGet(
        url=url,
        params=params,
        headers=headers,
        meta={'name': name, 'task': task},
        jump_spider=TycJumpSpider,  # 导入 JumpSpider(不需要实例化)
        jump_request_middleware=RequestMiddleware,  # 导入请求中间件(不需要实例化)
        keep_cookie=True
    )

settings 开启

REQUEST_BORROW = False  # 分发大量任务时,需要在请求中传递的参数(默认回收 cookie 复用)
REQUEST_BORROW_DELETE_WHEN_START = True  # 启动时删除所有 BORROW(非分布式是在内存里,该选项忽略)

spider 回收

因为 spider 内无法判断是否是最后一个请求,所以需要主动回收

self.recycle_request(request=request)

middleware 判断

注意:这只是个案例

该案例是在请求结束时,判断到当前状态不对,随后修改请求,跳转登录,放弃原请求的同时,发出新的请求

def request_close(self, spider, request, response):
    if response.status_code != 200:
        request.jump_spider = LoginJumpSpider
        request.jump_request_middleware = ProxyRequestMiddleware
        return request

案例

"""
Create on 2023-01-03 10:45:25.681820
----------
@summary:
----------
@author:
"""
import palp


class BaiduLocalSpider(palp.DistributiveSpider):
    spider_name = "baidu"  # 自定义的名字
    spider_domains = []  # 允许通过的域名,默认不限制
    spider_settings = None  # 字典形式或导入形式的设置

    def start_requests(self) -> None:
        """
        起始函数

        :return:
        """
        yield palp.RequestGet("https://www.baidu.com", keep_cookie=True)
        yield palp.RequestGet("https://www.baidu.com", keep_cookie=True)

    def parse(self, request, response) -> None:
        """
        解析函数

        :param request:
        :param response:
        :return:
        """
        print(request.cookie_jar.get_cookie())  # 可以看到两次是一样的 cookie
        self.recycle_request(request=request)


if __name__ == '__main__':
    BaiduLocalSpider(thread_count=1).start()

CycleSpider

周期爬虫,使用 mysql 创建任务表,并在爬虫内部获取进行抓取

个人认为,任务需求是多样化的,所以任务表的 task 字段是一个字符串,并且获取的任务是全表字段,这样就可以自定义部分表字段,实现更多需求

注意:

  • 继承周期爬虫的【同时需要继承 分布式 或 本地爬虫】才可以执行
  • 手动导入中间件:palp.middleware.CycleSpiderRecordMiddleware(不需要 record 表,不需要传递 task_id 时框架设置已爬取,可以不要,自己维护就行了)
  • 记录中间件会:自动修改失败、成功的任务状态(request 传递 task_id 时,或自己调用)
  • 记录中间件会:自动在记录表中添加、修改任务执行情况

含有以下方法:

方法 说明
initialize_all_task_states 重置所有任务状态为 0
get_tasks 根据指定状态获取任务
get_tasks_state0 获取状态为 0 的任务
get_tasks_state2 获取状态为 2 的任务
set_task_state 设置指定任务的状态
set_task_state_running 设置指定任务状态为 1
set_task_state_done 设置指定任务状态为 2
set_task_state_failed 设置指定任务状态为 3
create_mysql_table 创建任务表、记录表
check_mysql_table_exists 检查表是否存在
insert_tasks 插入任务(字符串或字符串列表)
insert_task_record_start 创建记录表的初始记录
update_task_record 更新记录表任务处理量
update_task_record_end 更新当前爬虫结束
delete_task 删除任务

创建

palp create -s baidu 3

设置

注意:索引是顺序,应该将该中间件放在最后一个

# 启用则修改 task 表中对应任务的失败、生成状态
REQUEST_MIDDLEWARE = {
    1: "palp.middleware.CycleSpiderRecordMiddleware",
}

# 启用则生成、汇总 record 表单批次爬取记录
SPIDER_MIDDLEWARE = {
    1: 'palp.middleware.CycleSpiderRecordMiddleware',
}

结构

import palp


class CycleSpider(palp.LocalSpider, palp.CycleSpider):
    spider_name = "baidu"  # 自定义的名字
    spider_domains = []  # 允许通过的域名,默认不限制
    spider_settings = None  # 字典形式或导入形式的设置
    spider_table_task_name = f'palp_cycle_task_{spider_name}'  # 任务表
    spider_table_record_name = f'palp_cycle_record_{spider_name}'  # 记录表

    def start_requests(self) -> None:
        self.initialize_all_task_states()  # 重置所有任务状态为 0

        # 获取任务状态为 0 的任务
        for task in self.get_tasks_state0():
            print(task)
            yield palp.RequestGet(url=task['task'], task_id=task['id'])

    def parse(self, request, response) -> None:
        print(response)


if __name__ == '__main__':
    CycleSpider.create_mysql_table()  # 快捷创建表
    CycleSpider.insert_tasks(['https://www.baidu.com', 'https://www.jd.com'])  # 快捷插入任务
    CycleSpider().start()

task_id

注意到这里传递了 task_id,那么什么时候传递 task_id?

  • 首先:task_id 代表 task 的 id,Request 中传递则代表当当前请求成功时 == 任务执行成功,框架自带更新状态
  • task 字段就是一个 url,请求成功视为执行成功
  • task 字段不是 url,那么就不需要 task_id 字段,只有在你觉得这个 task 执行结束时调用以下代码(比如 pipeline 收到指定数据)
spider.set_task_state_done(task_id=xxx)
spider.set_task_state_failed(task_id=xxx)

CycleSpiderRecordMiddleware

该中间件会记录爬虫的执行状态

  • spider_start: 启动时创建记录表初始记录(record 表)
  • request_failed: 传递 task_id 时,记录失败的请求( task 表)
  • request_close: 传递 task_id 时,记录成功的请求( task 表)
  • request_record: spider 结束时,更新 record 表,汇总结果

当你想一直更新 record 表,那么可以自己去写实现,只需调用 CycleSpider 的方法即可

JumpSpider

这是用来解决频繁的登录、验证的,避免逻辑混乱
该 spider 和其余 spider 没有关系,不走任何中间件,是完全独立的

该 spider 含有以下方法

  • spider_start:执行开始时的方法
  • spider_close:执行结束时的方法
  • jump_out:自动合并 cookie、meta(可根据需求重写)

注意:

  • spider 运行错误时,将会将输入请求放入 redis 失败队列(分布式时,需开启对应设置)

创建

palp create baidu 4

使用

JumpSpider 内注意判断状态
不正常的时候可以 yield 原始请求,达到循环的目的,直到达到我们的需求,再去执行正常代码

【示例】

yield RequestGet(
    url='xx',
    jump_spider='xxx',  # 导入 JumpSpider(不需要实例化)
    jump_spider_kwargs={},  # 导入的参数 JumpSpider 中 self.xxx 访问
    jump_request_middleware='xxx',   # 导入请求中间件(不需要实例化)
    callback='xxx'
)

启动爬虫

爬虫内启动

BaiduSpider(thread_count=1).start() # 启动可选参数看源码

启动文件启动

注意:是进程形式启动,推荐直接使用 start.py 即可,可同时启动多个爬虫

参数介绍

  • spider_name:爬虫名(spider 内一致)
  • count:启动多少个实例(可实现单机分布式,就是进程启动多少个)
  • **kwargs:爬虫启动参数,同 spider 的启动参数

【示例】

from palp import start_spider


def main():
    start_spider.execute(spider_name='xxx', count=1)


if __name__ == '__main__':
    main()