scrapy框架

一:scrapy框架

什么是框架?

  • 就是一个集成了很多功能并且具有很强通用性的一个项目模板

什么是scrapy?

  • 爬虫中封装好的一个框架。功能:高性能的持久化存储,异步的数据下载,高性能的数据解析,分布式

大体过程

创建一个工程

scrapy startproject 工程名

创建爬虫文件

**进入工程目录(cd 工程目录)**后

scrapy genspider 爬虫文件名 www.xxx.com

在spiders子目录中创建一个爬虫文件,其中url为起始url

例如:

scrapy startproject firstBlood  # 工程名
scrapy genspider first www.xxx.com  # 在创建的项目文件夹中 爬虫文件名

scrapy crawl 爬虫文件名 # 执行

结果:

image-20210121000217374

执行爬虫工程

scrapy crawl 爬虫文件名

  • scrapy crawl 文件名 --nolog 去掉日志输出,避免干扰自己代码中定义的输出[但会使错误类型的输出不显示]

    • 可以在settings文件中加==LOG_LEVEL='ERROR'==显示错误相关的输出
  • 爬虫文件名称是爬虫源文件的唯一标识

自动发送请求 与 手动发送请求(scrapy.Request)

spiders子目录下的爬虫文件中

# -*- coding: utf-8 -*-
import scrapy


class FirstSpider(scrapy.Spider):
    #爬虫文件的名称:就是爬虫源文件的一个唯一标识
    name = 'first'
    #允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
    allowed_domains = ['www.baidu.com']  # start_urls中只有这个域名下的url才会访问

    #起始的url列表:该列表中存放的url会被scrapy自动进行请求的发送
    start_urls = ['https://www.baidu.com/','https://www.sogou.com']

    #用作于数据解析:response参数表示的就是请求成功后对应的响应对象,对该相应对象进行解析
    def parse(self, response):  # start_urls有几个url就会调用几次parse
        print(response)

  • start_urls起始的url列表:该列表中存放的url会被scrapy自动进行请求的发送

  • 重点: 如果要手动请求发送:scrapy.Request(url=new_url, callback) 要包含scrapy包

适用于全站数据爬取,爬取不同页面 (其url有一定的规律,与页面号有关) 的数据;当然也可以将所要的url放到start_urls

# 在parse函数中
# 手动请求发送:callback回调函数是专门用作于数据解析  类似递归调用
yield scrapy.Request(url=new_url, callback=self.parse)  
  • callback参数代表对对应url的相应对象,专门用于数据解析的函数,可以自己写一个函数进行数据解析

例子:

# 在spiders文件夹下的爬虫文件中
class XiaohuaSpider(scrapy.Spider):
    name = 'xiaohua'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['http://www.521609.com/meinvxiaohua/']

    #生成一个通用的url模板(不可变)
    url = 'http://www.521609.com/meinvxiaohua/list12%d.html'
    page_num = 2  

    def parse(self, response):
        li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li')
        for li in li_list:
            img_name = li.xpath('./a[2]/b/text() | ./a[2]/text()').extract_first()
            print(img_name)

        if self.page_num <= 3:  # page_num从2开始,因为上面已经对第一页解析过了
            new_url = format(self.url%self.page_num)  # 得到新的页面url
            self.page_num += 1
            #手动请求发送:callback回调函数是专门用作于数据解析
            yield scrapy.Request(url=new_url,callback=self.parse)  # 对新的url手动发送请求

伪装策略 — 不遵循robots与UA伪装

  1. 默认爬取是遵循robots协议的,所以没有几个网址可以正确的爬取,所以要在settings.py中修改
  • ROBOTSTXT_OBEY=False
  1. 使用UA伪装

settingsUSER_AGENT中写入浏览器相关的参数

进行数据解析 — response.xpath()与.extract()

使用xpath进行解析,平时的一般使用(先实例化一个etree对象,再将页面的相应对象response.text作为参数传入进行解析)

# 导入模块
from lxml import etree

# 网站爬取的
page_text = response.text
tree = etree.HTML('page_text')  # 将页面上的数据加载到该对象 

r = tree.xpath('/html//div')

而在scrapy中的进行解析的函数parse中直接使用

response.xpath('...')

image-20210121163111328

response.xpath 结果:

  • 返回的是一个列表,列表中元素为Selector对象

  • .extract()

    • Selector对象直接调用.extract()方法会得到data参数存储的字符串

      image-20210121164706393

    • 列表调用了.extract()方法会使得每一个Selector对象中的data对应字符串都提取出来,仍然为字符串

      • 使用content = ''.join(content),将列表中的字符串拼接成一个字符串
      • .extract_first()将列表中第一个Selector对象进行得到其data参数中的字符串,适用于:该列表中仅有一个Selector对象

scrapy持久化存储

基于终端指令对parse返回值持久化存储

只可以将parse方法返回值存储到本地的文件中

终端命令:scrapy crawl 文件名 -o ./xxx.csv

  • 保存文件的类型只有:json, jsonlines, jl, csv, xml, marshal, pickle

  • 优缺点:

    • 好处:简洁
    • 缺点:局限性强,仅可以将parse返回值存到指定后缀的文件中
例子
# 在spider文件夹中的项目文件中
class 项目名Spider(scrapy.Spider):
    name = 'qiubai'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['https://www.qiushibaike.com/text/']
    
    def parse(self, response):
        #解析:作者的名称+段子内容
        div_list = response.xpath('//div[@id="content-left"]/div')
        all_data = [] #存储所有解析到的数据
        for div in div_list:
            #xpath返回的是列表,但是列表元素一定是Selector类型的对象
            #extract可以将Selector对象中data参数存储的字符串提取出来
            # author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()
            author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
            #列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
            content = div.xpath('./a[1]/div/span//text()').extract()
            content = ''.join(content)

            dic = {
                'author':author,
                'content':content
            }

            all_data.append(dic)

        return all_data  
  • 之后scrapy crawl 文件名 -o ./xxx.csv对返回值进行存储

重:基于管道 — pipelines中的process_item

  • 优缺点:
    • 好处:通用性强,无后缀限制
  • 涉及到的文件:
    • parse函数:对数据进行解析,并把数据放到item对象
    • items.py:定义item类,便于在parse函数中使用
    • pipelines.pyprocess_item函数,对传入的item对象进行自定义存储,重写父类相关方法 open_spider(self,spider)close_spider(self,spider)
    • settings.py中设置ITEM_PIPELINES
编码流程
  1. 数据解析 — 使用xpath
  2. 在item类重定义相关的属性**(该item类在文件items.py中)**使用scrapy.Field()定义属性

image-20210122092346064

  1. 解析的数据封装到item类型对象

  2. 将item类型对象提交给管道进行持久化存储**(管道类的定义在pipelines.py中)**

  • 在管道类的process_item中要将其接受到的item对象中存储的数据进行持久化存储操作

image-20210122093659690

  1. 配置文件中开启管道

在==settings.py==中取消注释ITEM_PIPELINES

实例 — 创建Item类,数据封装到item与存储
  1. Item类中定义相关的属性
class 工程名Item(scrapy.Item):
    # define the fields for your item here like:
    author = scrapy.Field()
    content = scrapy.Field()
  • items中属性用Field(),而其继承了字典,所以可以使用item[‘key’]进行赋值
  1. 将解析数据封装到item对象的属性中:要注意包含相应包
# 项目在 爬虫文件中
from 工程名.pipelines import pipe类名

item = items.py中item类名()  

# items中属性用Field(),而其继承了字典,所以可以使用item['key']进行赋值
item['author'] = author
  1. 将item提交给管道
yield item  # 当有多个结果要提交时,使用for循环,在for循环内部使用yield
  • parse函数每次想管道提交一个item,pipelines文件中的类就会被调用一次process_item

parse函数的例子

import scrapy
from qiubaiPro.items import QiubaiproItem  # 对相应项目的items包进行包含,便于将解析数据传入item对象中

class QiubaiSpider(scrapy.Spider):
    name = 'qiubai'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['https://www.qiushibaike.com/text/']

    def parse(self, response):
        #解析:作者的名称+段子内容
        div_list = response.xpath('//div[@id="content-left"]/div')
        all_data = [] #存储所有解析到的数据
        for div in div_list:
            # xpath返回的是列表,但是列表元素一定是Selector类型的对象
            # extract可以将Selector对象中data参数存储的字符串提取出来
            # author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()
            author = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span/h2/text()').extract_first()
            
            # 列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
            content = div.xpath('./a[1]/div/span//text()').extract()
            content = ''.join(content)

            # 实例化一个Item对象
            item = QiubaiproItem()
            item['author'] = author
            item['content'] = content

            yield item # 将item提交给了管道
  1. 在管道中持久存储

问题:

在pipelines文件中,打开文件并写入的代码 最好不要在函数process_item,因为每提交一个item,就调用一次process_item,这样会经常打开文件

==解决办法:==在文件pipelines

  • 定义一个属性为 文件对象fp
  • 重写父类相关方法 open_spider(self,spider)close_spider(self,spider)会在爬虫之前和结束后调用
# pipelines.py文件中
class 项目名Pipeline(object):
    # 定义一个属性,在开始爬虫,写入数据,结束爬虫时调用
    fp = None
    
    # 重点 --- 重写父类的一个方法:该方法只在开始爬虫的时候被调用一次
    def open_spider(self,spider):
        print('开始爬虫......')
        self.fp = open('./qiubai.txt','w',encoding='utf-8')

  	# 重点 --- 重写父类一个方法:该方法在结束爬虫时调用
    def close_spider(self,spider):
        print('结束爬虫!')
        self.fp.close()
        
    # 专门用来处理item类型对象
    # 该方法可以接收爬虫文件提交过来的item对象
    # 该方法没接收到一个item就会被调用一次
    def process_item(self, item, spider):
        author = item['author']
        content= item['content']

        self.fp.write(author+':'+content+'\n')

        return item # 就会传递给下一个即将被执行的管道类

爬虫文件向管道提交的item对象,仅仅会提交给优先级最高的管道类(仅给一个管道类)

  • 但若该管道类中的process_iter函数中有return item,item会传递给下一个即将被执行的管道类
  1. 配置文件中打开管道

在==settings.py==中取消注释ITEM_PIPELINES

image-20210122102800785

  • 其中的key表示一个类,value表示优先级,数越低优先级越高,优先级高的先执行

  • 多个管道类时,写入该字典ITEM_PIPELINES

    • 爬虫文件向管道提交的item对象

      仅仅会提交给优先级最高的管道类(仅给一个管道类)

      • 但若该管道类 中的 process_iter函数 中有return itemitem会传递给下一个即将被执行的管道类

二:scrapy五大核心部件

五大核心组件

  1. 引擎(Scrapy)

    用来处理整个系统的数据流处理

    触发事务(框架核心)

  2. 调度器(Scheduler)

    用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址

    • 组成部分:
      1. 过滤器:将请求对象去重
      2. 队列:将请求对象放到队列
  3. 下载器(Downloader)

    用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)

  4. 爬虫(Spiders)

    爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面

    • 作用:
      1. 产生url,将url封装成请求对象,并手动发送到引擎
      2. 进行数据解析
  5. 项目管道(Pipeline)

    负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。

相互作用:

  1. Spider产生url,将url封装成请求对象,并手动发送到引擎

  2. 引擎将收到的url给调度器,调度器进行对其进行去除并放到队列中

  3. 调度器从队列中调度请求对象给引擎,引擎再将其给下载器,下载器去互联网下载数据得到response,并交给引擎

  4. 引擎将response交给Spider,之后Spider将==response交给爬虫文件中的parse方法==,进行数据解析,将解析的数据放到item

  5. Spider将==item==给引擎后再给管道进行持久化存储

image-20210122160244804

scrapy中间件

下载中间件 — UA伪装,代理,爬取动态加载数据

位置:在引擎和下载器之间的为下载中间件(Downloader Middlewares)

作用:批量拦截到整个工作中所以 请求 和 响应

  • 拦截请求

    • 拦截请求对象后对其进行UA伪装:可以实现对每个请求对象实现不同的UA伪装
    • 代理IP设定

    middlewares.py下的MiddleproDownloaderMiddleware类

    • UA伪装:process_request函数
    • 代理IP:process_exception函数,注意修改完该函数后要return request进行重新发送请求
  • 拦截相应

    • 篡改相应数据,相应对象:process_response进行爬取动态数据

==下载中间件代码:==在middlewares.py下的MiddleproDownloaderMiddleware类

UA伪装
  • process_request(self, request, spider):拦截请求,修改请求对应请求载体请求标识

    • 可以进行UA伪装
    user_agent_list = [
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 "
        "(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
        "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 "
        "(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 "
        "(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
        "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 "
        "(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
        "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 "
        "(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
        "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 "
        "(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
        "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 "
        "(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
        "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
        "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
        "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
        "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
        "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
        "(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
        "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 "
        "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
        "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 "
        "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
    ]  # 很多UA
    
    #拦截请求
    def process_request(self, request, spider):
        #UA伪装
        request.headers['User-Agent'] = random.choice(self.user_agent_list)  # 拦截所有请求,进行随机UA伪装
    
        #为了验证代理的操作是否生效
        request.meta['proxy'] = 'http://183.146.213.198:80'
        return None
    
爬取动态加载数据
  • process_response(self, request, response, spider):拦截所有响应对象

    • spider参数是爬虫类实例化的一个对象,因此可以调用spiders文件夹下自己创建的爬虫项目中的 各个属性

    • 一个url对应一个request(请求对象)

      一个request对应一个response(响应对象)

    • 思路: 使用selenium获取动态响应数据,用其 对需要请求的url的响应进行修改。

动态加载的数据不能对页面直接请求得到,但可以使用selenium进行爬取,将爬取的数据修改到获取的响应中

步骤:

  1. spiders文件夹下的爬虫文件中记录需要动态请求的url

  2. 实例化一个selenium对象spiders文件夹下爬虫文件中

    (不写在process_response方法中,因为该方法每得到一个响应对象就调用一次)

    在爬虫文件的构造方法中实例化一个浏览器对象

    from selenium import webdriver
    
    # 实例化一个浏览器对象
    def __init__(self):
        self.bro = webdriver.Chrome(executable_path='...')
    

    middlewares.py中的process_response函数中

    bro.get(request.url) # 对 对应的url进行请求
    page_text = bro.page_source  # 包含了动态加载的新闻数据
    
    1. middlewares.py中的process_response函数中,使用selenium对象对需要动态请求的数据爬取

    (通过spider.selenium对象名进行调用)

    1. 将对应的url得到的新的响应数据进行封装,得到一个新的响应对象

    使用函数new_response = HtmlResponse(url=request.url,body=page_text,encoding='utf-8',request=request)

    • url为对应的需要动态访问的urlrequest.url
    • body为得到的动态加载数据,selenium对象bro.page_source得到
    • encoding为编码
    • request为对应的请求对象
from scrapy.http import HtmlResponse  # 包含对应头文件

#该方法拦截五大板块对应的 响应对象,进行篡改
def process_response(self, request, response, spider):  
  # spider参数 是 爬虫类实例化对象, 可以调用`spiders文件夹`下自己创建的爬虫项目中的 各个属性
  
  bro = spider.bro  # 获取了在爬虫类中定义的浏览器对象

  # 挑选出指定的响应对象进行篡改
  # 通过url指定request
  # 通过request指定response
  if request.url in spider.models_urls:  # spider.models_urls 记录需要动态请求的url
      
      bro.get(request.url) # 五个板块对应的url进行请求
      sleep(3)
      page_text = bro.page_source  # 包含了动态加载的新闻数据

      # response #五大板块对应的响应对象
      # 针对定位到的这些response进行篡改
      # 实例化一个新的响应对象(符合需求:包含动态加载出的新闻数据),替代原来旧的响应对象

      # 基于selenium便捷的获取动态加载数据
      new_response = HtmlResponse(url=request.url,body=page_text,encoding='utf-8',request=request)

      return new_response
  
  else:  # 对于不需要动态请求的url,直接返回得到的响应
      # response #其他请求对应的响应对象
      return response
  1. 关闭浏览器对象
  • spiders文件夹中的爬虫文件中重写父类方法closed(self,spider)

    • 该方法在爬虫结束时进行调
    def closed(self,spider):
        self.bro.quit()
    
代理
  • process_exception:拦截发送异常的请求

    • 代理 — 对不能访问的请求使用代理,使得可以访问
    # 请求的url是http类型则使用http的代理
    PROXY_http = [
        '153.180.102.104:80',
        '195.208.131.189:56055',
    ]
    PROXY_https = [
        '120.83.49.90:9000',
        '95.189.112.214:35508',
    ]
    
    #拦截发生异常的请求
    def process_exception(self, request, exception, spider):
        if request.url.split(':')[0] == 'http':
            #代理
            request.meta['proxy'] = 'http://'+random.choice(self.PROXY_http)
            else:
                request.meta['proxy'] = 'https://' + random.choice(self.PROXY_https)
    
                return request  #将修正之后的请求对象进行重新的请求发送
    

开启下载中间件

要使用中间件时,一定要在settings.py中取消DOWNLOADER_MIDDLEWARES

三:手动发送请求 — 并实现 请求传参(meta参数)

**适用:**如果爬取解析的数据不在同一张页面中,即要进行保存到item的解析的数据不在一个url中

  • 要使用多个数据解析方法进行解析数据 保存到一个item中

例子:

# -*- coding: utf-8 -*-
import scrapy
from bossPro.items import BossproItem


class BossSpider(scrapy.Spider):
    name = 'boss'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['https://www.zhipin.com/job_detail/?query=python&city=101010100&industry=&position=']

    # 回调函数接受item
    def parse_detail(self, response):
        item = response.meta['item']

        job_desc = response.xpath('//*[@id="main"]/div[3]/div/div[2]/div[2]/div[1]/div//text()').extract()
        job_desc = ''.join(job_desc)
        # print(job_desc)
        item['job_desc'] = job_desc

        yield item

    # 解析首页中的岗位名称
    def parse(self, response):
        li_list = response.xpath('//*[@id="main"]/div/div[3]/ul/li')
        for li in li_list:
            item = BossproItem()  # 实例化一个Item对象

            job_name = li.xpath('.//div[@class="info-primary"]/h3/a/div[1]/text()').extract_first()
            item['job_name'] = job_name
            # print(job_name)
            detail_url = 'https://www.zhipin.com' + li.xpath('.//div[@class="info-primary"]/h3/a/@href').extract_first()
            
            # 对详情页发请求获取详情页的页面源码数据
            # 手动请求的发送
            # 请求传参:meta={},可以将meta字典传递给请求对应的回调函数parse_detail
            yield scrapy.Request(detail_url, callback=self.parse_detail, meta={'item': item})
            
           
  • 对主页面项目进行解析得到项目名并报存到item中,每个项目对应的detail_rul,然后请求detail_url,得到详细说明保存到item中

    但由于要获得两个项目所用到的xpath不同,并存在手动请求发送的关系,所以:写了两个函数parseparse_detail

    问题:item需要得到这两个函数中分别解析的数据以便持久化存储,所以需要在parse函数中进行手动请求发送调用回调函数parse_datail时,以item作为参数

  • scrapy.Request(detail_url, callback=self.parse_detail, meta={‘item’: item})

    • callback参数:表示回调函数,对主动请求的url对应的相应对象进行解析
    • meta参数:是一个字典,meta={'item': item},将meta字典传递给请求对应的回调函数
    • 回调函数接受item:item = response.meta['item'],就可以在回调函数将解析的数据保存到item中

四:基于scrapy爬取图片 — ImagesPipeline类

字符串:只需要基于xpath进行解析且提交管道进行持久化存储

图片:xpath得到图片src属性值,单独对图片地址发起请求获取图片二进制数据

ImagesPipeline类:

  • 只需要将img的src的属性值进行解析,提交到管道
  • 管道就会对图片的src进行请求发送获取图片的二进制类型的数据,且还会帮我们进行持久化存储。

图片软加载

只有图片移动到可视化区域时,该图片才会被显示出来**(即该图片的软属性会变成真正的属性)**

image-20210123111737053

  • 该图片未移动到可视化区域,看到伪属性src2

image-20210123111824306

  • 该图片移动到可视化区域,看到伪属性src2变成了src

解决办法:基于scrapy爬取时,没有可视化界面,也就是说所以图片都是基于伪属性src2的,可以爬取该属性得到图片的结果

重:使用的流程

使用流程:

  • 数据解析(图片的地址)

  • 将存储图片地址的item提交到制定的管道类

  • 在管道文件中自定制一个基于ImagesPipeLine的一个管道类

    • get_media_request
    • file_path
    • item_completed
  • 在配置文件中:

    • 指定图片存储的目录:IMAGES_STORE = ‘./imgs_bobo’
    • 指定开启的管道:自定制的管道类

    将src属性传到管道中

将得到的src封装到item中

  • 要在爬虫文件中包含items.py对应的类

  • 使用yield item将其提交到管道中

    创建父类ImagesPipeline类的管道类,并重写三个方法get_media_requestsfile_pathitem_completed

  1. get_media_requests:根据图片地址进行图片数据的请求
  2. file_path:指定图片存储的路径
  3. item_completed:返回item,将item返回下一个执行的管道类
import scrapy
from scrapy.pipelines.images import ImagesPipeline

class imgsPileLine(ImagesPipeline):  

    # 就是可以根据图片地址进行图片数据的请求
    def get_media_requests(self, item, info):

        yield scrapy.Request(item['src'])

    # 指定图片存储的路径
    def file_path(self, request, response=None, info=None):
        imgName = request.url.split('/')[-1]
        return imgName

    def item_completed(self, results, item, info):
        return item # 返回给下一个即将被执行的管道类
  • items中属性用Field(),而其继承了字典,所以可以使用item[‘key’]进行赋值

  • 对图片url手动请求不需要callback参数,因为得到url对应的相应后不需要调用回调函数进行解析

  • file_path(self, request, response=None, info=None)

    • request请求对象
    • 该函数要将图片路径进行返回,包括图片名称返回
    • 而图片存储的目录
      • 在==settings.py中加入参数IMAGES_STORE==

写好管道类后一定要在settings.py中修改ITEM_PIPELINES

五:CrawlSpider类 — 全站数据爬取

为Spider的一个子类,用于全站数据爬取 — 将所有页码数据爬取

实现全站数据爬取

  • 基于Spider,使用手动请求发送

  • 基于CrawlSpider进行

基本使用

创建一个工程

scrapy startproject 工程名

执行爬虫工程

scrapy crawl 爬虫文件名

  • 可以在settings文件中加==LOG_LEVEL='ERROR'==仅错误相关的输出忽略其他错误输出

  • 爬虫文件名称是爬虫源文件的唯一标识

创建爬虫文件

**进入工程目录(cd 工程目录)**后

scrapy genspider -t crawl 爬虫文件名 www.xxx.com

在spiders子目录中创建一个爬虫文件,其中url为起始url,文件中:

  • parse_item:对数据进行解析

  • rules属性中Rule( LinkExtractor(allow=r'正则'), callback='parse_item', follow=True)

    rules 包含一个或多个 Rule 对象的集合。每个 Rule 对爬取网站的动作定义了特定规则。如果多个 Rule 匹配了相同的链接,则根据他们在本属性中被定义的顺序,第一个会被使用

    所以:规则的顺序可以决定获取的数据,应该把 精确匹配的规则放在前面,越模糊的规则放在后面。

    • LinkExtractor链接提取器:根据指定规则(allow=“正则”)进行指定链接的提取

    • Rule规则解析器:对链接提取器提取到的链接进行指定规则(callback)的解析操作

      • 对链接提取器提取到的链接会自动的请求,只需将链接提取器结果放入Rule中

      • follow = True:可以将 链接提取器 继续作用到 连接提取器提取到的链接 所对应的页面中

        这样可以得到所有的页码对应链接

    • 作用

    1. 得到网址中全部的页码对应的链接
    2. 得到一个页面中所有项目的详情页
    import scrapy
    from scrapy.linkextractors import LinkExtractor
    from scrapy.spiders import CrawlSpider, Rule
    
    
    class TestSpider(CrawlSpider):
        name = 'test'
        allowed_domains = ['www.baidu.com']
        start_urls = ['http://www.baidu.com/']
    	
        # LinkExtractor 链接提取器:根据指定规则(allow="正则")进行指定链接的提取
        # Rule 规则解析器:将链接提取器提取到的链接进行指定规则(callback)的解析操作
        #	其中参数 follow=True:可以将链接提取器 继续作用到 连接提取器提取到的链接 所对应的页面中
        rules = (
            Rule( LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
        )
    
        def parse_item(self, response):
            item = {}
            #item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
            #item['name'] = response.xpath('//div[@id="name"]').get()
            #item['description'] = response.xpath('//div[@id="description"]').get()
            return item
    
    
爬虫文件的例子

属性rules中可以==写多个规则解析器对应的链接解析器解析的链接 进行调用 定义的解析函数==

  • 如果多个 Rule 匹配了相同的链接,则根据他们在本属性中被定义的顺序,第一个会被使用

    所以:规则的顺序可以决定获取的数据,应该把 精确匹配的规则放在前面,越模糊的规则放在后面。

  • 即rules仅定义了规则,而对得到的响应就使用这些规则看看是否匹配,匹配则进行调用回调函数进行 解析

import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from sunPro.items import SunproItem, DetailItem  # 导入两个Item类的包


# 需求:爬取sun网站中的编号,新闻标题,新闻内容,标号
class SunSpider(CrawlSpider):
    name = 'sun'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page=']

    # LinkExtractor 链接提取器:根据指定规则(allow="正则")进行指定链接的提取
    link = LinkExtractor(allow=r'type=4&page=\d+')  # 对起始url下 所有页码的url进行提取
    link_detail = LinkExtractor(allow=r'question/\d+/\d+\.shtml')  # 对某一页面上的所有项目url提取
   	
    rules = (
    	# Rule 规则解析器:将链接提取器提取到的链接进行指定规则(callback)的  解析操作
    	# 	follow=True:可以将链接提取器 继续作用到 连接提取器提取到的链接 所对应的页面中
        Rule(link, callback='parse_item', follow=True),  # 对响应的url发送请求并进行解析
        Rule(link_detail, callback='parse_detail')
    )

    
    # 如下两个解析方法中是不可以实现请求传参!
    # 如法将两个解析方法解析的数据存储到同一个item中,可以以此存储到两个item
    
    # 解析 --- 新闻编号和新闻的标题
    def parse_item(self, response):
        # 注意:xpath表达式中不可以出现tbody标签
        tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr')
        for tr in tr_list:
            new_num = tr.xpath('./td[1]/text()').extract_first()
            new_title = tr.xpath('./td[2]/a[2]/@title').extract_first()
            item = SunproItem()
            item['title'] = new_title
            item['new_num'] = new_num

            yield item  # 提交给管道

    # 解析 --- 详情页的 新闻内容和新闻编号
    def parse_detail(self, response):
        new_id = response.xpath('/html/body/div[9]/table[1]//tr/td[2]/span[2]/text()').extract_first()
        new_content = response.xpath('/html/body/div[9]/table[2]//tr[1]//text()').extract()
        new_content = ''.join(new_content)

        # print(new_id,new_content)
        item = DetailItem()
        item['content'] = new_content
        item['new_id'] = new_id

        yield item

问题

如果爬取解析的数据不在同一张页面中,即要进行保存到item的解析的数据不在一个url中

例如:对一个网址上所有页面上的数据解析得到A,并得到对应详情页的数据内容B,将A,B进行存储,这是要使用两个解析函数

image-20210124230714810

法1:如果使用是Spider,可以进行手动请求发送,这样可以进行传参(使用meta参数传递Item对象),让两个解析函数进行传参,让两个解析函数解析的内容保存到一个Item类的对象中

法2:但是使用是CrawlSpider时,请求是自动发送的,没有办法传递参数

  • 所以要使用两个Item类,一个保存解析数据A,一个保存解析数据B

    # 一个保存解析数据A ,一个保存数据B
    class SunproItem(scrapy.Item): 
        # define the fields for your item here like:
        title = scrapy.Field()
        new_id = scrapy.Field()
    
    class DetailItem(scrapy.Item):
        new_id = scrapy.Field()
        content = scrapy.Field()
    
  • 在管道pipelines.py的函数process_item中使用item.__class__.__name__得到定义的 Item类 的类名,根据类名将得到的数据进行保存

    class SunproPipeline(object):
        def process_item(self, item, spider):
            # 如何判定item的类型 --- new_id
            # 将数据写入数据库时,如何保证数据的一致性
            if item.__class__.__name__ == 'DetailItem':
                print(item['new_id'], item['content'])
            else:
                print(item['new_id'], item['title'])
            return item
    
  • 因为是两个Item类,为了使得两个类的数据进行保存时不破坏一致性,要额外保存一个通用属性,来标记是来自同一页面的信息

    例如上面的new_id就是为了标记同一页面

  • 注意要开启管道:settings.py中设置ITEM_PIPELINES

六:分布式爬虫 — scrapy-redis

概念:我们需要搭建一个分布式的机群,让其对一组资源进行分布联合爬取。
作用:提升爬取数据的效率

如何实现分布式?

  • 安装一个scrapy-redis的组件

  • 原生的scarapy是不可以实现分布式爬虫,必须要让scrapy结合着scrapy-redis组件一起实现分布式爬虫。

    • 如果直接使用scrapy各个机器的scrapy五大核心部件没有关联

    • 如果调度器可以被共享,则可以进行多个机器分布式爬取

      • 调度器(Scheduler)

        用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址

    • 如果管道可以被共享,则可以汇总保存数据

scrapy-redis组件作用:

  • 可以给原生的scrapy框架提供可以被共享的管道和调度器

实现流程

  • 创建一个工程

  • 创建一个基于CrawlSpider的爬虫文件

  • 修改当前的爬虫文件:

    • 导包:from scrapy_redis.spiders import RedisCrawlSpider

    • start_urlsallowed_domains进行注释(起始的url在客户端进行给出)

    • 添加一个新属性redis_key = '调度器队列名称'

      表示可以被共享的调度器队列的名称

    • 编写数据解析相关的操作(使用rules,解析数据,创建Item对象,保存到item)

    • 将当前爬虫类的父类修改成RedisCrawlSpider

  • 修改配置文件settings

    • 指定使用可以被共享的管道

      ITEM_PIPELINES = {
      'scrapy_redis.pipelines.RedisPipeline': 400
      }
      
    • 指定调度器

      # 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化
      DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
      
      # 使用scrapy-redis组件自己的调度器
      SCHEDULER = "scrapy_redis.scheduler.Scheduler"
      
      # 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据
      SCHEDULER_PERSIST = True  # 某台机器宕机恢复后,可以从未爬取的地方爬
      
    • 指定redis服务器 — 将数据保存到对应服务器上

      #指定redis
      REDIS_HOST = '127.0.0.1' # redis远程服务器的ip(修改)
      REDIS_PORT = 6379
      
  • redis相关操作配置:

    • 配置redis的配置文件:

      • linux或者mac:redis.conf

      • windows:redis.windows.conf

      • 代开配置文件修改:

        • bind 127.0.0.1进行删除,不去掉的话代表仅运行本机访问服务器

        • 关闭保护模式:protected-mode yes改为no

    • 结合着配置文件开启redis服务

      • redis-server 配置文件路径 ,配置相关环境变量后可以直接redis-server
        在这里插入图片描述
    • 启动客户端:

      • redis-cli
        image-20210125095219401
    • 关闭服务器:

      • redis-cli shutdown
  • 执行工程:

    • scrapy runspider 爬虫文件名称.py
  • 向调度器的队列中放入一个起始的url

    • 调度器的队列 在redis的客户端中
      • lpush 调度器队列名称 起始url
  • 爬取到的数据存储在了redis的proName:items这个数据结构中

    image-20210125015211296

七:增量式爬虫

监测网站数据更新的情况,只会爬取更新出来的数据

  • 可以使用简单的记录已经爬取过的url来实现

    而接下来是为了实现基于服务器redis进行记录,实现增量式爬取

分析

制定起始url

基于CrawSpider获取其他页码链接

基于Rule将其他页码链接请求

对每个页码对应页面的源码解析出详情页url

核心:检测详情页url是否请求过

  • 将爬取过的电影详情页url存储
    • 可以存储到redis的set集合中(在数据解析时实现)
      • 把所有url先放入集合中,如果返回1,则之前没有在集合中,需要进行爬取
      • 把所有url先放入集合中,如果返回0,则之前在集合中,不需要进行爬取

对详情页url进行发起请求,解析

进行持久化存储

代码

spiders文件夹下

from redis import Redis

# 创建一个redis链接对象
conn = Redis(host='...', port=6379)

# 在解析函数中判断是否之前爬取过详情页的url
def parse_item(self, response):
    ... # 进行解析得到详情页detail_url
    ex = self.conn.sadd('urls', detail_url)  # 将detail_url保存到服务器中的集合urls中
    
    # 返回值1 不在集合中,需要进行爬取
    # 返回值0 在集合中,不需要进行爬取
    if ex == 1:
        yield scrapy.Request(url=detail_url, callback=self.parst_detail)  # 手动进行请求发送
    else:
        print('已爬取')

对爬取数据封装的Item对象item进行保存 — 在pipelines.py文件中对应的类中

conn = None
def open_spider(self, spider):
    self.comm = spider.conn
def process_item(self, item, spider):
    dic = {
        'xxx':item['xxx'],
        ...
    }
    self.conn.lpush('定义在服务器一个列表名', dic)
    return item
  • 最后服务器上有一个集合urls,一个自定义的列表用于存储

参考视频

Python爬虫+数据分析基础全套课程(无私分享)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值