scrapy学习笔记

Scrapy

Ciallo~(∠・ω< )⌒★

一、概念和流程

1.概念

​ Scrapy是一个python编写的开源异步网络爬虫框架,可以以少量的代码进行快速高效的爬取

2.工作流程

Scrapy工作流程

名称作用实现
Scrapy Engine(Scrapy引擎)整个框架的核心,负责控制数据和信号在不同模块间的传递Scrapy内置
Scheduler(调度器)一个队列,存放引擎发过来的request请求,接收从引擎发来的url,并将url加入队列Scrapy内置
Downloader(下载器)发送请求并获取响应,将response响应封装后发送给引擎Scrapy内置
Spider(爬虫文件)处理引擎发来的response响应,提取数据和url并解析,完成后发送给引擎需要改写
Item Pipline(管道)处理引擎传来的数据,对数据进行存储需要改写
Downloader Middlewares(下载中间件)位于下载器和引擎中间,用于处理请求和响应,可以自定义扩展,如设置代理等一般不需要改写
Spider Middlewares(爬虫中间件)位于引擎和爬虫文件之间,用来处理爬虫程序的响应和输出结果以及处理新需求一般不需要改写

其具体的工作流程如下:

  1. 爬虫程序发送url给引擎
  2. 引擎将url发生给调度器,调度器将url加入到队列中
  3. 调度器取出一个url交给引擎
  4. 引擎将调度器取出的url通过下载中间件发送给下载器
  5. 下载器拿到url后发送网络请求,获取url相应的response响应
  6. 下载器将response响应通过下载中间件发送给引擎
  7. 引擎将response响应通过爬虫中间件发送给爬虫程序进行解析
  8. 爬虫程序对response响应进行处理,将数据通过引擎发送给管道,或将获取到的新url重新执行流程
  9. 管道获取到引擎发送的数据,对数据进行保存

3.Scrapy中的三个内置对象

  • requests请求对象:由url、method、post_data、headers等构成
  • response响应对象:由url、body、status、headers等构成
  • item数据对象:本质是一个字典

二、Scrapy入门使用

0.目标

  • 安装Scrapy
  • 创建Scrapy项目
  • 创建Scrapy爬虫
  • 运行Scrapy爬虫
  • 应用Scrapy定位以及提取数据或属性值的方法
  • 掌握response响应对象的常用属性

1.安装Scrapys

pip install scrapy

2.Scrapy项目开发流程

  1. 创建项目

    • scrapy startproject 项目名
      
  2. 生成爬虫

    • scrapy genspider 爬虫名 爬虫域名
      
  3. 提取数据

    • 根据网站结构在Spider中实现数据的采集
  4. 保存数据

    • 使用pipeline(管道)进行数据后续处理和保存

3.创建项目

使用scrapy startproject mySpider创建项目

项目目录结构如下:

  • mySpider
    • mySpider
      • spiders
        • _init_.py
      • _init_.py
      • items.py ——>爬取内容设置,可以进行数据清洗等操作
      • middlewares.py ——>自定义中间件
      • pipelines.py ——>管道,用于保存数据
      • settings.py ——>设置文件,UA,启动器管道配置等
    • scrapy.cfg ——>项目配置文件

4.创建爬虫

我们可以通过命令创建爬虫文件,爬虫文件为主要的代码作业文件

在项目路径下(即第一个mySpider)执行:

scrapy genspider <爬虫名> <允许爬取的域名>

  • 爬虫名
    • 爬虫运行时的名字
  • 允许爬取的域名
    • 对爬虫爬取设置范围,在指定域名范围内才可被爬取

scrapy genspider csdn csdn.net

创建后的文件将会被放在mySpider->spiders

其文件内容如下

import scrapy	#自动导入Scrapy库
class DoubanSpider(scrapy.Spider):	#我们定义的爬虫,自动继承scrapy.Spider类
    name = "csdn"	#我们自定义的爬虫名字
    allowed_domains = ["csdn.net"]	#允许爬取的域名
    start_urls = ["https://csdn.net"]	#自动使用允许爬取的域名创建的起始地址

    def parse(self, response):	#用于对起始url的响应对象进行处理的函数,
        pass
#当爬虫开始执行时,爬虫程序会将起始url传递给引擎,引擎传递给下载器来获得响应,并将起始url的响应发送给爬虫程序的parse方法进行处理

实例:

#编写parse方法内容,将起始url的响应内容写入本地的html
    def parse(self, response):
        with open("test.html","wb+") as f:
            f.write(response.body)
        pass

运行爬虫

在项目目录下使用命令:

scrapy crawl <爬虫名>

scrapy crawl csdn

在执行语句后,我们会看到输出框输出了很多字符,这些字符为日志,暂且不用管。

在运行结束后,我们可以看到在我们的项目目录下生成了test.html文件,打开可以查看是否成功完成了起始url的爬取

5.完成爬虫

5.1 三个步骤

完成爬虫时,我们首先应该修改起始的url,确保爬虫能从我们想要的地方开始

如我们想要爬取是CSDN主页的模块名,在此不需要更改

接着,我们应该检查并修改允许的域名,确保我们的起始url在allowed_domains内

最后,完善pares方法,完成数据处理

5.2 示列

输出CSDN主页的模块名

import scrapy
from pprint import pprint


class CsdnSpider(scrapy.Spider):
    name = "csdn"
    allowed_domains = ["csdn.net"]
    start_urls = ["https://www.csdn.net/"]


    def parse(self, response):
        bankuai=response.css(".navigation-right a::text")
        for i in bankuai:
            pprint(i.extract())
        pass
####输出结果####
后端
前端
移动开发
编程语言
Java
Python
人工智能
AIGC
大数据
数据库
数据结构与算法
音视频
云原生
云平台
前沿技术
开源
小程序
运维
服务器
操作系统
硬件开发
嵌入式
微软技术
软件工程
测试
网络空间安全
网络与通信
用户体验设计
学习和成长
搜索
开发工具
游戏
HarmonyOS
区块链
数学
3C硬件
资讯

若想要将数据进行保存,则需要将获取到的数据使用yield返回给引擎,引擎会将数据交给管道保存

:解析函数支持返回的类型只能有:baseitem、request,dict,None

5.3 元素定位和提取方法

  • css方法
    • 使用css选择器选中元素
    • 返回选中元素的列表
  • xpath方法
    • 使用xpath选择器选中元素
    • 返回选中元素的列表
  • extract方法
    • 将一个元素列表中的元素转换为字符串格式的html,并组合成列表
    • 返回字符串列表
  • extract_first方法
    • 同extract方法,但是只提取出第一个
    • 返回一个字符串
  • 运行爬虫时加上*–nolog*可以取消日志显示

5.4 response响应对象的常用属性

  • response.url
    • 当前响应的url地址
  • response.request.url
    • 当前响应对应的请求的url地址
  • response.headers
    • 当前响应的响应头
  • response.requests.headers
    • 当前响应对应的请求的请求头
  • response.body
    • 响应体,byte类型
  • response.status
    • 响应状态码

6.保存数据

利用管道pipline来保存数据

6.1 在pipelines.py文件中定义对数据的操作

  1. 定义一个管道类

    • 当当前项目中只存在一个爬虫时,scrapy会自动以该爬虫名定义一个管道类

    • class MyspiderPipeline:
      
  2. 重写管道类的process_item方法

    • 原始的process_itme方法如下

    •     def process_item(self, item, spider):
              return item
      
    • 其中item为我们爬虫使用yield返回的数据,spider为我们的爬虫,这两个参数都是自动传入

    • 在该方法内,我们进行数据处理的编写

  3. process_item方法处理完item后必须使用return返回将item返回给引擎

6.2 在settings.py配置启用管道

settings.py文件中找到变量ITEM_PIPELINES,该变量为一个字典,其内容应该如下

ITEM_PIPELINES = {
   "mySpider.pipelines.MyspiderPipeline": 300,
}

该字典中的每一个键值对都代表我们的一个管道,管道键的规则为:

项目目录.管道文件.管道类

管道键对应的值表示管道执行的优先级,数值越小,优先级越高

ITEM_PIPELINES启用即可开启管道

6.3 将爬取到的板块名储存在本地

爬虫

class CsdnSpider(scrapy.Spider):
    name = "csdn"
    allowed_domains = ["csdn.net"]
    start_urls = ["https://www.csdn.net/"]

    def parse(self, response):
        result={}
        #获取所有的板块名
        bankuai=response.css(".navigation-right a::text")
        #将板块封装
        for i in range(len(bankuai)):
            result[f"{i}"]=bankuai[i].extract()
        #发送给管道
        print("发送管道")
        yield result

管道

from itemadapter import ItemAdapter
import json

class MyspiderPipeline:
    def __init__(self):
        #定义file属性,用于打开文件
        self.file=open("test.json",'w+',encoding="utf-8")
    def __del__(self):
        #管道完成操作后关闭文件
        self.file.close()
    def process_item(self, item, spider):
        print("管道开始工作")
        #转换为json文件
        data=json.dumps(item,ensure_ascii=False)
        #保存
        self.file.write(data)
        print("管道结束工作")
        return item

三、数据建模与请求

0.目标

  1. 在scrapy项目中进行建模
  2. 构造Requests对象,并发送请求
  3. 利用meta参数在不同的解析函数中传递数据

通常在做项目的过程中,在items.py文件中进行数据的建模

1.数据建模

1.1 建模的原因

  1. 定义item可以提前规划好哪些字段需要爬取,在运行过程中,系统会自动检查爬取内容是否与字段符合
  2. 配合注释可以清晰的知道要抓取哪些字段
  3. 使用scrapy的一些特定组件需要item做支持

1.2 如何建模

在items.py文件中定义要提取的字段:

class MyspiderItem(scrapy.Item):#继承父类scrapy.Item
    name = scrapy.Field()
    	......

字段定义格式:

字段名=scrapy.Field()

1.3 如何使用模版

模版在定义后需要在爬虫中导入并实例化,实例化后的对象用法与字典相同

使用from myspider.items import MyspiderItem导入到爬虫文件中,并实例化类MySpiderItem来使用模版

注:

  • 在使用模版时,可以将实例化对象当做固定了键的字典使用,若给不存在的键进行赋值则会报错

  • 模版对象可以直接使用yield返回出爬虫

  • 模版对象可以直接使用dict强制转换为真正的字典

1.4 目前开发流程

  1. 使用scrapy startproject <projectName>来创建项目
  2. items.py文件中对计划爬取的数据进行建模
  3. 使用scrapy genspider <spiderName> <allowedDonation>来创建爬虫,并进一步完成爬虫
  4. pipelines.py文件中定义管道,并在settings.py中注册启用管道

2.构造Requests对象,发送请求

2.1 实现方法

  1. 确定url地址
  2. 使用scrapy.Request(url,callback)构造一个请求对象
    • callback:指定解析函数名称,表示该请求返回的响应由那一个函数进行解析,默认为当前的parse函数
    • 该函数的参数不止这两个,其他参数见后文
  3. 使用yield将请求对象交给引擎

发送请求常常用于在起始页面内寻找新的页面,如翻页的操作

2.2 例:豆瓣豆瓣读书Top250

需要数据:书名、标签、评分、评价人数、选句

在这里插入图片描述

2.2.1 创建项目
scrapy startproject doubanbook
2.2.2 创建爬虫
scrapy genspider dbb douban.com 
2.2.3 数据建模

items.py

import scrapy
class DoubanbookItem(scrapy.Item):
    name=scrapy.Item()#书名
    tag=scrapy.Item()#标签
    soccer=scrapy.Item()#评分
    num=scrapy.Item()#评价人数
    paragram=scrapy.Item()#选句

2.2.4 完善爬虫

dbb.py

import scrapy
#3.导入模型
from doubanbook.items import DoubanbookItem

class DbbSpider(scrapy.Spider):
    name = "dbb"
    #2.检查域名范围
    allowed_domains = ["douban.com"]
    #1.更改起始url
    start_urls = ["https://book.douban.com/top250"]

    def parse(self, response):
        #爬取内容
        #获取所有的图书节点
        nodes=response.css(".article>.indent>table>.item>:nth-child(2)")
        #提取信息
        for i in range(len(nodes)):
            #使用模版
            items=DoubanbookItem()
            #书名
            items['name']=nodes[i].css("td>.pl2>a::text").extract_first().strip()
            #标签
            items['tag']=nodes[i].css('td>p[class="pl"]::text').extract_first().strip()
            #评分
            items['soccer']=nodes[i].css('td span[class="rating_nums"]::text').extract_first().strip()
            #评分人数
            items['num']=nodes[i].css('td span[class="pl"]::text').extract_first().replace(")","").replace("(","").strip()
            #选句
            items['quote']=str(nodes[i].css('td>p[class="quote"]>span::text').extract_first()).strip()
            #将数据交给引擎
            yield items

        #模拟翻页
        #获取下一页的url
        next_url=response.css('div[class="paginator"]>span[class="next"]>a::attr(href)').extract_first()
        if next_url:#当下一页url存在时
            print(next_url)
            #将请求交给引擎
            yield scrapy.Request(url=next_url, callback=self.parse)
        else:#不存在时
            print("爬取完毕")
  • 若在运行爬虫时报错DEBUG: Forbidden by robots.txt可以前往settings.py文件中将ROBOTSTXT_OBEY改为False
  • 还可以在settings.py启用USER_AGENTDEFAULT_REQUEST_HEADERS
2.2.5 储存数据

pipelines.py

from itemadapter import ItemAdapter
#导入json库
import json

class DoubanbookPipeline:
    def __init__(self):
        #打开本地的json文件来储存数据
        self.file=open("datas.json","w+")
        self.file.write('{"datas":[')
    def __del__(self):
        #关闭文件
        self.file.write(']}')
        self.file.close()

    def process_item(self, item, spider):
        #将传过来的数据变为字典后转换为json字符串,并添加逗号和换行符来分隔不同的数据
        jsonstr=json.dumps(dict(item),ensure_ascii=False)+",\n"
        self.file.write(jsonstr)
        return item
  • 编写完成后记得在settings.py中启用管道
  • 使用scrapy crawl dbb来启动爬虫

2.3 Request的更多参数

scrapy.Request(url[,callback,method="GET",headers,body,cookies,meta,dont_filter=False])
  • url
    • 访问的链接
  • callback
    • 处理响应的函数,默认为parse函数,即self.parse
  • method
    • 请求类型,默认为GET(我们一般不用Request构造post请求)
  • headers
    • 不包含Cookie的请求头
  • body
    • post请求的请求体,接受一个json格式数据
  • cookies
    • 请求用到的cookie
  • meta
    • 实现数据在不同的解析函数中传递,meta默认带有部分数据
  • dont_filter
    • 默认为False,表示会过滤重复请求的url,若设置为True,则不会过滤重复的请求

2.4 meta的使用

1)使用介绍
  • meta用于在不同的解析函数中传递数据,常常用于当我们的数据不在同一页面时,通过meta传参来整合数据
  • meta参数接收一个字典值,字典中存放我们需要传递的数据
  • 在使用的时候,我们可以通过response.meta的方式调用我们传入的字典
  • meta字典中有一个固定的键proxy,表示代理ip,代理ip的使用见scrapy的下载中间键部分
2)完善上例,增加详情信息

通过点击书名,我们可以看到更详细的标签信息如下

在这里插入图片描述

故,我们在爬取排行榜页面时,我们可以将详情页的url也一并爬取

并将标签删除,换成更加精细的作者出版社出版年定价装帧丛书ISBN

更改以下文件

items.py

import scrapy


class DoubanbookItem(scrapy.Item):
    name=scrapy.Field()#书名
    soccer=scrapy.Field()#评分
    num=scrapy.Field()#评价人数
    quote=scrapy.Field()#选句
    url=scrapy.Field()#详情页链接
    author=scrapy.Field()#作者
    publisher=scrapy.Field()#出版社
    date=scrapy.Field()#出版年
    price=scrapy.Field()#定价
    decoration=scrapy.Field()#装帧
    series=scrapy.Field()#丛书
    ISBN=scrapy.Field()#ISBN

dbb.py

import scrapy
#3.导入模型
from doubanbook.items import DoubanbookItem
import re

class DbbSpider(scrapy.Spider):
    name = "dbb"
    #2.检查域名范围
    allowed_domains = ["douban.com"]
    #1.更改起始url
    start_urls = ["https://book.douban.com/top250"]

    def parse(self, response):
        #爬取内容
        #获取所有的图书节点
        nodes=response.css(".article>.indent>table>.item>:nth-child(2)")
        #提取信息
        for i in range(len(nodes)):
            #使用模版
            items=DoubanbookItem()
            #书名
            items['name']=nodes[i].css("td>.pl2>a::text").extract_first().strip()
            #评分
            items['soccer']=nodes[i].css('td span[class="rating_nums"]::text').extract_first().strip()
            #评分人数
            items['num']=nodes[i].css('td span[class="pl"]::text').extract_first().replace(")","").replace("(","").strip()
            #选句
            items['quote']=str(nodes[i].css('td>p[class="quote"]>span::text').extract_first()).strip()

            #更改部分
            #url
            items['url']=nodes[i].css("td>.pl2>a::attr(href)").extract_first().strip()
            #这里数据并未爬取完成,故不需要将数据交给引擎,我们需要进入详情页获取更多信息,
            #故需要构建请求,并把未爬取完成的数据通过meta发送到新的解析函数内去
            yield scrapy.Request(url=items['url'],callback=self.parse_detail,meta={'items':items})
            # #将数据交给引擎
            # yield items

        #模拟翻页
        #获取下一页的url
        next_url=response.css('div[class="paginator"]>span[class="next"]>a::attr(href)').extract_first()
        if next_url:#当下一页url存在时
            print(next_url)
            #将请求交给引擎
            yield scrapy.Request(url=next_url, callback=self.parse)
        else:#不存在时
            print("爬取完毕")

    def parse_detail(self, response):
        #获取传递过来的信息
        items=response.meta['items']
        #详细信息节点
        node=response.css(".subject.clearfix")
        #作者
        items['author']="/".join(node.css("#info>span:first-of-type>a::text").extract())
        #出版社
        items['publisher']=node.css("#info>a:first-of-type::text").extract_first().strip()
        #使用正则表达式匹配
        datas = node.css("#info").extract_first()
        #出版年
        items["date"]=re.search(r'<span class="pl">出版年:</span>(.*?)<br>',datas).group(1).strip()
        #定价
        items["price"]=re.search(r'<span class="pl">定价:</span>(.*?)<br>',datas).group(1).strip()
        #装帧
        items["decoration"]=re.search(r'<span class="pl">装帧:</span>(.*?)<br>',datas).group(1).strip()
        #丛书
        items["series"]=re.search(r'<span class="pl">丛书:</span>(.*?)<a href="(.*?)">(.*?)</a><br>',datas).group(3).strip()
        #ISBN
        items["ISBN"]=re.search(r'<span class="pl">ISBN:</span>(.*?)<br>',datas).group(1).strip()

        #将数据交给引擎
        yield items

四、scrapy模拟登录

0.目标

  1. 请求对象cookies参数的使用
  2. start_reequests函数的作用
  3. 构造并发送post请求

1.scrapy模拟登录方法

  1. 直接携带cookies请求页面
  2. 找到url地址,发生post请求储存cookie(抓包)

2.直接携带cookie获取登录后的页面

对于Requests请求,我们可以直接指定其cookies参数为一个字典来携带cookie,但是对于起始请求,则有一些不同。

2.1 使用条件

  • cookie过期时间很长(常见于一些不规范的网站)
  • 能在cookie过期前将数据拿到
  • 配合其他程序使用,如使用selenium将登录之后的cookie获取保存到本地,scrapy发送请求之前先读取本地cookie

2.2 起始请求的原理及携带cookie的方法

我们的爬虫都继承自类scrapy.Spider,因此,我们可以在该类中找到起始请求相关的方法

打开scrapy.Spider类的定义,我们可以找到一个名为start_requests的方法,该方法即为我们需要查看的发送初始请求的方法

该方法定义如下

    def start_requests(self) -> Iterable[Request]:
        #该部分为抛出错误,可以跳过
        if not self.start_urls and hasattr(self, "start_url"):
            raise AttributeError(
                "Crawling could not start: 'start_urls' not found "
                "or empty (but found 'start_url' attribute instead, "
                "did you miss an 's'?)"
            )
        #该部分为起始请求的构建部分
        for url in self.start_urls:
            yield Request(url, dont_filter=True)

可以看到,起始请求的发起过程中向引擎发送了请求,请求格式为Request(url,dont_filter=True),可以看到,起始请求默认不会被调度器筛选,且起始请求默认不带有cookies参数,而我们想要让起始请求带上cookie进行请求,于是,我们需要做的就很明了了——重构start_requests方法

2.3 重构start_requests方法

重构start_requests方法时,我们主要修改的部分为下面的构造请求的部分,我们想要让他构造请求时带上cookie,故只需要给Requests添加参数cookies即可

可以在我们的爬虫文件中对start_requests方法进行如下重构

    def start_requests(self):
        cookies={}#cookie字典
        for url in self.start_urls:
            yield scrapy.Request(url, dont_filter=True,cookies=cookies)

2.4 例:github登录测试

首先登录上github,获取用户详情页的cookie,接着将cookie加入到初始请求中,打印出页面的标题

git1.py

import scrapy


class Git1Spider(scrapy.Spider):
    name = "git1"
    allowed_domains = ["github.com"]
    start_urls = ["https://github.com/Yuanjilin"]
    def start_requests(self):
        #从浏览器上拿出我们的cookie
        temp='_octo=GH1.1.1716492497.1696557418; _device_id=7cd2c053d6e3a01f2175f3a8c568d91b; preferred_color_mode=light; tz=Asia%2FShanghai; disabled_global_site_banners=universe24_super_early_bird_extended; saved_user_sessions=175548845%3A-my24-QpvnIzhbyJGeUetyLZx3nl6AIwgu3koryZMU6ooU1S; user_session=-my24-QpvnIzhbyJGeUetyLZx3nl6AIwgu3koryZMU6ooU1S; __Host-user_session_same_site=-my24-QpvnIzhbyJGeUetyLZx3nl6AIwgu3koryZMU6ooU1S; tz=Asia%2FShanghai; color_mode=%7B%22color_mode%22%3A%22auto%22%2C%22light_theme%22%3A%7B%22name%22%3A%22light%22%2C%22color_mode%22%3A%22light%22%7D%2C%22dark_theme%22%3A%7B%22name%22%3A%22dark%22%2C%22color_mode%22%3A%22dark%22%7D%7D; logged_in=yes; dotcom_user=Yuanjilin; _gh_sess=e2ZAwHwNDlsVqbVQXY9W%2Bk%2B5FMIeJFjYdzi1srlC4oSTPXYUBu3Focd4EuQH%2Bych%2F7byBuRu8KulTWxymppGLQ4jeY8AWt9f7hIteG9prS%2BNhf7ArM0MKh%2BKbg8pJ65Wj3mmPKGEeKKl%2F%2F%2BmtlQkzjd3aknLVMtr%2FZtzguxx%2FzUnGjubiXDI%2BfcWdtY2p2Lva1IdkKpgQ%2Bw40CjfdB9c%2FRAUYGg4yafzk4f4DHHetucQurJnZKBrRqryhAWaHZ%2BY%2FG1k48YH2cePdZwnP8jPgXeuqDG9PEz8IzfQvAZv%2BeVhhawXtrqmiOFyCfCBdGMMGOOTFk2LV8mu2U7B4O60QVeqJnE68jtDHFs3N6CTPr0aOt%2F4xOkOH8Bnr03re1e5lKhOK1G1eOKbXGy5%2Ba1HVEXzolGeeeMB8otCpmw0uthhlr1KjWdIAw%2BxSVmcBwXK0UKcCUFsknsdWbm8muwbAcmYuUY5r4YM0sI3B5GCtu6pZa8pgNKwcmbyuiAA%2B%2BvOOx89%2FPYYT5MMyrlQ1GJ38JWfI0eSBqI0QzirjwL%2Baf4VVbq9mnn%2FAeeUKYo2%2BoO0dOemdvegDo%2F2BUo0Us0vjBKg41s%2F7AKtk4n6SRp3Ml%2BSKWC3r3JtYOvJ002sN6%2FYXtDV1gldh%2Fw78fV83tDpLUWJW%2F9ykEMVJC%2FLmeKZTsWG2He90WNmCAhaw235Qthe8LVM9D%2F5tGwwLROlRuXAbnBu8rDsduFB5ttm29Hp7G89r%2BawUdSB3TuTJdYn8c%2BQSI7sWFjP0t9h33CeX%2FV%2BokeQdXJTOrq1Aec8SgUt%2Fo6U0c%2F7NzZw5Jh%2BoutQjRUQsvNk%2FgbPIhLH1u%2Bqv%2F%2FBSFZyIjk74r56uCV6E8tmssQ0gsjmg0rvvQ%3D%3D--gc1NLyU6q3nhlXwz--sn%2FV2MjVN9nfYMWcgbRhpg%3D%3D'
        #将字符串cookie处理为字典(使用字典推导式)
        cookies={i.split("=")[0]:i.split("=")[-1] for i in temp.split(';')}
        for url in self.start_urls:
            yield scrapy.Request(url, dont_filter=True,cookies=cookies)

    def parse(self, response):
        #输出html的head中的title标签内容
        print(response.css("head title::text").extract_first())

结果

Yuanjilin
#若未加上cookie,则结果为
#Yuanjilin · GitHub

3.发送post请求登录

3.1 发送post请求的方法

  • 指定scrapy.Request()指定method、body参数来发送post请求
    • 该方法灵活性更高,适用于需要自定义请求头和请求体的情况
    • 该方法使用情况较少
  • 通过scrapy.FormRequest()来发送post请求
    • 该方法更适合传统的表单数据提交,可以发送表单和ajax请求
    • 该方法使用较多,下面以该方法进行讲解

3.2 FormRequests参数

  • url
    • 发送请求的链接
  • callback
    • 处理请求的方法
  • formdata
    • 请求体

3.3 例:模拟登录github

3.3.1 目标
  1. 从登录界面发送post请求
  2. 访问个人主页,观察是否包含用户名,以验证是否成功登录
3.3.2 寻找登录表单的post地址

github登录页面url为:https://github.com/login

在该页面中,我们可以找到登录请求的地址为/session
在这里插入图片描述

则我们需要请求的地址为https://github.com/session

我们首先进行一次常规的登录操作,并对该操作进行网络抓包

在这里插入图片描述

通过抓包,我们可以看到https://github.com/session的请求详情

我们将其的请求体(负载)拿下,放在我们的爬虫中(token为动态的令牌,可以通过分析表单元素来提取)

git2.py

import scrapy


class Git2Spider(scrapy.Spider):
    name = "git2"
    allowed_domains = ["github.com"]
    start_urls = ["https://github.com/login"]
    def parse(self, response):
        #token为登录验证的令牌,可以在表单中寻找并提取出来
        a_token=response.css("form>input[name=\"authenticity_token\"]::attr(value)").extract_first()
        form_data={
                'commit': 'Sign in',
                'authenticity_token':a_token ,
                'login': '此处账号隐去',
                'password': '此处密码隐去',
                'webauthn-conditional':'undefined',
                'javascript-support':'true',
                'webauthn-support': 'supported',
                'webauthn-iuvpaa-support': 'unsupported',
                'return_to': 'https://github.com/login'
        }
        #构建post请求并返回
        yield scrapy.FormRequest(
            #post请求地址
            url="https://github.com/session",
            #post请求体
            formdata=form_data,
            #解析函数
            callback=self.after_login
        )
    def after_login(self,response):
        #向构建个人主页的请求,检验是否登录成功
        yield scrapy.Request(url="https://github.com/Yuanjilin",callback=self.check)
    def check(self,response):
        #提取出页面标题,检验是否登录成功
        print(response.css("head title::text").extract_first())

#输出结果
Yuanjilin
#不带Github,表示登录成功

五、scrapy管道使用

0.目标

  • 掌握scrapy管道的进一步使用

1.pipeline中常用的方法

  1. process_item(self,item,spider)
    • 管道中必须有的函数
    • 实现对item数据的 处理
    • 必须return item
  2. open_spider(self,spider)
    • 在爬虫开启时执行一次
    • 用于打开文件等初始化操作
    • 可以代替前面用到的__init__方法
  3. close_spider(self,spider)
    • 在爬虫关闭的时候执行一次
    • 用于关闭文件等清理操作
    • 可以代替前面用到的__del__方法

2.管道使用注意点

  • 使用之前必须要在settings中开启
  • pipeline在settings中的键表示执行优先级,值小的优先级高
  • 有多个pipeline的时候,可以使用pipeline的参数spider来进行管道使用控制(如对spider的name字段进行判断),否则所有的管道都会执行

3.例:修改豆瓣读书爬虫,创建简化版爬虫

  1. 生成爬虫

    • cmd中运行命令scrapy genspider dbb_simple douban.com 生成爬虫dbb_simple
  2. 对简化版数据进行建模

    • 修改items.py文件,在其中添加如下模型

    • class DoubanbookSimpleItem(scrapy.Item):
          name=scrapy.Field()#书名
          soccer=scrapy.Field()#评分
          num=scrapy.Field()#评价人数
          quote=scrapy.Field()#选句
      
  3. 完善爬虫dbb_simple

    • dbb_simple.py

    • import scrapy
      from doubanbook.items import DoubanbookSimpleItem
      
      class DbbSimpleSpider(scrapy.Spider):
          name = "dbb_simple"
          # 2.检查域名范围
          allowed_domains = ["douban.com"]
          # 1.更改起始url
          start_urls = ["https://book.douban.com/top250"]
      
          def parse(self, response):
              # 爬取内容
              # 获取所有的图书节点
              nodes = response.css(".article>.indent>table>.item>:nth-child(2)")
              # 提取信息
              for i in range(len(nodes)):
                  # 使用模版
                  items = DoubanbookSimpleItem()
                  # 书名
                  items['name'] = nodes[i].css("td>.pl2>a::text").extract_first().strip()
                  # 评分
                  items['soccer'] = nodes[i].css('td span[class="rating_nums"]::text').extract_first().strip()
                  # 评分人数
                  items['num'] = nodes[i].css('td span[class="pl"]::text').extract_first().replace(")", "").replace("(","").strip()
                  # 选句
                  items['quote'] = str(nodes[i].css('td>p[class="quote"]>span::text').extract_first()).strip()
                  #将数据交给引擎
                  yield items
      
              #模拟翻页
              #获取下一页的url
              next_url=response.css('div[class="paginator"]>span[class="next"]>a::attr(href)').extract_first()
              if next_url:#当下一页url存在时
                  print(next_url)
                  #将请求交给引擎
                  yield scrapy.Request(url=next_url, callback=self.parse)
              else:#不存在时
                  print("爬取完毕")import scrapy
      from doubanbook.items import DoubanbookSimpleItem
      
      class DbbSimpleSpider(scrapy.Spider):
          name = "dbb_simple"
          allowed_domains = ["douban.com"]
          start_urls = ["https://douban.com"]
      
          def parse(self, response):
              # 2.检查域名范围
              allowed_domains = ["douban.com"]
              # 1.更改起始url
              start_urls = ["https://book.douban.com/top250"]
      
              def parse(self, response):
                  # 爬取内容
                  # 获取所有的图书节点
                  nodes = response.css(".article>.indent>table>.item>:nth-child(2)")
                  # 提取信息
                  for i in range(len(nodes)):
                      # 使用模版
                      items = DoubanbookSimpleItem()
                      # 书名
                      items['name'] = nodes[i].css("td>.pl2>a::text").extract_first().strip()
                      # 评分
                      items['soccer'] = nodes[i].css('td span[class="rating_nums"]::text').extract_first().strip()
                      # 评分人数
                      items['num'] = nodes[i].css('td span[class="pl"]::text').extract_first().replace(")", "").replace("(","").strip()
                      # 选句
                      items['quote'] = str(nodes[i].css('td>p[class="quote"]>span::text').extract_first()).strip()
                      #将数据交给引擎
                      yield items
      
  4. 修改管道

    • items.py

    • 
      from itemadapter import ItemAdapter
      #导入json库
      import json
      #原管道
      class DoubanbookPipeline:
          def open_spider(self,spider):
              #判断爬虫是否为原版爬虫
              if spider.name=="dbb":
                  #打开本地的json文件来储存数据
                  self.file=open("dbb.json","w+",encoding="gbk")
                  self.file.write('{"datas":[')
          def close_spider(self,spider):
              if spider.name=="dbb":
                  #关闭文件
                  self.file.write(']}')
                  self.file.close()
      
          def process_item(self, item, spider):
              if spider.name=="dbb":
                  #将传过来的数据变为字典后转换为json字符串,并添加逗号和换行符来分隔不同的数据
                  jsonstr=json.dumps(dict(item),ensure_ascii=False)+",\n"
                  self.file.write(jsonstr)
              #!无论是否对数据进行处理,都要返回item,不然后面执行的管道得到的item为None,导致错误
              return item
      
      #精简管道
      class DoubanbookSimplePipeline:
          def open_spider(self,spider):
              #判断爬虫是否为精简爬虫
              if spider.name=="dbb_simple":
                  #打开本地的json文件来储存数据
                  self.file=open("dbb_simple.json","w+",encoding="gbk")
                  self.file.write('{"datas":[')
          def close_spider(self,spider):
              if spider.name=="dbb_simple":
                  #关闭文件
                  self.file.write(']}')
                  self.file.close()
      
          def process_item(self, item, spider):
              if spider.name=="dbb_simple":
                  #将传过来的数据变为字典后转换为json字符串,并添加逗号和换行符来分隔不同的数据
                  jsonstr=json.dumps(dict(item),ensure_ascii=False)+",\n"
                  self.file.write(jsonstr)
              #!无论是否对数据进行处理,都要返回item,不然后面执行的管道得到的item为None,导致错误
              return item
      
    • 记得启用管道!

  5. 运行爬虫即可得到精简版数据

六、crawlspider爬虫类

0.介绍

crawlspider类继承自spider类,为对spider类的一个扩展,可以自动根据正则规则提取链接构建请求发送给引擎

注:crawlspider内不含有parse方法,即他是自动开始提取链接的

1.创建方式

通过在创建爬虫的语句上添加可选参数-t crawl来使用模板crawl创建爬虫

scrapy genspider -t crawl 爬虫名 允许的域

如:

来到我们之前的那个豆瓣读书top250项目目录下

scrapy genspider -t crawl dbb_crawl douban.com

在这里,我们生成了一个crawl爬虫dbb_crawl

该爬虫初始内容如下:

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule


class DbbCrawlSpider(CrawlSpider):
    name = "dbb_crawl"
    allowed_domains = ["douban.com"]
    start_urls = ["https://douban.com"]

    rules = (Rule(LinkExtractor(allow=r"Items/"), callback="parse_item", follow=True),)

    def parse_item(self, response):
        item = {}
        return item

2.分析crawlspider

2.1 新增类

crawlspider相较于spider新添加了如下几个类:

  • CrawlSpider

    • 我们需要继承的爬虫类
  • LinkExtractor

    • 链接提取器,用于提取链接
    • 常用的构造参数
      • allow=()
        • 需要爬取的URL正则表达式
      • deny=()
        • 不需要爬取的URL正则表达式
      • allow_domains|deny_domains=()
        • 需要|不需要爬取的域名
  • Rule

    • 链接提取器需要用到的提取规则
    • 常用的构造参数
      • link_extractor
        • 链接提取器对象,表示要提取的链接
        • 链接被提取到后会被构造成一个请求
      • callback
        • 处理响应的解析函数名
        • 该出应该传入字符串格式的解析函数名
      • follow
        • 是否在解析函数中再次对Rule进行调用
        • 即是否在新的页面中再次进行连接爬取

2.2 parse函数

通过查看生成的代码,我们可以发现生成的类中没有parse方法,这是因为crawlspider已经为我们重写了parse方法,一般情况下我们是不需要去考虑parse方法的

由于我们不会手动构建请求,故当涉及到了数据需要在多个页面上采集时,我们无法使用meta传参,即crawlspider爬虫通常无法处理分散的数据

当我们要采集的数据在多个页面上时,我们不能使用crawlspider进行采集

2.3 例:豆瓣读书Top250

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

class DbbCrawlSpider(CrawlSpider):
    name = "dbb_crawl"
    allowed_domains = ["douban.com"]
    #1.修改起始地址
    start_urls = ["https://book.douban.com/top250"]

    #2.编写爬取规则
    rules = (
        #详情页URL
        Rule(LinkExtractor(allow=r"https://book.douban.com/subject/\d+/"), callback="parse_detail", follow=False),
        #详情页的URL为https://book.douban.com/subject/任意数字/
        #详情页中我们不需要再去提取url,故将follow设为False
#https://book.douban.com/top250?start=25
        #翻页URL
        Rule(LinkExtractor(allow=r'https://book.douban.com/top250\?start=\d+'),follow=True),
        #翻页的url为https://book.douban.com/top250?start=任意数字
        #在下一页中我们仍然需要查找下一页,故将follow设置为True

    )
    #当我们调用翻页时,会自动再继续执行上面两个规则,发送详情页请求以及新的页面的请求

    def parse_detail(self, response):
        #简单采集一下
        item = {}
        #书名
        item['name']=response.css('h1>span::text').extract_first()
        #作者
        item['author']=response.css('#info>:nth-child(1)>a::text').extract_first()
        #评分
        item['soccer']=response.css('.rating_num::text').extract_first()

        #输出采集结果
        print(item)

七、中间件

0.目标

  1. 使用中间件实现随机UA的方法
  2. 使用中间件实现代理ip的方法
  3. scrapy与selenium配合使用

1.scrapy中间件的分类和作用

1.1 中间件的分类

根据scrapy运行流程中所在位置不同可以分为

  • 下载中间件
  • 爬虫中间件

1.2 中间件的作用

scrapy中,中间件主要负责预处理request和response对象

  • 对header以及cookie进行更换和处理
  • 使用代理ip等
  • 对请求进行定制化操作

在scrapy默认情况下,两种中间件都在middlewares.py

爬虫中间件使用方法和下载中间件都相同,且功能重复,故我们通常使用下载中间件

2.下载中间件的使用方法

打开middlewares.py文件,在这个文件中我们可以看到scrapy已经帮我们定义好了爬虫中间件和下载中间件,我们重点关注下载中间件

其内容应该与下面的相似

class DoubanbookDownloaderMiddleware:

    @classmethod
    def from_crawler(cls, crawler):
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_request(self, request, spider):
        return None

    def process_response(self, request, response, spider):
        return response

    def process_exception(self, request, exception, spider):
        pass

    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)

我们重点关注process_request方法process_response方法

  • process_request(self, request, spider)
    • 当请求想要交给下载器而经过下载中间件时,该方法会被调用
    • 该方法可以有三种返回值
      • None:没有return时返回的也是None,该request对象会传递给下载器,或通过引擎传递给其他权重低的process_request方法
      • Request对象:将request对象返回给引擎,引擎再交给调度器,此时原来的那个request对象将不再通过其他权重低的process_request方法
        • 相当于中间件从这个链接中又提取了新的链接,只需要下载新链接的内容,老链接中的内容不需要下载
      • Response对象:不将请求发给下载器,直接将Response对象返回给下载器
        • 相当于中间件充当了下载器的作用
  • process_response(self, request, response, spider)
    • 当下载器完成请求下载后,将响应传递给引擎的时候会调用该方法
    • 其返回值有两个:Request对象或Response对象,其作用与process_request相同

我们在编写完中间件后,一定要去settings.py中开启中间件,并设置权重值,权重值越小优先级越高

3.实现随机请求头

3.1 原因

当我们使用爬虫爬取网页时,如果使用同一个请求头进行大量爬取,则网站很有可能会通过这一个请求头捕捉到我们的异常请求,鉴定出我们的爬虫,然后对我们的ip进行限制,故我们需要使用随机的请求头,以躲避网站的反爬机制

3.2 设置请求头

让我们新建一个项目,用于爬取豆瓣电影top250

scrapy startproject DouBanMovie 
scrapy genspider dbm douban.com

请求头的设置在文件settings.py中,其字段为USER_AGENT

请求头可以从浏览器上获得,也可以在网上获得

当我们启用了USER_AGENT字段时,我们的爬虫请求都会自带该字段设置的请求头

settings.py中设置请求头如下

USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0"

编写如下爬虫

import requests
import scrapy


class DbmSpider(scrapy.Spider):
    """豆瓣电影top250"""
    name = "dbm"
    allowed_domains = ["douban.com"]
    start_urls = ["https://movie.douban.com/top250"]

    def parse(self, response):
        #输出每个页面请求的UA
        print(response.request.headers['User-Agent'])

        #电影简介的节点
        nodes=response.css(".info")
        #由于这里仅做UA设置演示,故只爬取名字,实际爬虫的时候应该爬取更多更详细的数据
        for node in nodes:
            #这里直接使用字典,实际爬取时需要设置模版
            test_item={}
            test_item['name']=node.css(".title::text").extract_first()

        #翻页
        next_page_url=response.css(".next>a::attr(href)").extract_first()
        if next_page_url!=None:
            next_page_url=response.urljoin(next_page_url)
            yield scrapy.Request(url=next_page_url)

输出结果

b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0'
b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0'
b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0'
b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0'
......

可以看到,我们成功设置并使用了自定义的请求头,并且所有的请求用的都是同一个请求头

3.3 设置随机请求头

为了迷惑网站的反爬虫机制,我们需要设置随机的请求头

  1. 首先,在settings.py中自定义一个字段USER_AGENTS(名字自定义,自己记得就行)

  2. 接着,上网查找多个请求头,将这些请求头放入USER_AGENTS

    • USER_AGENTS=[
       'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36',
       'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0',
       'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36 OPR/63.0.3368.43',
       'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362',
       'User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; LCTE; rv:11.0) like Gecko',
       'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/534.54.16 (KHTML, like Gecko) Version/5.1.4 Safari/534.54.16',
       'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3722.400 QQBrowser/10.5.3739.400',
       'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE'
      ]
      
  3. 然后进入middlewares.py文件中编辑中间件

    • middlewares.py文件中应该会有已经定义好的中间件,我们可以在原基础上进行更改,也可以自己新建中间件,该处我们演示使用自己新建中间件

    • 由于请求头是在发送请求的时候使用,故我们需要在发送请求之前更改UA

    • 即我们需要编写process_request方法

    • from scrapy import signals
      #导入随机库
      import random
      #导入我们定义的请求头
      from DouBanMovie.settings import USER_AGENTS
      class RandomUA:
          #请求过程操作
          def process_request(self, request, spider):
              #将UA随机设置
              request.headers['User-Agent']=random.choice(USER_AGENTS)
              #返回None时,请求会交给下载器进行下载
              return None
      
  4. settings.py中启用中间件

    • DOWNLOADER_MIDDLEWARES = {
          "DouBanMovie.middlewares.RandomUA": 543,
      }
      
      

现在,当我们运行爬虫时,会看到输出的请求头应该如下:

b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
b'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/534.54.16 (KHTML, like Gecko) Version/5.1.4 Safari/534.54.16'
b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
b'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/534.54.16 (KHTML, like Gecko) Version/5.1.4 Safari/534.54.16'
b'User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; LCTE; rv:11.0) like Gecko'
b'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3722.400 QQBrowser/10.5.3739.400'

可以看到,请求头变成随机的了

4.设置随机代理

首先,我们需要获取一些代理,当代理数量少的时候可以直接放在settings.py文件中,当代理数量多的时候需要放在数据库中对其进行维护,此处演示将其放在settings.py文件中

PROXYS=[
    #有密码的代理
    {'ip_port':'183.234.215.11:8443','password':'123456'},
    #无密码的代理
    {'ip_port':'45.92.158.59:9098'}
]

之后,同样的,在我们的middlewares.py文件中编写中间件即可实现随机代理

class RandomIP:
    def process_request(self, request, spider):
        #随机获取一个代理
        proxy=random.choice(PROXYS)
        #检查是否有密码
        if 'password' in proxy:
            #对密码进行编码
            b64_up=base64.b64encode(proxy['password'].encode())
            #设置认证
            request.headers['Proxy-Authorization']=f'Basic {b64_up.decode()}'
        #在meta中添加代理
        request.meta['proxy']=proxy['ip_port']

拓:HTTP基本认证(Basic Authentication)-CSDN博客

5.渲染页面

5.1 方法

在实际爬取网页的过程中,我们会遇到很多需要js渲染的网页,这些网页直接爬取数据是难以爬取到的,故我们需要对其进行渲染再爬取。

在scrapy中,解析响应是由爬虫完成的,爬虫使用的响应是由下载器通过引擎传递过来的。故我们想要渲染页面,必须在引擎-下载器这条路线上进行操作。

scrapy的下载器不支持渲染,故我们应该从下载中间件上下手。我们可以对于需要渲染的页面使用中间件来渲染,将渲染好的response直接返回给引擎,故此时,我们要编写的应该是process_request方法

此处使用selenium库进行渲染演示

5.2 例:爬取B站排行榜数据

网站:哔哩哔哩排行榜 (bilibili.com)

  1. 修改设置,添加请求头

  2. 编写管道

    • import scrapy
      class VideoItem(scrapy.Item):
          ranknum = scrapy.Field()      #排名
          title   = scrapy.Field()      #标题
          author  = scrapy.Field()      #作者
          views   = scrapy.Field()      #播放量
          comment = scrapy.Field()      #评论数
          bv      = scrapy.Field()      #bv号
          pass
      
  3. 编写爬虫

    • import scrapy
      #导入模版
      from bilibili.items import VideoItem
      
      class RanklistSpider(scrapy.Spider):
          name = "rankList"
          allowed_domains = ["bilibili.com"]
          #修改起始路径
          start_urls = ["https://www.bilibili.com/v/popular/rank/all/"]
      
          def parse(self, response):
              #获取所有的视频节点
              nodes=response.css("li[class='rank-item']>div[class='content']")
              for node in nodes:
                  #使用模版
                  item=VideoItem()
                  #排名
                  item['ranknum']=node.css('div[class="img"]>i>span::text').extract_first()
                  #标题
                  item['title']=node.css(".info>a::attr(title)").extract_first()
                  #bv号
                  item['bv']=node.css(".info>a::attr(href)").extract_first().split('/')[0].replace('BV',"")
                  #作者
                  item['author']=str(node.css(".info>.detail>a>span::text").extract_first()).strip()
                  #播放量
                  item['views']=str(node.css(".info>.detail>.detail-state>span:nth-child(1)::text").extract_first()).strip()
                  #评论数
                  item['comment']=str(node.css(".info>.detail>.detail-state>span:nth-child(2)::text").extract_first()).strip()
                  #数据交给引擎
                  yield item
      
  4. 编写管道

    • from itemadapter import ItemAdapter
      import json
      
      
      class BilibiliPipeline:
          def open_spider(self,spider):
              self.f=open('videoDatas.json','w+')
      
          def close_spider(self,spider):
              self.f.close()
      
          def process_item(self, item, spider):
              json_string=json.dumps(dict(item),ensure_ascii=False)+",\n"
              self.f.write(json_string)
              return item
      
      
  5. 修改中间件

    • from scrapy import signals
      from scrapy.http import HtmlResponse
      from selenium import webdriver
      from selenium.webdriver.chrome.service import Service
      from selenium.webdriver.chrome.options import Options
      import time
      
      class BilibiliJS:
          def process_request(self, request, spider):
              url=request.url
              #如果存在部分有渲染需求,部分没有时,可以用判断语句进行条件判断,执行不同的操作
              #如没有渲染需求就返回None,使用下载器进行下载
              #此处示列中均由渲染需求
              #本地chromedriver服务,这样启动会比较快
              service=Service(r'C:\Users\源吉铃·落樱\AppData\Local\futaike\chromedriver.exe')
      
              #创建一个设置
              chrome_options=Options()
              #添加无头设置
              chrome_options.add_argument('--headless')
              #打开一个无头浏览器
              driver=webdriver.Chrome(options=chrome_options,service=service)
              #发送请求
              driver.get(url=url)
              #等待渲染
              time.sleep(3)
              #获取页面
              data=driver.page_source
              #关闭服务
              driver.close()
      
              #创建并返回response对象
              return HtmlResponse(url=url,body=data,encoding='utf-8',request=request)
              #注:该处必须要用return返回,如果使用yield返回,则返回的是一个迭代器对象,会导致报错
      
  6. 启用中间件,管道,并执行爬虫

八、分布式爬虫介绍

1.分布式爬虫概念

简单来说,分布式爬虫就是将多台主机联合起来,一起完成一个爬虫任务,可以充分的利用每一台计算机的硬件与网络资源,提高完成任务的效率。在分布式爬虫网络中的每一个爬虫都称为一个节点,节点之间共享队列共享数据共同完成一个任务

2.scrapy_redis介绍

2.1 概念

scrapy_redis是scrapy框架的基于Redis的分布式组件,其通过持久化请求队列和请求的指纹集合实现了断点续爬分布式快速爬取的功能

2.2 scrapy_redis工作流程

2.2.1 回顾scrapy工作流程

在scrapy的基础上实现分布式爬虫我们可以从如下方面来考虑

  • scrapy通过Scheduler管理URL任务队列,爬虫运行时需要Scheduler从URL队列中取出URL
    • 分布式爬虫中的各个节点应该共享同一个URL队列,故我们应该将URL任务队列从单个节点中抽出单独存放,令各个节点都从这一个URL队列中取出URL
    • 针对于此,我们可以将URL放在一个数据库中,该数据库必须具有去重功能,且能维护数据队列,将未爬取的URL与已爬取的URL正确存放
  • scrapy通过ItemPipline来将数据存储在数据队列中
    • 分布式爬虫的各个节点通常都分布在不同主机上,我们得到的数据也由各个节点管道处理,但是我们的节点是共同完成一个任务,该任务的结果也应该汇总在一起
    • 针对于此,我们可以令ItemPipline将数据都存储在同一数据队列上,即将数据队列也抽出放在一个数据库中
2.2.2 scrapy_redis流程

单个节点流程图

img

  • 在scrapy_redis中,URL队列储存储存在公用服务器的redis数据库中
  • 所有节点公用同一个redis中的请求队列
  • 所有URl存入redis前都会通过该redis中URL指纹集合进行判断是否重复
  • 在默认情况下,所有数据都会保存在redis中

分布式流程图

在这里插入图片描述

对于Redis数据库中内容的注解:

  • 集合
    • 一个去重集合,存放所有节点产生的URL
  • 任务队列
    • 所有节点的调度器都从该队列中获取请求对象
  • redis_key
    • 一个redis的键,储存了起始URL,在爬虫任务开始时,所有的节点都会去争抢该键,哪个节点抢到了该键就由那个节点发起起始请求
  • 数据队列
    • scrapy_redis提供的一个管道,用于将所有节点产生的数据存储到公共Redis数据库上
    • 非必须,亦可以自己定义管道

九.scrapy_redis原理分析并实现断点续爬以及分布式爬虫

0.学习目标

  • 了解scrapy实现去重的原理
  • 了解scrapy中请求入队的条件
  • 掌握scrapy_redis基于url地址的增量式单机爬虫
  • 掌握scrapy_redis分布式爬虫

注:

scrapy_redis需要下载

使用pip install scrapy_redis进行下载

1.scrapy_redis的demo分析

1.1 下载github上的demo

输入以下命令下载demo

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

运行命令后我们会得到一个名字为scrapy-redis的文件夹

该文件夹内会有一个名为example-project的项目文件夹

该文件夹内的example文件夹即为我们需要研究的demo

1.2 分析demo

1.2.1 项目目录

demo的项目目录应该如下:

  • example
    • spiders
      • _init_.py
      • dmoz.py
      • mycrawler_redis.py
      • myspider_redis.py
    • _init_.py
    • items.py
    • pipelines.py
    • settings.py

可以看到,该文件夹为一个标准的scrapy爬虫项目文件夹

注:老版本的scrapy是不会自动生成中间件的,故该文件夹内没有中间件

1.2.2 dmoz.py文件

首先,dmoz.py文件内定义了一个用于爬取demo网站的爬虫,该爬虫使用了crawlspider模版,可以自动根据规则提取新的连接(关于crawlspider模版可以看六、crawlspider爬虫类)

但是,该网站URL现在已经更改了,我们需要将起始URL换为http://odp.org/World/Chinese_Simplified/

并且,我们需要对该爬虫进行更改,使得其适配新的网页

from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
import re

class DmozSpider(CrawlSpider):
    """Follow categories and extract links."""

    name = "dmoz"
    allowed_domains = ["odp.org"]
    start_urls = ["http://odp.org/"]

    rules = [
        Rule(
            LinkExtractor(restrict_css=("#triple>li>a", "#triple>li>a")),
            callback="parse_directory",
            follow=True,
        ),
    ]

    def parse_directory(self, response):
        for div in response.css(".listings"):
            item={}
            #使用css选择器
            item["name"]=div.css("h4>a>strong::text").extract_first()
            item["link"]=div.css("p>font>strong::text").extract_first()
            #该项必须使用正则表达式匹配
            content=div.css("p").extract_first()
            item['description']= re.search(r'<br>(.*?)</p>', content).group(1).strip()

            yield item


1.2.3 settings.py文件

该文件内容应该如下


SPIDER_MODULES = ["example.spiders"]
NEWSPIDER_MODULE = "example.spiders"

USER_AGENT = "scrapy-redis (+https://github.com/rolando/scrapy-redis)"

#设置重复过滤器的模块
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
#设置调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
#是否保持调度器数据
SCHEDULER_PERSIST = True

ITEM_PIPELINES = {
    "example.pipelines.ExamplePipeline": 300,
    #scrapy_redis数据管道
    "scrapy_redis.pipelines.RedisPipeline": 400,
}

LOG_LEVEL = "DEBUG"

#下载延迟
DOWNLOAD_DELAY = 1
  • DUPEFILTER_CLASS
    • 设置重复过滤器的模块
    • 原先未设置该变量时,默认使用python中的集合来进行重复过滤操作
    • 设置该变量后使用的即为redis来进行重复过滤操作
  • SCHEDULER
    • 设置调度器
    • 将调度器从默认的scrapy调度器改为scrapy_redis中的调度器,该调度器具有与redis数据库进行交互的功能
  • SCHEDULER_PERSIST
    • 设置当爬虫结束时(自动或意外结束),是否保持redis数据库中的去重集合与任务队列
    • 该设置与断点续传相关
  • ITEM_PIPELINES中的scrapy_redis.pipelines.RedisPipeline管道
    • 该管道为scrapy_redis提供给我们的管道,该管道开启时,会将数据存放到redis数据库中
    • 该管道不开启时,我们可以自定义管道来实现数据的统一存放
  • DOWNLOAD_DELAY
    • 设置下载延迟,即下载间隔
    • 该参数为可选参数

可以看到上面所有的操作都依赖于redis,而scrapy_redis连接redis的方式如下:

在settings.py文件中添加如下内容:

#方法1
REDIS_URL="redis://<地址>:<端口>"
#方法2
REDIS_HOST="<地址>"
REDIS_PORT="<端口>"

1.3 运行demo

更改爬虫内容,调整好设置,将redis启动后,输入scrapy crawl dmoz启动爬虫

爬虫启动后,我们可以在redis数据库内看到新增了如下三个键

  • dupefilter

    • 去重集合,存放经过哈希处理后的URL
    • 在这里插入图片描述
  • items

    • 数据队列,存放爬取到的数据
    • 在这里插入图片描述
  • requests

    • 请求队列,内为序列化的二进制的请求对象
    • 在这里插入图片描述

在这里插入图片描述

1.4 断点续爬

由于我们在设置中已经设置了SCHEDULER_PERSIST = True,故我们的断点续爬已经打开

当我们关闭爬虫后,redis数据库中请求队列与集合并不会被清除

此时我们重新开始爬虫后,爬虫可以继续从请求队列中获取请求,继续我们之前的数据爬取

1.5 分布式爬虫实现

在demo的爬虫文件夹中还有另外两个分布式爬虫

  • myspider_redis:默认模版爬虫
  • mycrawler_redis:crawler模版爬虫

这两个爬虫即两种分布式爬虫的演示demo

启动分布式爬虫的命令:

scrapy runspider <爬虫文件名>.py [-a domain="<允许的域1>,<允许的域2>,..."]

1.5.1 默认模版爬虫
1.5.1.1 内容解释

首先,打开myspider_redis.py文件

from scrapy_redis.spiders import RedisSpider
class MySpider(RedisSpider):
    name = "myspider_redis"
    redis_key = "myspider:start_urls"
    def __init__(self, *args, **kwargs):
        domain = kwargs.pop("domain", "")
        self.allowed_domains = filter(None, domain.split(","))#需要强制转换为list
        super().__init__(*args, **kwargs)
    def parse(self, response):
        return {
            "name": response.css("title::text").extract_first(),
            "url": response.url,
        }
  • 第1行
    • 首先从scrapy_redis中导入了RedisSpider爬虫类,该爬虫类具有与Redis数据库交互的能力,可以参与争抢起始URL
  • 第2行
    • 编写了自己的爬虫类,该爬虫类继承自RedisSpider,具有与redis数据库交互的功能
  • 第4行
    • 对于普通的爬虫,该处应该为allowed_domainsstart_urls的定义处,在这里变成了redis_key
    • redis_key的内容为该分布式爬虫要参与争抢的存放起始URL的键
  • 第5行
    • 添加了类的构造器,用来动态设置允许的域
    • 首先,第6行的pop方法会尝试从字典kwargs中获取键domain的值,其值应该为允许的域名组成的字符串,各域名之间以*,*分隔
    • 然后,第7行使用过滤器将域名组成的字符串分解为域名组成的列表过滤掉其中的空值
      • python2中filter会将过滤后的值封装为字典返回,但是python3中的filter则会将其封装为filter对象后返回,故此处不能直接使用filter返回值,我们需要将其强制转换为list后才可以使用
  • 第9行即为标准的数据爬取函数pares
1.5.1.2 运行爬虫

首先,我们更改一下爬虫的文件内容

from scrapy_redis.spiders import RedisSpider


class MySpider(RedisSpider):
    name = "myspider_redis"
    redis_key = "startURL"#设置起始URL键为startURL

    def __init__(self, *args, **kwargs):
        domain = kwargs.pop("domain", "")
        self.allowed_domains = list(filter(None, domain.split(",")))
        super().__init__(*args, **kwargs)

    def parse(self, response):
        return {
            "name": response.css("title::text").extract_first(),
            "url": response.url,
        }

打开多个cmd窗口以模拟多个节点,输入启动命令

scrapy runspider myspider_redis.py

在这里插入图片描述

三个爬虫启动后都陷入等待状态,这是因为我们没有设置起始URL,导致三个爬虫都在等待争夺起始URL

但是此时爬虫仍能输出日志,日志中会显示该爬虫每分钟爬取的页面,保存的数据

此时,我们打开进入redis,将startURL设置为www.baidu.com

lpush startURL www.baidu.com

在这里插入图片描述

可以看到,最上面的节点抢到了URL发起了请求,但是由于parse方法内为设置提取新链接的功能,故爬取起始URL后爬虫便停止了

1.5.2 crawler模版爬虫
1.5.2.1 内容解释
#导入提取链接规则相关的类
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import Rule
#导入Redis的CrawlSpider类
from scrapy_redis.spiders import RedisCrawlSpider


class MyCrawler(RedisCrawlSpider):
    name = "mycrawler_redis"
    redis_key = "startURL"

    rules = (
		#提取所有的链接
        Rule(LinkExtractor(), callback="parse_page", follow=True),
    )

    def __init__(self, *args, **kwargs):
        domain = kwargs.pop("domain", "")
        self.allowed_domains =list(filter(None, domain.split(","))) 
        super().__init__(*args, **kwargs)

    def parse_page(self, response):
        return {
            "name": response.css("title::text").extract_first(),
            "url": response.url,
        }
1.5.2.2 运行爬虫

这次我们将https://www.vilipix.com/传入进行提取

在这里插入图片描述

可以看到,中间的节点首先抢到起始URL发起请求,并从页面内提取出链接

在这里插入图片描述

其他三个节点也紧随其后开始了爬取

在这里插入图片描述

同时,当我们停止其中的一个节点后,其他的节点也未受到影响,继续完成爬取任务

2.分布式爬虫的编写流程

  1. 编写普通爬虫
    • 创建项目
    • 明确目标
    • 创建爬虫
    • 保存内容
  2. 改造成分布式爬虫
    1. 改造爬虫
      1. 导入scrapy_redis中对应的分布式爬虫类
      2. 继承分布式爬虫类
      3. 注销start_urlsallowed_domains
      4. 设置redis_key获取start_urls
      5. 设置__init__获取允许的域
    2. 改造配置文件
      • 复制demo内配置

3.分布式爬虫示例

3.1 目标

豆瓣音乐标签 (douban.com)中获取每个地区的音乐信息

要获取的信息具体有:

  • 歌曲地区
  • 歌曲名
  • 表演者
  • 专辑类型
  • 发行时间
  • 豆瓣评分
  • 评价人数
  • 简介

3.2 构建普通爬虫

3.2.1 创建项目

首先,创建项目目录

scrapy startproject dbMusic 

进入项目

cd dbMusic

接着,我们需要明确爬取目标,创建模版

items.py

import scrapy
class DbmusicItem(scrapy.Item):
    #地区
    region = scrapy.Field()
    #歌曲名
    name=scrapy.Field()
    #表演者
    author=scrapy.Field()
    #专辑类型
    stype=scrapy.Field()
    #发行时间
    publish_date=scrapy.Field()
    #豆瓣评分
    score=scrapy.Field()
    #评价人数
    appraise_num=scrapy.Field()
    #简介
    introduction=scrapy.Field()

由于我们需要的地区信息并不在音乐的详情页面里,故这里涉及到从多个页面上采集数据,需要使用meta传参

故这里使用普通的模版生成爬虫

scrapy genspider music douban.com  
3.2.2 更改设置并创建下载中间件
  • 首先,将settings.pyROBOTSTXT_OBEY改为False

  • 然后,定义随机请求头以防止机器人检测

  • #随机请求头	
    USER_AGENTS=[
     'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36',
     'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0',
     'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36 OPR/63.0.3368.43',
     'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18362',
     'User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; LCTE; rv:11.0) like Gecko',
     'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/534.54.16 (KHTML, like Gecko) Version/5.1.4 Safari/534.54.16',
     'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3722.400 QQBrowser/10.5.3739.400',
     'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE'
    ]
    
    • 此时也可以设置动态请求延时,减小被ban的概率

      • #设置爬虫延时
        DOWNLOAD_DELAY=1
        #设置动态延迟
        RANDOMIZE_DOWNLOAD_DELAY=True
        
  • 接着,编写随机UA下载中间件

    • from scrapy import signals
      import random
      #导入我们定义的随机请求头
      from dbMusic.settings import USER_AGENTS
      
      class RandomUA:
          """随机请求头"""
          #编写下载中间件
          def process_request(self, request, spider):
              #设置随机请求头
              request.headers['User-Agent']=random.choice(USER_AGENTS)
              request.headers['Accept']="text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
              request.headers['Accept-Language']='zh-CN,zh;q=0.9'
      
              #返回None时,请求会交给下载器进行下载
              return None
      
  • 然后,在settings.py中启用我们的中间件

    • DOWNLOADER_MIDDLEWARES = {
         "dbMusic.middlewares.RandomUA": 543,#启用随机UA下载中间件
      }
      
3.2.3 编写爬虫
import scrapy
import re
#导入模型
from dbMusic.items import DbmusicItem

class MusicSpider(scrapy.Spider):
    name = "music"
    #检查允许的域名
    allowed_domains = ["douban.com"]
    #修改起始URL
    start_urls = ["https://music.douban.com/tag/"]

    def parse(self, response):
        """起始URL解析"""
        #获取地区/语言板块
        div=response.css('div[id="地区/语言"]')
        #获取单个地区节点
        regions=div.css(".bd>table>tbody>tr>td")
        #遍历地区节点
        for region in regions:
            #创建模版
            item=DbmusicItem()
            #获取地区名
            region_name=region.css("a::text").extract_first()
            #获取地区连接
            region_url=response.urljoin(region.css("a::attr(href)").extract_first())

            #将数据放入模版
            item["region"]=region_name

            #模拟点击地区链接进入对应地区表单
            yield scrapy.Request(
                url=region_url,
                meta={"data":item},#将已有模版传入meta
                headers={"referer": response.url},
                callback=self.region_detail_page#用于解析地区歌曲页面
            )

    def region_detail_page(self,response):
        """解析地区音乐页面"""
        #取出meta中的item
        item=response.meta["data"]
        #获取每个音乐详情的节点
        muss=response.css("table .pl2")
        # pprint(muss)
        #遍历音乐节点
        for mus in muss:
            #获取详情页的链接
            mus_url=mus.css("a::attr(href)").extract_first()
            # print(mus_url)
            #访问详情页
            yield scrapy.Request(
                url=mus_url,
                meta={"data":item},
                headers={"referer":response.url},
                callback=self.detail_page#交给详情页爬取方法处理
            )


        #模拟翻页
        #获取下一页链接
        next_url=response.css(".next>a::attr(href)").extract_first()
        #检测下一页是否存在
        if next_url:
            #拼接完整路径
            next_url=response.urljoin(next_url)
            #构建请求
            yield scrapy.Request(
                url=next_url,
                meta={"data":item},#传递item
                headers={"referer": response.url},
                callback=self.region_detail_page#将获取到的页面交给地区详情处理方法处理
            )


    def detail_page(self,response):
        """爬取详情页的数据"""
        #取出item
        item=response.meta["data"]
        #歌曲名
        item["name"]=response.css("h1>span::text").extract_first()
        #豆瓣评分
        item["score"] =response.css("strong::text").extract_first()
        #评价人数
        item["appraise_num"] =response.css(".rating_sum>a>span::text").extract_first()
        #简介
        item["introduction"] =" ".join(",".join(response.css("#link-report>span::text").extract()).split())
        #表演者
        item["author"]=response.css("#info>span>.pl>a::text").extract_first()

        #下面两项使用正则表达式获取
        html_txt=str(response.css("#info").extract_first())
        #去除空格和换行,避免对正则表达式产生干扰
        content="".join(html_txt.split())
        #专辑类型
        stype=re.search(r'专辑类型:</span>(.*?)<br>',content)
        item["stype"]=stype.group(1).strip() if stype else None
        #发行时间
        pbdate=re.search(r"发行时间:</span>(.*?)<br>",content)
        item["publish_date"]=pbdate.group(1).strip() if pbdate else None

        #输出结果
        print(item)

3.3.4 编写管道

由于等下该爬虫需要改为分布式爬虫,故现在先不编写管道保存数据

3.3 修改为分布式爬虫

3.3.1 修改爬虫文件

打开我们的爬虫文件

music.py

import scrapy
import re
#导入模型
from dbMusic.items import DbmusicItem
#----- 1.导入scrapyRedis中对应的爬虫
from scrapy_redis.spiders import RedisSpider

#----- 2.修改继承的类
class MusicSpider(RedisSpider):
    name = "music"

#----- 3.注销allowed_domains和start_urls
    # #检查允许的域名
    # allowed_domains = ["douban.com"]
    # #修改起始URL
    # start_urls = ["https://music.douban.com/tag/"]

#----- 4.设置redis-key
    redis_key = "startURL"

#----- 5.设置__init__
    def __init__(self, *args, **kwargs):
        domain = kwargs.pop("domain", "")
        self.allowed_domains = list(filter(None, domain.split(",")))
        super().__init__(*args, **kwargs)
#----- 6.爬虫文件修改结束

    def parse(self, response):
        """起始URL解析"""
        #获取地区/语言板块
        div=response.css('div[id="地区/语言"]')
        #获取单个地区节点
        regions=div.css(".bd>table>tbody>tr>td")
        #遍历地区节点
        for region in regions:
            #创建模版
            item=DbmusicItem()
            #获取地区名
            region_name=region.css("a::text").extract_first()
            #获取地区连接
            region_url=response.urljoin(region.css("a::attr(href)").extract_first())

            #将数据放入模版
            item["region"]=region_name

            #模拟点击地区链接进入对应地区表单
            yield scrapy.Request(
                url=region_url,
                meta={"data":item},#将已有模版传入meta
                headers={"referer": response.url},
                callback=self.region_detail_page#用于解析地区歌曲页面
            )

    def region_detail_page(self,response):
        """解析地区音乐页面"""
        #取出meta中的item
        item=response.meta["data"]
        #获取每个音乐详情的节点
        muss=response.css("table .pl2")
        # pprint(muss)
        #遍历音乐节点
        for mus in muss:
            #获取详情页的链接
            mus_url=mus.css("a::attr(href)").extract_first()
            # print(mus_url)
            #访问详情页
            yield scrapy.Request(
                url=mus_url,
                meta={"data":item},
                headers={"referer":response.url},
                callback=self.detail_page#交给详情页爬取方法处理
            )


        #模拟翻页
        #获取下一页链接
        next_url=response.css(".next>a::attr(href)").extract_first()
        #检测下一页是否存在
        if next_url:
            #拼接完整路径
            next_url=response.urljoin(next_url)
            #构建请求
            yield scrapy.Request(
                url=next_url,
                meta={"data":item},#传递item
                headers={"referer": response.url},
                callback=self.region_detail_page#将获取到的页面交给地区详情处理方法处理
            )


    def detail_page(self,response):
        """爬取详情页的数据"""
        #取出item
        item=response.meta["data"]
        #歌曲名
        item["name"]=response.css("h1>span::text").extract_first()
        #豆瓣评分
        item["score"] =response.css("strong::text").extract_first()
        #评价人数
        item["appraise_num"] =response.css(".rating_sum>a>span::text").extract_first()
        #简介
        item["introduction"] =" ".join(",".join(response.css("#link-report>span::text").extract()).split())
        #表演者
        item["author"]=response.css("#info>span>.pl>a::text").extract_first()

        #下面两项使用正则表达式获取
        html_txt=str(response.css("#info").extract_first())
        #去除空格和换行,避免对正则表达式产生干扰
        content="".join(html_txt.split())
        #专辑类型
        stype=re.search(r'专辑类型:</span>(.*?)<br>',content)
        item["stype"]=stype.group(1).strip() if stype else None
        #发行时间
        pbdate=re.search(r"发行时间:</span>(.*?)<br>",content)
        item["publish_date"]=pbdate.group(1).strip() if pbdate else None

        #输出结果
        print(item)
        #将数据返回给引擎
        yield item

3.3.2 修改配置文件

打开settings.py

添加以下内容

DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
SCHEDULER_PERSIST = True

启用scrapy_redis的管道

ITEM_PIPELINES = {
    "example.pipelines.ExamplePipeline": 300,
    "scrapy_redis.pipelines.RedisPipeline": 400,
}

启用日志

LOG_LEVEL = "DEBUG"

配置redis数据库

REDIS_URL="redis://192.168.245.129:6379"

开启下载延时

DOWNLOAD_DELAY = 1
3.3.3 执行
scrapy runspider music.py -a domain="douban.com"

在这里插入图片描述

3个爬虫已经准备就绪

lpush startURL https://music.douban.com/tag/

在这里插入图片描述

可以看到,3个爬虫都开始了爬取

数据也成功的进入了数据库

在这里插入图片描述

Ciallo~(∠・ω< )⌒★

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值