• 爬虫 — Scrapy-Redis


    一、背景

    随着互联网+大数据时代的来临,传统的关系型数据库已经不能满足中大型网站日益增长的访问量和数据量。这个时候就需要⼀种能够快速存取数据的组件来缓解数据库服务 I/O 的压力,来解决系统性能上的瓶颈。

    1、数据库的发展历史

    1、在互联网+大数据时代来临之前,企业的一些内部信息管理系统,一个单个数据库实例就能满足系统的需求。

    2、随着系统访问用户的增多,数据量的增大,单个数据库实例已经无法满足系统的读取需求,采用缓存(memcache)+单数据库实例

    3、缓存可以缓解系统的读取压力,但是数据量的写入压力持续增大,使用缓存+主从数据库+读写分离

    4、数据量再次增大,读写分离以后,主数据库的写库压力出现瓶颈,使用缓存+主从数据库集群+读写分离+分库分表

    5、互联网+大数据时代来临,关系型数据库不能很好地存取一些并发性高、实时性高,并且数据格式不固定的数据,采用 NoSQL +主从数据库集群+读写分离+分库分表

    2、NoSQL 和 SQL 数据库的比较

    1、适用场景不同:SQL 数据库适合用于关系特别复杂的数据查询场景,NoSQL 则相反。

    2、事务:SQL 对事务的支持非常完善,而 NoSQL 基本不支持事务。

    3、两者在不断的取长补短。

    二、Redis

    是⼀个高性能的,开源的,C 语言开发的,键值对存储数据的 nosql(not only sql,泛指非关系型数据库

    ) 数据库。

    点击阅读官方文档

    1、特性

    Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。

    Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 List、Set 等数据类型。

    Redis 支持数据的备份。

    2、作用

    快速存取。

    3、应用场景

    点赞、秒杀、直播平台的在线好友列表、商品排行榜等。

    4、用法

    官方地址:https://redis.io/

    命令地址:http://doc.redisfans.com/

    5、安装及启动

    查看帮助命令:

    redis-server --help

    启动服务:

    redis-server.exe

    连接客户端:

    redis-cli.exe

    6、Redis 数据库简单使用

    DBSIZE 查看当前数据库的 key 数量
    KEYS * 查看所有 key 的内容
    FLUSHDB 清空当前数据库的所有 key
    FLUSHALL 清空所有库的所有 key(谨慎使用)
    EXISTS key 判断 key 是否存在

    7、Redis 常用五大数据类型

    7.1 Redis-String

    Redis 中的 String最基本的数据类型,一个 key 对应一个 value。String 可以包含任何数据,但最大不能超过 512M。

    常用操作

    • SET:设置值
    • GET:获取值
    • MSET:设置多个值
    • MGET:获取多个值
    • APPEND:添加字段
    • DEL:删除
    • STRLEN:返回字符串长度

    数值操作

    • INCR:增加
    • DECR:减少
    • INCRBY:指定增加多少
    • DECRBY:指定减少多少

    范围操作

    • GETRANGE:获取指定区间范围内的值,类似 SQL 中的 BETWEEN … AND …
    • SETRANGE:从指定位置开始替换,索引从 0 开始,从 0 到 -1 表示全部

    7.2 Redis-List (单值多value)

    Redis 的 List 类型是一个简单的字符串列表,按照插入顺序排序,可以在列表的头部(左边)或尾部(右边)添加元素。它的底层实际上是一个链表

    常用操作

    • LPUSH/RPUSH/LRANGE:从左/从右/获取指定长度
    • LPOP/RPOP:移除最左/最右的元素
    • LINDEX:按照索引下标获取元素
    • LLEN:求列表长度
    • LREM:删除指定数量的元素
    • LTRIM:截取指定范围的值
    • RPOPLPUSH:将最后一个元素从一个列表弹出并推入另一个列表的头部
    • LSET:设置指定位置的元素值
    • LINSERT:在指定元素前或后插入新元素

    7.3 Redis-Hash

    Redis 的 Hash 是一种键值对集合。它是一个字符串类型的字段和值的映射表,特别适合存储对象。

    常用操作

    • HSET/HGET/HMSET/HMGET/HGETALL/HDEL:设值/取值/设值多个值/取多个值/取全部值/删除值
    • HLEN:求哈希长度
    • HEXISTS:检查某个值是否存在
    • HKEYS/HVALS:获取所有字段名/获取所有字段值

    7.4 Redis-Set (不重复的)

    Redis 的 Set 类型是一个无序集合,存储的值是唯一的。

    常用操作

    • SADD/SMEMBERS/SISMEMBER:添加/查看集合/查看是否存在
    • SCARD:获取集合里的元素个数
    • SREM:删除集合中的元素
    • SPOP:随机移除集合中的一个元素
    • SMOVE:将一个元素从一个集合移到另一个集合
    • SDIFF/SINTER/SUNION:求差集/交集/并集

    7.5 Redis-Zset (有序集合)

    Redis 的 Zset 是一种有序集合,每个元素都关联着一个分数,根据分数的排名来排序。

    常用操作

    • ZADD/ZRANGE:添加元素/获取范围内的元素
    • ZRANGEBYSCORE:根据分数范围获取元素
    • ZREM:删除元素
    • ZCARD:获取有序集合的总数
    • ZCOUNT:获取分数范围内的元素个数
    • ZRANK:获取元素在有序集合中的排名

    8、Python 操作 Redis

    8.1、安装

    pip install redis

    8.2、导入 Redis 模块和创建连接

    # 导入 Redis 模块
    import redis
    
    # 创建一个用于字符串操作的测试类
    class TestString(object):
        def __init__(self):
            # 连接 Redis 服务器
            self.r = redis.StrictRedis(host='192.168.75.130', port=6379, db=0)
    
    # 创建一个用于列表操作的测试类
    class TestList(object):
        def __init__(self):
            # 连接 Redis 服务器
            self.r = redis.StrictRedis(host='192.168.75.130', port=6379)
    
    # 创建一个用于集合操作的测试类
    class TestSet(object):
        def __init__(self):
            # 连接 Redis 服务器
            self.r = redis.StrictRedis(host='192.168.75.130', port=6379)
    
    # 创建一个用于哈希操作的测试类
    class TestHash(object):
        def __init__(self):
            # 连接 Redis 服务器
            self.r = redis.StrictRedis(host='192.168.75.130', port=6379)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    8.3、字符串相关操作

    # 设置值
    def test_set(self):
        res = self.r.set('user1', 'juran-1')  # 设置 key 'user1' 的值为 'juran-1'
        print(res)
    
    # 取值
    def test_get(self):
        res = self.r.get('user1')  # 获取 key 'user1' 的值
        print(res)
    
    # 设置多个值
    def test_mset(self):
        d = {
            'user2': 'juran-2',
            'user3': 'juran-3'
        }
        res = self.r.mset(d)  # 批量设置多个键值对
        print(res)
    
    # 取多个值
    def test_mget(self):
        l = ['user2', 'user3']
        res = self.r.mget(l)  # 批量获取多个键的值
        print(res)
    
    # 删除
    def test_del(self):
        self.r.delete('user2')  # 删除 key 'user2'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    8.4、列表相关操作

    # 插入记录
    def test_push(self):
        res = self.r.lpush('common', '1')  # 将元素 '1' 插入到列表 'common' 的左边
        res = self.r.rpush('common', '2')  # 将元素 '2' 插入到列表 'common' 的右边
    
    # 弹出记录
    def test_pop(self):
        res = self.r.lpop('common')  # 从列表 'common' 的左边弹出一个元素
        res = self.r.rpop('common')  # 从列表 'common' 的右边弹出一个元素
    
    # 范围取值
    def test_range(self):
        res = self.r.lrange('common', 0, -1)  # 获取列表 'common' 中的所有元素
        print(res)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    8.5、集合相关操作

    # 添加数据
    def test_sadd(self):
        res = self.r.sadd('set01', '1', '2')  # 向集合 'set01' 中添加元素 '1' 和 '2'
        lis = ['Cat', 'Dog']
        res = self.r.sadd('set02', *lis)  # 向集合 'set02' 中添加多个元素
    
    # 删除数据
    def test_del(self):
        res = self.r.srem('set01', '1')  # 从集合 'set01' 中删除元素 '1'
    
    # 随机删除数据
    def test_pop(self):
        res = self.r.spop('set02')  # 随机从集合 'set02' 中弹出一个元素
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    8.6、哈希相关操作

    # 批量设值
    def test_hset(self):
        dic = {
            'id': 1,
            'name': 'huawei'
        }
        res = self.r.hmset('mobile', dic)  # 批量设置哈希表 'mobile' 的字段及对应的值
    
    # 批量取值
    def test_hgetall(self):
        res = self.r.hgetall('mobile')  # 获取哈希表 'mobile' 中的所有字段及对应的值
    
    # 判断是否存在 存在返回1 不存在返回0
    def test_hexists(self):
        res = self.r.hexists('mobile', 'id')  # 检查字段 'id' 是否存在于哈希表 'mobile'
        print(res)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    8.7、有序集合相关操作

    class TestSortedSet(object):
        def __init__(self):
            # 连接 Redis 服务器
            self.r = redis.StrictRedis(host='192.168.75.130', port=6379)
    
    # 批量设值
    def test_zadd(self):
        score_members = {
            'v1': 60,
            'v2': 70,
            'v3': 80,
            'v4': 90,
            'v5': 100
        }
        res = self.r.zadd('zset01', score_members)  # 向有序集合 'zset01' 添加带有分数的成员
    
    # 批量取值
    def test_zrange(self):
        res = self.r.zrange('zset01', 0, -1)  # 获取有序集合 'zset01' 中的所有成员
        print(res)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    8.8、连接 Redis 数据库

    def main():
        # 创建字符串操作测试类的实例
        test_string = TestString()
        
        # 调用字符串相关操作
        test_string.test_set()
        test_string.test_get()
        test_string.test_mset()
        test_string.test_mget()
        test_string.test_del()
        
        # 创建列表操作测试类的实例
        test_list = TestList()
        
        # 调用列表相关操作
        test_list.test_push()
        test_list.test_pop()
        test_list.test_range()
        
        # 创建集合操作测试类的实例
        test_set = TestSet()
        
        # 调用集合相关操作
        test_set.test_sadd()
        test_set.test_del()
        test_set.test_pop()
        
        # 创建哈希操作测试类的实例
        test_hash = TestHash()
        
        # 调用哈希相关操作
        test_hash.test_hset()
        test_hash.test_hgetall()
        test_hash.test_hexists()
        
        # 创建有序集合操作测试类的实例
        test_sorted_set = TestSortedSet()
        
        # 调用有序集合相关操作
        test_sorted_set.test_zadd()
        test_sorted_set.test_zrange()
    
    if __name__ == "__main__":
        main()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    三、Scrapy-分布式

    1、启动 Redis

    点击下载 Redis

    1、下载后解压到文件夹,进入解压后的目录下,在地址栏输入 cmd 后按回车。

    2、启动 Redis 服务,运行命令:

    redis-server

    3、新开一个窗口,启动 Redis 客户端,运行命令:

    redis-cli

    2、Scrapy-Redis 简介

    Scrapy-Redis 是 Scrapy 框架的一个扩展,用于实现分布式爬虫。它将 Scrapy 与 Redis 数据库集成,允许多个爬虫实例共享数据并协同工作,以提高爬取效率和可扩展性。

    Scrapy-Redis 使用 Redis 的集合来进行 URL 的去重处理。每个爬虫实例都会在将 URL 添加到队列之前检查它是否已经存在于集合中,以避免重复爬取。

    查看 GitHub 源代码

    3、Scrapy 工作流程

    在这里插入图片描述

    4、Scrapy-Redis 工作流程

    在这里插入图片描述

    5、github 示例代码

    安装 scrapy-redis,终端运行命令:

    pip install scrapy-redis

    在 github 上拉取示例代码,终端运行命令:

    git clone https://github.com/rolando/scrapy-redis.git

    6、Scrapy-Redis 中的 settings 文件

    # Scrapy settings for example project
    #
    # For simplicity, this file contains only the most important settings by
    # default. All the other settings are documented here:
    #
    # http://doc.scrapy.org/topics/settings.html
    #
    SPIDER_MODULES = ['example.spiders']
    NEWSPIDER_MODULE = 'example.spiders'
    USER_AGENT = 'scrapy-redis (+https://github.com/rolando/scrapy-redis)'
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"  # 指定那个去重⽅法给 request 对象去重
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"  # 指定 Scheduler 队列
    SCHEDULER_PERSIST = True  # 队列中的内容是否持久保存,为 false 的时候在关闭 Redis 的时候,清空 Redis
    #SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"
    #SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderQueue"
    #SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderStack"
    ITEM_PIPELINES = {
     'example.pipelines.ExamplePipeline': 300,
     'scrapy_redis.pipelines.RedisPipeline': 400,  # scrapy_redis 实现的 items 保存到 Redis 的 pipeline
    }
    LOG_LEVEL = 'DEBUG'
    # Introduce an artificial delay to make use of parallelism. to speed up the crawl.
    DOWNLOAD_DELAY = 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    7、Scrapy-Redis 运行

    allowed_domains = ['dmoztools.net']
    start_urls = ['http://www.dmoztools.net/']
    scrapy crawl dmoz
    
    • 1
    • 2
    • 3

    运行结束后,Redis 中会多出三个键:

    • “dmoz:requests”:存放待爬取的 requests 对象。
    • “dmoz:item”:存放爬取到的信息。
    • “dmoz:dupefilter”:存放爬取的 requests 的指纹信息。

    在这里插入图片描述

    四、案例

    目标网站:https://www.daomubiji.com/

    需求:

    爬取整套小说,分为1级、2级、3级页面。

    将爬取内容分门别类存放在文件夹中,有一个小说文件夹,里面新建文件夹,每一个文件夹存放每一套盗墓笔记。

    小说具体内容保存为 txt 文件。

    1、页面分析

    1级页面:获取标题以及2级页面的 URL。

    1级标题:‘//li[contains(@id, “menu-item”)]/a/text()’

    2级 URL:‘//li[contains(@id, “menu-item”)]/a/@href’

    2级页面:获取章节标题以及3级页面的 URL。

    2级标题:a_lst = ‘//article/a/text()’

    3级的 URL:a_url = ‘//article/a/@href’

    3级页面:获取的文本内容。

    content = response.xpath(‘//article/p/text()’)

    2、使用 Scrapy 框架实现

    1、在终端输入命令创建一个 Scrapy 框架。

    scrapy startproject dmbj

    cd dmbj

    scrapy genspider dm daomubiji.com

    2、修改 setting.py 文件。

    # setting.py
    LOG_LEVEL = 'WARNING'
    # Obey robots.txt rules
    ROBOTSTXT_OBEY = False
    # Override the default request headers:
    DEFAULT_REQUEST_HEADERS = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language': 'en',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
    }
    # Configure item pipelines
    # See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
    ITEM_PIPELINES = {
       'dmbj.pipelines.DmbjPipeline': 300,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3、在 dmbj 文件夹下新建一个 start.py 文件。

    # start.py
    # 使用 cmdline 模块来执行命令行命令
    from scrapy import cmdline
    
    # 使用 Scrapy 执行名为 dm 的爬虫
    cmdline.execute('scrapy crawl dm'.split())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4、编写 spiders 文件夹下的 dm.py 文件的代码,获取一级标题和二级 url。

    # spiders/dm.py
    import scrapy  # 导入 Scrapy 库,用于构建爬虫
    
    # 创建一个名为 DmSpider 的 Scrapy 爬虫类
    class DmSpider(scrapy.Spider):
        name = 'dm'  # 爬虫的名称
        allowed_domains = ['daomubiji.com']  # 允许爬取的域名
        start_urls = ['http://daomubiji.com/']  # 起始 URL 列表
    
        # 解析函数,用于处理响应并提取数据
        def parse(self, response):
            a_list = response.xpath('//li[contains(@id, "menu-item")]/a')
            for a in a_list:
                # 一级标题
                first_title = a.xpath('./text()').get()
                # 二级 url
                second_url = a.xpath('./@href').get()
                # 打印一级标题和二级 url
                print(first_title, second_url)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    5、编写 item.py 文件的代码,保存一级标题。

    # item.py
    import scrapy  # 导入 Scrapy 库,用于构建爬虫
    
    class DmbjItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
        # 一级标题
        first_title = scrapy.Field()
        pass
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    6、编写 spiders 文件夹下的 dm.py 文件的代码,构造二级页面。

    # spiders/dm.py
    import scrapy  # 导入 Scrapy 库,用于构建爬虫
    from dmbj.items import DmbjItem  # 导入自定义的 Item 类
    
    # 创建一个名为 DmSpider 的 Scrapy 爬虫类
    class DmSpider(scrapy.Spider):
        name = 'dm'  # 爬虫的名称
        allowed_domains = ['daomubiji.com']  # 允许爬取的域名
        start_urls = ['http://daomubiji.com/']  # 起始 URL 列表
    
        # 解析函数,用于处理响应并提取数据
        def parse(self, response):
            a_list = response.xpath('//li[contains(@id, "menu-item")]/a')
            for a in a_list:
                # 创建一个 DmbjItem 实例
                item = DmbjItem()
                # 一级标题
                item['first_title'] = a.xpath('./text()').get()
                # 二级 url
                second_url = a.xpath('./@href').get()
                # 构造二级页面,使用 parse_article 方法处理
                yield scrapy.Request(
                    url=second_url,
                    meta={'item': item},  # 将 item 传递给下一个回调函数
                    callback=self.parse_article
                )
    
        # 解析二级页面
        def parse_article(self, response):
            # 从响应的 meta 中获取之前传递的 item 对象
            item = response.meta.get('item')
            # 三级 url,章节标题
            a_lst = response.xpath('//article/a')
            for a in a_lst:
                # 存储章节标题到 item 对象
                item['second_title'] = a.xpath('./text()').get()
                # 三级 url
                third_url = a.xpath('./@href').get()
                print(item)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    7、编写 item.py 文件的代码,保存二级标题。

    # item.py
    import scrapy  # 导入 Scrapy 库,用于构建爬虫
    
    class DmbjItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
        # 一级标题
        first_title = scrapy.Field()
        # 二级标题
        second_title = scrapy.Field()
        pass
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    8、编写 spiders 文件夹下的 dm.py 文件的代码,获取文章内容的段落。

    # spiders/dm.py
    import scrapy  # 导入 Scrapy 库,用于构建爬虫
    from dmbj.items import DmbjItem  # 导入自定义的 Item 类
    
    # 创建一个名为 DmSpider 的 Scrapy 爬虫类
    class DmSpider(scrapy.Spider):
        name = 'dm'  # 爬虫的名称
        allowed_domains = ['daomubiji.com']  # 允许爬取的域名
        start_urls = ['http://daomubiji.com/']  # 起始 URL 列表
    
        # 解析函数,用于处理响应并提取数据
        def parse(self, response):
            a_list = response.xpath('//li[contains(@id, "menu-item")]/a')
            for a in a_list:
                # 创建一个 DmbjItem 实例
                item = DmbjItem()
                # 一级标题
                item['first_title'] = a.xpath('./text()').get()
                # 二级 url
                second_url = a.xpath('./@href').get()
                # 构造二级页面,使用 parse_article 方法处理
                yield scrapy.Request(
                    url=second_url,
                    meta={'item': item},  # 将 item 传递给下一个回调函数
                    callback=self.parse_article
                )
    
        # 解析二级页面
        def parse_article(self, response):
            # 从响应的 meta 中获取之前传递的 item 对象
            item = response.meta.get('item')
            # 三级 url,章节标题
            a_lst = response.xpath('//article/a')
            for a in a_lst:
                # 存储章节标题到 item 对象
                item['second_title'] = a.xpath('./text()').get()
                # 三级 url
                third_url = a.xpath('./@href').get()
                # print(item)
                # 向三级页面发请求
                yield scrapy.Request(
                    url=third_url,
                    meta={'item': item},  # 将 item 传递给下一个回调函数
                    callback=self.parse_content
                )
    
        # 解析获取数据内容
        def parse_content(self, response):
            # 从响应的 meta 中获取之前传递的 item 对象
            item = response.meta.get('item')
            # 使用 XPath 选择器提取文章内容的段落
            content_lst = response.xpath('//article[@class="article-content"]/p/text()').getall()
            # 将段落文本连接成一个字符串,并存储到 item 对象
            item['content'] = '\n'.join(content_lst)
            print(item)
            # 将 item 传递给下一个处理管道
            yield item
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    9、编写 item.py 文件的代码,保存内容。

    # item.py
    import scrapy  # 导入 Scrapy 库,用于构建爬虫
    
    class DmbjItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
        # 一级标题
        first_title = scrapy.Field()
        # 二级标题
        second_title = scrapy.Field()
        # 内容
        content = scrapy.Field()
        pass
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    10、创建文件夹,保存获取的数据。

    方式一:在 spiders 文件夹下的 dm.py 文件里编写代码。

    # spiders/dm.py
    import scrapy  # 导入 Scrapy 库,用于构建爬虫
    from dmbj.items import DmbjItem  # 导入自定义的 Item 类
    import re  # 导入正则表达式模块
    import os  # 导入操作系统模块
    
    # 创建一个名为 DmSpider 的 Scrapy 爬虫类
    class DmSpider(scrapy.Spider):
        name = 'dm'  # 爬虫的名称
        allowed_domains = ['daomubiji.com']  # 允许爬取的域名
        start_urls = ['http://daomubiji.com/']  # 起始 URL 列表
    
        # 解析函数,用于处理响应并提取数据
        def parse(self, response):
            a_list = response.xpath('//li[contains(@id, "menu-item")]/a')
            for a in a_list:
                # 创建一个 DmbjItem 实例
                item = DmbjItem()
                # 一级标题
                s = a.xpath('./text()').get()
                # 二级 url
                second_url = a.xpath('./@href').get()
                # 使用正则表达式替换一级标题中的特殊字符为下划线
                item['first_title'] = re.sub(r'[\\:<>*? ]', "_", s)
                # 构造一级标题的文件夹路径
                dir_path = r"小说/{}".format(item['first_title'])
                # 创建之前一定要做判断,如果文件夹不存在,则创建
                if not os.path.exists(dir_path):
                    os.makedirs(dir_path)
                # 构造二级页面,使用 parse_article 方法处理
                yield scrapy.Request(
                    url=second_url,
                    meta={'item': item},  # 将 item 传递给下一个回调函数
                    callback=self.parse_article
                )
    
        # 解析二级页面
        def parse_article(self, response):
            # 从响应的 meta 中获取之前传递的 item 对象
            item = response.meta.get('item')
            # 三级 url,章节标题
            a_lst = response.xpath('//article/a')
            for a in a_lst:
                # 存储章节标题到 item 对象
                item['second_title'] = a.xpath('./text()').get()
                # 三级 url
                third_url = a.xpath('./@href').get()
                # print(item)
                # 向三级页面发请求
                yield scrapy.Request(
                    url=third_url,
                    meta={'item': item},  # 将 item 传递给下一个回调函数
                    callback=self.parse_content
                )
    
        # 解析获取数据内容
        def parse_content(self, response):
            # 从响应的 meta 中获取之前传递的 item 对象
            item = response.meta.get('item')
            # 使用 XPath 选择器提取文章内容的段落
            content_lst = response.xpath('//article[@class="article-content"]/p/text()').getall()
            # 将段落文本连接成一个字符串,并存储到 item 对象
            item['content'] = '\n'.join(content_lst)
            print(item)
            # 将 item 传递给下一个处理管道
            yield item
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    方式二:在管道文件 pipelines.py 中创建。

    # pipelines.py
    import re  # 导入 re 模块,用于正则表达式操作
    
    # 定义 DmbjPipeline 类
    class DmbjPipeline:
        # 处理爬取到的数据项
        def process_item(self, item, spider):
            # 构建保存小说文件的文件夹路径,使用一级标题和二级标题,将特殊字符替换为下划线
            dir_path = r'小说/{}/{}'.format(
                item['first_title'],
                re.sub(r'[\\:<>*? ]', "_", item['second_title'])
            )
            # 构建保存小说内容的文件路径,加上 .txt 后缀
            filename = dir_path + '.txt'
            # 将小说内容写入文件中,使用 UTF-8 编码
            with open(filename, 'w', encoding='utf-8') as f:
                f.write(item['content'])
            # 返回 item 对象,用于后续的处理或保存
            return item
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    11、对数据做一个深拷贝处理,使用 Scrapy 框架实现就完成了。

    # spiders/dm.py
    import scrapy  # 导入 Scrapy 库,用于构建爬虫
    from dmbj.items import DmbjItem  # 导入自定义的 Item 类
    import re  # 导入正则表达式模块
    import os  # 导入操作系统模块
    from copy import deepcopy  # 导入深拷贝函数
    
    # 创建一个名为 DmSpider 的 Scrapy 爬虫类
    class DmSpider(scrapy.Spider):
        name = 'dm'  # 爬虫的名称
        allowed_domains = ['daomubiji.com']  # 允许爬取的域名
        start_urls = ['http://daomubiji.com/']  # 起始 URL 列表
    
        # 解析函数,用于处理响应并提取数据
        def parse(self, response):
            a_list = response.xpath('//li[contains(@id, "menu-item")]/a')
            for a in a_list:
                # 创建一个 DmbjItem 实例
                item = DmbjItem()
                # 一级标题
                s = a.xpath('./text()').get()
                # 二级 url
                second_url = a.xpath('./@href').get()
                # 使用正则表达式替换一级标题中的特殊字符为下划线
                item['first_title'] = re.sub(r'[\\:<>*? ]', "_", s)
                # 构造一级标题的文件夹路径
                dir_path = r"小说/{}".format(item['first_title'])
                # 创建之前一定要做判断,如果文件夹不存在,则创建
                if not os.path.exists(dir_path):
                    os.makedirs(dir_path)
                # 构造二级页面,使用 parse_article 方法处理
                yield scrapy.Request(
                    url=second_url,
                    meta={'item': item},  # 将 item 传递给下一个回调函数
                    callback=self.parse_article
                )
    
        # 解析二级页面
        def parse_article(self, response):
            # 从响应的 meta 中获取之前传递的 item 对象
            item = response.meta.get('item')
            # 三级 url,章节标题
            a_lst = response.xpath('//article/a')
            for a in a_lst:
                # 存储章节标题到 item 对象
                item['second_title'] = a.xpath('./text()').get()
                # 三级 url
                third_url = a.xpath('./@href').get()
                # print(item)
                # 向三级页面发请求
                yield scrapy.Request(
                    url=third_url,
                    meta={'item': deepcopy(item)},  # 将 item 传递给下一个回调函数
                    callback=self.parse_content
                )
    
        # 解析获取数据内容
        def parse_content(self, response):
            # 从响应的 meta 中获取之前传递的 item 对象
            item = response.meta.get('item')
            # 使用 XPath 选择器提取文章内容的段落
            content_lst = response.xpath('//article[@class="article-content"]/p/text()').getall()
            # 将段落文本连接成一个字符串,并存储到 item 对象
            item['content'] = '\n'.join(content_lst)
            print(item)
            # 将 item 传递给下一个处理管道
            yield item
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    3、改写成 Scrapy-Redis

    1、导入 Scrapy-Redis,修改继承类。

    # spiders/dm.py
    import scrapy  # 导入 Scrapy 库,用于构建爬虫
    from dmbj.items import DmbjItem  # 导入自定义的 Item 类
    import re  # 导入正则表达式模块
    import os  # 导入操作系统模块
    from copy import deepcopy  # 导入深拷贝函数
    from scrapy_redis.spiders import RedisSpider  # 导入 Scrapy-Redis 中的 RedisSpider 类
    
    
    # 创建一个名为 DmSpider 的 Scrapy 爬虫类,继承自 RedisSpider
    class DmSpider(RedisSpider):
        name = 'dm'  # 爬虫的名称
        redis_key = 'daomu_key'  # 指定 Redis 的键名,从中读取起始 URL
        # allowed_domains = ['daomubiji.com']  # 允许爬取的域名
        # start_urls = ['http://daomubiji.com/']  # 起始 URL 列表
    
        # 解析函数,用于处理响应并提取数据
        def parse(self, response):
            a_list = response.xpath('//li[contains(@id, "menu-item")]/a')
            for a in a_list:
                # 创建一个 DmbjItem 实例
                item = DmbjItem()
                # 一级标题
                s = a.xpath('./text()').get()
                # 二级 url
                second_url = a.xpath('./@href').get()
                # 使用正则表达式替换一级标题中的特殊字符为下划线
                item['first_title'] = re.sub(r'[\\:<>*? ]', "_", s)
                # 构造一级标题的文件夹路径
                dir_path = r"小说/{}".format(item['first_title'])
                # 创建之前一定要做判断,如果文件夹不存在,则创建
                if not os.path.exists(dir_path):
                    os.makedirs(dir_path)
                # 构造二级页面,使用 parse_article 方法处理
                yield scrapy.Request(
                    url=second_url,
                    meta={'item': item},  # 将 item 传递给下一个回调函数
                    callback=self.parse_article
                )
    
        # 解析二级页面
        def parse_article(self, response):
            # 从响应的 meta 中获取之前传递的 item 对象
            item = response.meta.get('item')
            # 三级 url,章节标题
            a_lst = response.xpath('//article/a')
            for a in a_lst:
                # 存储章节标题到 item 对象
                item['second_title'] = a.xpath('./text()').get()
                # 三级 url
                third_url = a.xpath('./@href').get()
                # print(item)
                # 向三级页面发请求
                yield scrapy.Request(
                    url=third_url,
                    meta={'item': deepcopy(item)},  # 将 item 传递给下一个回调函数
                    callback=self.parse_content
                )
    
        # 解析获取数据内容
        def parse_content(self, response):
            # 从响应的 meta 中获取之前传递的 item 对象
            item = response.meta.get('item')
            # 使用 XPath 选择器提取文章内容的段落
            content_lst = response.xpath('//article[@class="article-content"]/p/text()').getall()
            # 将段落文本连接成一个字符串,并存储到 item 对象
            item['content'] = '\n'.join(content_lst)
            print(item)
            # 将 item 传递给下一个处理管道
            yield item
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    2、修改 setting.py 文件。

    # setting.py
    # Scrapy settings for dmbj project
    #
    # For simplicity, this file contains only settings considered important or
    # commonly used. You can find more settings consulting the documentation:
    #
    #     https://docs.scrapy.org/en/latest/topics/settings.html
    #     https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
    #     https://docs.scrapy.org/en/latest/topics/spider-middleware.html
    
    BOT_NAME = 'dmbj'
    LOG_LEVEL = 'WARNING'
    SPIDER_MODULES = ['dmbj.spiders']
    NEWSPIDER_MODULE = 'dmbj.spiders'
    
    
    # 设置用户代理信息
    USER_AGENT = 'scrapy-redis (+https://github.com/rolando/scrapy-redis)'
    # 指定去重方式,给请求对象去重
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
    # 设置调度器
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"
    # 队列中的内容是否进行持久保留
    # True redis 关闭的时候数据会保留
    # False 不会保留
    SCHEDULER_PERSIST = True
    # SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"
    # SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderQueue"
    # SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderStack"
    
    # LOG_LEVEL = 'DEBUG'
    
    # Introduce an artifical delay to make use of parallelism. to speed up the
    # crawl.
    DOWNLOAD_DELAY = 1
    
    # 配置 Redis 连接信息
    REDIS_URL = "redis://127.0.0.1:6379"
    
    
    # Crawl responsibly by identifying yourself (and your website) on the user-agent
    #USER_AGENT = 'dmbj (+http://www.yourdomain.com)'
    
    # Obey robots.txt rules
    ROBOTSTXT_OBEY = False
    
    # Configure maximum concurrent requests performed by Scrapy (default: 16)
    #CONCURRENT_REQUESTS = 32
    
    # Configure a delay for requests for the same website (default: 0)
    # See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay
    # See also autothrottle settings and docs
    #DOWNLOAD_DELAY = 3
    # The download delay setting will honor only one of:
    #CONCURRENT_REQUESTS_PER_DOMAIN = 16
    #CONCURRENT_REQUESTS_PER_IP = 16
    
    # Disable cookies (enabled by default)
    #COOKIES_ENABLED = False
    
    # Disable Telnet Console (enabled by default)
    #TELNETCONSOLE_ENABLED = False
    
    # Override the default request headers:
    DEFAULT_REQUEST_HEADERS = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language': 'en',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
    }
    
    # Enable or disable spider middlewares
    # See https://docs.scrapy.org/en/latest/topics/spider-middleware.html
    #SPIDER_MIDDLEWARES = {
    #    'dmbj.middlewares.dmbjSpiderMiddleware': 543,
    #}
    
    # Enable or disable downloader middlewares
    # See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
    #DOWNLOADER_MIDDLEWARES = {
    #    'dmbj.middlewares.dmbjDownloaderMiddleware': 543,
    #}
    
    # Enable or disable extensions
    # See https://docs.scrapy.org/en/latest/topics/extensions.html
    #EXTENSIONS = {
    #    'scrapy.extensions.telnet.TelnetConsole': None,
    #}
    
    # Configure item pipelines
    # See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
    ITEM_PIPELINES = {
       'dmbj.pipelines.DmbjPipeline': 300,
       'scrapy_redis.pipelines.RedisPipeline': 400,  # 保存数据到 Redis 数据库
    }
    
    # Enable and configure the AutoThrottle extension (disabled by default)
    # See https://docs.scrapy.org/en/latest/topics/autothrottle.html
    #AUTOTHROTTLE_ENABLED = True
    # The initial download delay
    #AUTOTHROTTLE_START_DELAY = 5
    # The maximum download delay to be set in case of high latencies
    #AUTOTHROTTLE_MAX_DELAY = 60
    # The average number of requests Scrapy should be sending in parallel to
    # each remote server
    #AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
    # Enable showing throttling stats for every response received:
    #AUTOTHROTTLE_DEBUG = False
    
    # Enable and configure HTTP caching (disabled by default)
    # See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
    #HTTPCACHE_ENABLED = True
    #HTTPCACHE_EXPIRATION_SECS = 0
    #HTTPCACHE_DIR = 'httpcache'
    #HTTPCACHE_IGNORE_HTTP_CODES = []
    #HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115

    3、启动 Redis

    进入 Redis 目录,在地址栏输入 cmd 后按回车两次,分别启动 Redis 服务端和 Redis 客户端。

    启动 Redis 服务端,运行命令:

    redis-server

    启动 Redis 客户端,运行命令:

    redis-cli

    在 Redis 客户端输入命令:

    清空:

    flushal

    查询:

    keys *

    4、启动程序

    在 Redis 客户端输入命令:

    lpush Redis键名 网址

    lpush daomu_key http://daomubiji.com/

    在 Redis 服务端输入命令:

    cd 项目的绝对路径

    scrapy crawl dm

    可以开多个终端运行以上两条命令,会同时爬取数据。

    4、改写分布式总结

    1、导入类,修改继承类。

    from scrapy_redis.spiders import RedisSpider
    class DmSpider(RedisSpider):
    
    • 1
    • 2

    2、修改配置文件。

    # 设置用户代理信息
    USER_AGENT = 'scrapy-redis (+https://github.com/rolando/scrapy-redis)'
    # 指定去重方式,给请求对象去重
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
    # 设置调度器
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"
    # 队列中的内容是否进行持久保留
    # True redis 关闭的时候数据会保留
    # False 不会保留
    SCHEDULER_PERSIST = True
    # SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"
    # SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderQueue"
    # SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderStack"
    
    # LOG_LEVEL = 'DEBUG'
    
    # Introduce an artifical delay to make use of parallelism. to speed up the
    # crawl.
    DOWNLOAD_DELAY = 1
    
    # 配置 Redis 连接信息
    REDIS_URL = "redis://127.0.0.1:6379"
    
    # 管道
    ITEM_PIPELINES = {
       'dmbj.pipelines.DmbjPipeline': 300,
       'scrapy_redis.pipelines.RedisPipeline': 400,  # 保存数据到 Redis 数据库
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    3、注意爬取数据前要在 Redis 客户端输入命令。

    lpush Redis键名 网址

    lpush daomu_key http://daomubiji.com/

    记录学习过程,欢迎讨论交流,尊重原创,转载请注明出处~

  • 相关阅读:
    MySQL数据库之事物
    web网页设计期末课程大作业:美食餐饮文化主题网站设计——美食汇5页HTML+CSS+JavaScript
    JavaScript 31 JavaScript 日期获取方法
    【算法设计与分析】基于Go语言实现动态规划法解决TSP问题
    学习C语言 8.10 枚举、位运算、指针函数
    sql server 分区表
    c++核心准则
    EasyGBS录像回放速度慢是什么原因?该如何解决?
    React18 之 Suspense
    6.3 应用动态内存补丁
  • 原文地址:https://blog.csdn.net/muyuhen/article/details/132986463