python爬虫--scrapy框架

Scrapy

一 介绍

Scrapy简介

1.Scrapy是用纯Python实现一个为了爬取网站数据、提取结构性数据而编写的应用框架,用途非常广泛

2.框架的力量,用户只需要定制开发几个模块就可以轻松的实现一个爬虫,用来抓取网页内容以及各种图片,非常之方便

Scrapy架构图

Scrapy主要包括了以下组件:

1.引擎(Scrapy)
	用来处理整个系统的数据流处理, 触发事务(框架核心)
	
2.调度器(Scheduler)
    用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
    
3.下载器(Downloader)
	用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
	
4.爬虫(Spiders)
	爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
	
5.项目管道(Pipeline)
	负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
	
6.下载器中间件(Downloader Middlewares)
	位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应。
	
7.爬虫中间件(Spider Middlewares)
	介于Scrapy引擎和爬虫之间的框架,主要工作是处理蜘蛛的响应输入和请求输出。
	
8.调度中间件(Scheduler Middewares)
	介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。

Scrapy运行流程

1.引擎从调度器中取出一个链接(URL)用于接下来的抓取,包括过滤器和对列,过滤后的url交给对列
2.引擎把URL封装成一个请求(Request)传给下载器
3.下载器把资源下载下来,并封装成应答包(Response)
4.爬虫解析Response
5.解析出实体(Item),则交给引擎,在提交到管道进行进一步的处理(持久化存储处理)
6.解析出的是链接(URL),则把URL交给调度器等待抓取
都会经过引擎进行调度

二 安装

#Windows平台
    1、pip3 install wheel #安装后,便支持通过wheel文件安装软件,wheel文件官网:https://www.lfd.uci.edu/~gohlke/pythonlibs
    3、pip3 install lxml
    4、pip3 install pyopenssl
    5、下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/
    6、下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
    7、执行pip3 install 下载目录\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
    8、pip3 install scrapy

#Linux平台
    1、pip3 install scrapy

三 命令行工具

介绍

#1 查看帮助
    scrapy -h
    scrapy <command> -h

#2 有两种命令:其中Project-only必须切到项目文件夹下才能执行,而Global的命令则不需要
    Global commands: #全局命令
        startproject #创建项目
        genspider    #创建爬虫程序 
           # 示例 scrapy genspider  baidu     www.baidu.com 
                           
                    
        settings     #如果是在项目目录下,则得到的是该项目的配置
        runspider    #运行一个独立的python文件,不必创建项目
        shell        #scrapy shell url地址  在交互式调试,如选择器规则正确与否
        fetch        #独立于程单纯地爬取一个页面,可以拿到请求头
        view         #下载完毕后直接弹出浏览器,以此可以分辨出哪些数据是ajax请求
        version      #scrapy version 查看scrapy的版本,scrapy version -v查看scrapy依赖库的版本
        
    Project-only commands: #项目文件下
        crawl        #运行爬虫,必须创建项目才行,确保配置文件中ROBOTSTXT_OBEY = False
        check        #检测项目中有无语法错误
        list         #列出项目中所包含的爬虫名
        edit         #编辑器,一般不用
        parse        #scrapy parse url地址 --callback 回调函数  #以此可以验证我们的回调函数是否正确
        bench        #scrapy bentch压力测试

#3 官网链接
    https://docs.scrapy.org/en/latest/topics/commands.html

示例

#1、执行全局命令:请确保不在某个项目的目录下,排除受该项目配置的影响
scrapy startproject MyProject(项目名)

cd MyProject (切换到项目下)
scrapy genspider baidu www.baidu.com  #创建爬虫程序
					      #(baidu)爬虫名    对应相关域名(www.baidu.com可以先随便写个www.xxx.com ) 
                            #代表这个爬虫程序只能爬取www.baidu.com 这个域名或者百度的子域名

scrapy settings --get XXX #如果切换到项目目录下,看到的则是该项目的配置

scrapy runspider baidu.py #执行爬虫程序

scrapy shell https://www.baidu.com
    response
    response.status
    response.body
    view(response)
    
scrapy view https://www.taobao.com #如果页面显示内容不全,不全的内容则是ajax请求实现的,以此快速定位问题

scrapy fetch --nolog --headers https://www.taobao.com

scrapy version #scrapy的版本

scrapy version -v #依赖库的版本

#2、执行项目命令:切到项目目录下
scrapy crawl baidu
scrapy check
scrapy list
scrapy parse http://quotes.toscrape.com/ --callback parse
scrapy bench

四 项目结构以及爬虫应用简介

目录结构

project_name/
   scrapy.cfg
   project_name/
       __init__.py
       items.py
       pipelines.py
       settings.py
       spiders/
           __init__.py
           爬虫1.py
           爬虫2.py
           爬虫3.py

应用说明

scrapy.cfg:爬虫项目的配置文件。

__init__.py:爬虫项目的初始化文件,用来对项目做初始化工作。

items.py:爬虫项目的数据容器文件,用来定义要获取的数据。

pipelines.py:爬虫项目的管道文件,用来对items中的数据进行进一步的加工处理。

settings.py:爬虫项目的设置文件,包含了爬虫项目的设置信息。

middlewares.py:爬虫项目的中间件文件,

pycharm中运行爬虫程序

在项目目录先创建entrypoint.py文件,文件名不能变
from scrapy.cmdline import execute
execute(['scrapy','crawl','baidu','--nolog']) #百度为爬虫名,列表的前两项不变 --nolog可写可不写,作用是不在打印其他配置项,只打印需要的内容

五 Spiders

1.介绍

1.Spider是由一系列类(定义了一个网址一组网址将别爬取)组成,具体包括了如何执行爬取任务并且如何从页面中提取结构化的数据
2.Spider是你为了一个特定的网址或一组网址自定义爬取或解析页面行为的地方

2.Spider会循环做的事情

#1、生成初始的Requests来爬取第一个URLS,并且标识一个回调函数
第一个请求定义在start_requests()方法内默认从start_urls列表中获得url地址来生成Request请求,默认的回调函数是parse方法。回调函数在下载完成返回response时自动触发

#2、在回调函数中,解析response并且返回值
返回值可以4种:
        包含解析数据的字典
        Item对象
        新的Request对象(新的Requests也需要指定一个回调函数)
        或者是可迭代对象(包含Items或Request)

#3、在回调函数中解析页面内容
通常使用Scrapy自带的Selectors,但很明显你也可以使用Beutifulsoup,lxml或其他你爱用啥用啥。

#4、最后,针对返回的Items对象将会被持久化到数据库
通过Item Pipeline组件存到数据库:https://docs.scrapy.org/en/latest/topics/item-pipeline.html#topics-item-pipeline)
或者导出到不同的文件(通过Feed exports:https://docs.scrapy.org/en/latest/topics/feed-exports.html#topics-feed-exports)

3.爬取格式

entrypoint.py

from scrapy.cmdline import execute
execute(['scrapy','crawl','amazon1','-a','keywords=iphone8手机','--nolog'])
加参数的格式
# -*- coding: utf-8 -*-
import scrapy
from urllib.parse import urlencode

class Amazon1Spider(scrapy.Spider):
    name = 'amazon1'
    allowed_domains = ['www.amazon.cn']   #也可以不用,写的话就会对请求的url进行限制,下边的start_urls就只能请求allowed_domains中url
    start_urls = ['https://www.amazon.cn/'] #可以放多个请求的url
	#不写allowed_domains,就可以在start_urls请求列表中写多个url
    
    # 自定义的配置,可以加请求头系列的配置,先从这里找,没有去settings中找
    custom_settings = {
        'REQUEST_HEADERS':{
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36',

        }
    }
    
    # 规定外部传进来的参数
    def __init__(self,keywords,*args,**kwargs):
        super(Amazon1Spider,self).__init__(*args,**kwargs)
        self.keywords=keywords

    # 发送请求
    def start_requests(self):
        url = 'https://www.amazon.cn/s?%s&ref=nb_sb_noss_1' %(urlencode({'k':self.keywords}))
        yield scrapy.Request(url=url,
                             callback=self.parse,
                             dont_filter=True
                             ) #请求方式GET
        #post请求 : scrapy.FormRequest(url,formdata=data,callback)
        
    # 解析
    def parse(self, response):
        detail_url = response.xpath('//*[@id="search"]/div[1]/div[2]/div/span[4]/div[1]/div[1]/div/span/div/div/div[2]/div[3]/div/div/h2/a/@href') #如果这里得到多个url,商品详情url求,通过for循环再次对详情的url返送请求,每次返送请求必须有对应的回调函数
        for url in detail_url: 
            yiled yield scrapy.Request(url=url,
                             callback=self.parse_detail
                             dont_filter=True
                             )
        print(detail_url)
        
    def parse_detail(self,response):
        print(response) #详情页的响应结果
        
    def close(spider, reason): #解析之后执行close
        print('结束')

4.示例

爬取三国演义的文章标题以及每篇文章的内容

from scrapy.cmdline import execute
execute(['scrapy','crawl','sang','--nolog'])
# -*- coding: utf-8 -*-
import scrapy
class SangSpider(scrapy.Spider):
    name = 'sang'
    allowed_domains = ['www.shicimingju.com/book/sanguoyanyi.html']
    start_urls = ['http://www.shicimingju.com/book/sanguoyanyi.html/']

    custom_settings = { #加一些请求头信息
        'REQUEST_HEADERS':{
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36',
        }
    }
    def start_requests(self):
        url = 'http://www.shicimingju.com/book/sanguoyanyi.html/'
        yield scrapy.Request(url=url,
                             callback=self.parse,#解析主页面的回调函数
                             ) #向主页面发起请求

    def parse(self, response): #解析主页面的响应信息
        all_urls = response.xpath('/html/body/div[4]/div[2]/div[1]/div/div[4]/ul/li/a/@href').extract()#所有的文章的url 
        all_title = response.xpath('/html/body/div[4]/div[2]/div[1]/div/div[4]/ul/li/a/text()').extract()#所有标题

        for url in all_urls:
            # print(response.urljoin(url)) #拼接url 
            detail_urls = 'http://www.shicimingju.com'+url
            print(detail_urls)
            yield scrapy.Request(url=detail_urls,  
                                 callback=self.parse_detail,#解析详情页面的回调函数
                                 dont_filter=True
                                 ) #向详细文章发送请求
    def parse_detail(self,response):
        content = response.xpath('/html/body/div[4]/div[2]/div[1]/div[1]/div/p/text()') #解析详情文章的响应
        print(content)

5.数据解析

- scrapy中封装的xpath的方式进行数据解析。
- scrapy中的xpath和etree中的xpath的区别是什么?
	etree中的xpath返回的是字符串或列表
	scrapy的xpath进行数据解析后返回的列表元素为Selector对象,然后必须通过extract或者extract_first这两个方法将Selector对象中的对应数据取出

extract_first:将列表元素中第0个selector对象提取
extract:取出列表中的每一个selector对象提取

6.数据持久化存储

基于终端的指令存储

特性:只能将parse方法的返回值存储到本地的磁盘文件中
指令:scrapy crawl 爬虫程序名 -o 文件名
局限性:只能存储到磁盘中,存储的文件名有限制,只能用提供的文件名

示例

# 爬取糗事百科笑话的标题和内容
import scrapy
class QiubSpider(scrapy.Spider):
    name = 'qiub'
    start_urls = ['http://www.lovehhy.net/Joke/Detail/QSBK/']

    def parse(self, response):
        all_data = []
        #response.xpath拿到的是个列表里面是Selector对象
        title = response.xpath('//*[@id="footzoon"]/h3/a/text()').extract()
        content = response.xpath('//*[@id="endtext"]//text()').extract() #因为在内容中有对个br标签进行分割,所以用//text()
        title=''.join(title)
        content=''.join(content)
        dic = {
            'title':title,
            'content':content
        }
        all_data.append(dic)

        return all_data #必须有返回值才能用基于终端指令的存储
终端存储指令:scrapy crawl qiub -o qiubai.csv(csv是提供的文件类型)    

基于管道存储

实现流程
- 基于管道:实现流程
    1.数据解析
    2.在item类中定义相关的属性
    3.将解析的数据存储或者封装到一个item类型的对象(items文件中对应类的对象)
    4.向管道提交item
    5.在管道文件的process_item方法中接收item进行持久化存储
    6.在配置文件中开启管道
    
管道只能处理item类型的对象
文件形式
示例

qiub.py

# -*- coding: utf-8 -*-
import scrapy
from qiubai.items import QiubaiItem

class QiubSpider(scrapy.Spider):
    name = 'qiub'

    start_urls = ['http://www.lovehhy.net/Joke/Detail/QSBK/']
    def parse(self, response):
        all_data = []
        title = response.xpath('//*[@id="footzoon"]/h3/a/text()').extract()
        content = response.xpath('//*[@id="endtext"]//text()').extract()
        title=''.join(title)
        content=''.join(content)
        dic = {
            'title':title,
            'content':content
        }
        all_data.append(dic)
        item = QiubaiItem() #实例化一个item对象
        item['title'] = title #封装好的数据结构
        item['content'] = content
        yield item #向管道提交item,提交给优先级最高的管道类

items.py

import scrapy
class QiubaiItem(scrapy.Item):
    # define the fields for your item here like:
    title = scrapy.Field() #Fieid可以接受任意类型的数据格式/类型
    content = scrapy.Field()

pipelines.py

class QiubaiPipeline(object):
    f= None
    print('开始爬虫.....')
    def open_spider(self,spider): # 重写父类方法,开启文件
        self.f = open('qiubai.txt','w',encoding='utf-8')

    def close_spider(self,spider):# 重写父类方法,关闭文件
        print('结束爬虫')
        self.f.close()

    def process_item(self, item, spider):
        #item是管道提交过来的item对象
        title = item['title']  #取值
        content = item['content']
        self.f.write(title+':'+content+'\n') #写入文件
        return item  

settings.py

# 开启管道,可以开启多个管道
ITEM_PIPELINES = {
   'qiubai.pipelines.QiubaiPipeline': 300, #300表示的优先级
}
将同一份数据持久化到不同的平台
1.管道文件中的一个管道类负责数据的一种形式的持久化存储
2.爬虫文件向管道提交的item只会提交给优先级最高的那一个管道类
3.在管道类的process_item中的return item表示的是将当前管道接收的item返回/提交给下一个即将被执行的管道类
数据库(mysql)
示例
import pymysql
# 负责将数据存储到mysql
class MysqlPL(object):
    conn = None
    cursor = None

    def open_spider(self, spider):
        self.conn = pymysql.Connect(host='127.0.0.1', port=3306, user='root', password='123', db='spider',charset='utf8')
        print(self.conn)

    def process_item(self, item, spider):
        author = item['author']
        content = item['content']
        sql = 'insert into qiubai values ("%s","%s")' % (author, content)
        self.cursor = self.conn.cursor()
        try:
            self.cursor.execute(sql) #执行正确的话就提交事务
            self.conn.commit()
        except Exception as e:
            print(e)
            self.conn.rollback()  #回滚,如果出现错误就回滚
        return item
	
    def close_spider(self, spider):
        self.cursor.close()
        self.conn.close()

7.爬取校花网图片

ImagesPipeline的简介

专门爬取图片的管道

1.爬取一个Item,将图片的URLs放入image_urls字段
2.从Spider返回的Item,传递到Item Pipeline
3.当Item传递到ImagePipeline,将调用Scrapy 调度器和下载器完成4.image_urls中的url的调度和下载。
5.图片下载成功结束后,图片下载路径、url和校验和等信息会被填充到images字段中。

ImagesPipeline的使用:

from scrapy.pipelines.images import ImagesPipeline
import scrapy
# 通过重写父类方法
class SpiderImgPipeline(ImagesPipeline):
   
    # 对某一个媒体资源进行请求发送
    # item是提交过来的item(src)
    def get_media_requests(self, item, info): 
        yield scrapy.Request(item['src'])

    # 制定媒体数据存储的名称
    def file_path(self, request, response=None, info=None):
        img_name = request.url.split('/')[-1]
        print(img_name+'正在爬取')
        return img_name

    # 将item传递个下一个即将被执行的管道类
    def item_completed(self, results, item, info):
        return item

# 在配置中添加图片的存储路径
IMAGES_STORE = './imgslib'

爬取示例:

settings.py

IMAGES_STORE = './imgslib' #存储爬取到的图片
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36'
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
ITEM_PIPELINES = {
   'spider_img.pipelines.SpiderImgPipeline': 300,
}

img.py

# -*- coding: utf-8 -*-
import scrapy
from spider_img.items import SpiderImgItem

class ImgSpider(scrapy.Spider):
    name = 'img'
    start_urls = ['http://www.521609.com/daxuexiaohua/']
    url= 'http://www.521609.com/daxuexiaohua/list3%d.html' #通用的url模板,需要加page页数,每个页数的模板
    page_num = 1
    def parse(self, response):
        li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li')
        for li in li_list:
            img_src = 'http://www.521609.com/' + li.xpath('./a[1]/img/@src').extract_first() #获取的所有的图片的url,extract_first获取的字符串类型,extract获取的是list,里面存了selector对象
            item = SpiderImgItem() #实例化item对象
            item['src'] = img_src  #把数据添加到item中
            yield item #提交到item

        if self.page_num < 3: #爬取的前两页
            self.page_num += 1
            new_url = format(self.url%self.page_num)  #其他页数的url
            yield scrapy.Request(new_url,callback=self.parse)  #对页数发请求

items.py

import scrapy
class SpiderImgItem(scrapy.Item):
    src = scrapy.Field()
    pass

piplines.py

from scrapy.pipelines.images import ImagesPipeline
import scrapy

class SpiderImgPipeline(ImagesPipeline):
    # 对某一个媒体资源进行请求发送
    # item是提交过来的item(src)
    def get_media_requests(self, item, info):
        yield scrapy.Request(item['src'])
	
    # 制定媒体数据存储的名称
    def file_path(self, request, response=None, info=None):
        img_name = request.url.split('/')[-1]
        print(img_name+'正在爬取')
        return img_name

    # 将item传递个下一个即将被执行的管道类
    def item_completed(self, results, item, info):
        return item

8.请求传参(深度爬取)

示例:爬取4567tv电影网的电影信息

movie.py

# 需求:爬取电影网的电影名称和电影的简介
# 分析:去电影网的首页请求会看到所有的电影,但不能获取简介,需要现获取所有的电影的url,在对每个电影的url反请求,对电影详情发请求获取简介

import scrapy
from movie_spider.items import MovieSpiderItem

class MovieSpider(scrapy.Spider):
    name = 'movie'
    start_urls = ['https://www.4567tv.tv/index.php/vod/show/class/%E5%8A%A8%E4%BD%9C/id/1.html']
    page_num = 1
    def parse(self, response):
        movie_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li')
        for movie_li in movie_list:
            movie_title = movie_li.xpath('./div[1]/a/@title').extract_first()
            movie_url = 'https://www.4567tv.tv'+movie_li.xpath('./div[1]/a/@href').extract_first()
            item = MovieSpiderItem()
            item['movie_title'] = movie_title
            yield scrapy.Request(movie_url,callback=self.parse_movie_detail,meta={'item':item})#请求传参

        if self.page_num < 5:
            self.page_num += 1
            new_url = f'https://www.4567tv.tv/index.php/vod/show/class/%E5%8A%A8%E4%BD%9C/id/1/page/{self.page_num}.html'
			
            yield scrapy.Request(url=new_url,callback=self.parse,meta={'item':item})

    def parse_movie_detail(self,response):
        item = response.meta['item']#取出item,接收
        movie_about = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/span[2]/text()').extract_first()
        item['movie_about'] = movie_about #电影的简介
        yield item

# 深度爬取(传参):在电影首页获取不到电影的简介,在最后提交item时,电影的标题和电影的简介在不同的解析回调函数中,所以先把标题放在item中(但是不提交),传递给下一个回调函数,继续用item,向item中存入要提交的数据,!!!

# 为什么不能设置为全局?
	# 因为在最后提交yield item时会产生数据覆盖现象
    
item['movie_title'] = movie_title #存
meta={'item':item}   # meta参数传递
item = response.meta['item'] #取

items.py

import scrapy

class MovieSpiderItem(scrapy.Item):
    # define the fields for your item here like:
    movie_title = scrapy.Field()
    movie_about = scrapy.Field()
    pass

pipelines.py

import pymysql
class MovieSpiderPipeline(object):
    conn = None
    cursor = None

    def open_spider(self, spider):
        self.conn = pymysql.Connect(host='127.0.0.1', port=3306, user='root', password='123', db='movie_spider',charset='utf8')

    def process_item(self, item, spider):
        movie_title = item['movie_title']
        movie_about = item['movie_about']
        sql = 'insert into movie_info values ("%s","%s")' % (movie_title, movie_about)
        self.cursor = self.conn.cursor()
        try:
            self.cursor.execute(sql)  # 执行正确的话就提交事务
            self.conn.commit()
        except Exception as e:
            print(e)
            self.conn.rollback()  # 回滚,如果出现错误就回滚
        return item

    def close_spider(self, spider):
        self.cursor.close()
        self.conn.close()

9.scrapy中间件的应用

下载中间件

  • 作用:批量拦截请求和响应

  • 拦截请求:

    ​ 1.UA伪装

    ​ 2.代理操作

  • 拦截响应:

    ​ 1.篡改响应数据,篡改不满足需求的响应对象,例如有动态加载的数据,直接请求是请求不到的

    ​ 2.直接更换响应对象

- process_request  request通过下载中间件时,该方法被调用
- process_response 下载结果经过中间件时被此方法处理
- process_exception 下载过程中出现异常时被调用


# process_request(request, spider) :

  当每个request通过下载中间件的时候, 该方法被调用, 该方法必须返回以下三种中的任意一种:

    1. None: Scrapy将继续处理该request, 执行其他的中间件的响应方法, 知道何时的下载器处理函数(download handler)被调用, 该request被执行(其resposne被下载)

    2. Response对象: Scrapy将不会调用人其他的process_request()或者process_exception()方法, 或者响应的下载函数; 其将返回response. 已安装的中间件的process_response()方法胡子爱每个res[onse返回时被调用

    3. Request对象或raise异常: 

      返回Request对象时: Scrapy停止调用process_request方法并重新调度返回的request. 当新的request被执行之后, 相应的中间件将会根据下载的response被调用.

      当raises异常时: 安装的下载中间件的process_exception()方法会被调用. 如果没有任何一个方法处理该异常, 则request的errback(Request.errback)方法被调用. 如果没有代码处理抛出的异常, 则该异常被忽略且不被记录.

# process_response(request, response, spider) :

  process_response的返回值也是有三种:

    1. response对象: 如果返回的是一个Resopnse(可以与传入的response相同, 也可以是全新的对象), 该response会被链中的其他中间件的process_response()方法处理.

    2. Request对象: 如果其返回一个Request对象, 则中间件链停止, 返回的request会被重新调度下载. 处理类似于process_request()返回request所做的那样.

    3. raiseu异常: 如果其抛出一个lgnoreRequest异常, 则调用request的errback(Request.errback). 如果没有代码处理抛出的异常, 则该异常被忽略且不记录.

process_exception(request, exception, spider) :

  当下载处理器(downloader handler)或者process_request()(下载中间件)抛出异常(包括lgnoreRequest异常)时, Scrapy调用process_exception().

# process_exception()也是返回三者中的一个:

    1. 返回None: Scrapy将会继续处理该异常, 接着调用已安装的其他中间件的process_exception()方法,知道所有的中间件都被调用完毕, 则调用默认的异常处理.

    2. 返回Response: 已安装的中间件链的process_response()方法被调用. Scrapy将不会调用任何其他中间件的process_exception()方法.

    3. 返回一个Request对象: 返回的额request将会被重新调用下载. 浙江停止中间件的process_exception()方法的执行, 就如返回一个response那样. 相当于如果失败了可以在这里进行一次失败的重试, 例如当访问一个网站出现因为频繁爬取被封ip就可以在这里设置增加代理继续访问.

爬虫中间件

案例

爬虫程序

# -*- coding: utf-8 -*-
import scrapy
import requests
from wangyi.items import WangyiItem
from selenium import webdriver
class WySpider(scrapy.Spider):
    name = 'wy'
    start_urls = ['https://news.163.com/']

    un_url = []
    bro = webdriver.Chrome(executable_path=r'C:\pycahrm文件\chromedriver.exe')

    def parse(self, response):
        li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
        model_indexs = [3, 4, 6, 7, 8]
        for index in model_indexs:
            li_tag = li_list[index]
            # 解析出了每一个板块对应的url
            model_url = li_tag.xpath('./a/@href').extract_first()
            self.un_url.append(model_url)  # 里面的内容是动态生成的,所以是不满足条件的url,需要在中间件中进行进一步处理
            yield scrapy.Request(model_url,callback=self.news_parse)

    def news_parse(self, response):
        div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div/div/ul/li/div/div')
        for div in div_list:
            title = div.xpath('./div/div[1]/h3/a/text()').extract_first()

            # title = div.xpath('./div/div/div[1]/h3/a/text()').extract_first()
            news_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
            item = WangyiItem()
		
		   #对得到结果进行去除空格换行
            item['title'] = title.replace('\n','')
            item['title'] = title.replace('\t','')
            item['title'] = title.replace(' ','')
            item['title'] = title.replace('\t\n','')


            yield scrapy.Request(news_url,callback=self.detail_parse,meta={'item':item})


    def detail_parse(self,response):
        item = response.meta['item']
        content = response.xpath('//*[@id="endText"]//text()').extract()
        content = ''.join(content)

        #对得到结果进行去除空格换行
        item['content'] = content.replace('\n','')
        item['content'] = content.replace('\t','')
        item['content'] = content.replace(' ','')
        item['content'] = content.replace('\t\n','')
        item['content'] = content.replace('                    \n','')


        yield item

下载中间件

from scrapy import signals
from scrapy.http import HtmlResponse
import time


class WangyiDownloaderMiddleware(object):

    def process_request(self, request, spider):

        return None

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

        if request.url in spider.un_url:
            spider.bro.get(request.url)
            time.sleep(3)
            spider.bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
            time.sleep(2)
            spider.bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
            time.sleep(2)
            page_text = spider.bro.page_source

            # 新的响应的构造方法
            new_response = HtmlResponse(url=request.url,body=page_text,encoding='utf-8',request=request)


            return new_response  #拦截响应后并篡改后,并重新发送新的响应对象,响应的构造方法
        else:
            return response

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

        pass


10.CrawlSpider(全站数据爬取)

创建一个基于CrawlSpider的爬虫文件:scrapy genspider -t crawl sun www.xxx.com

CrawlSpider使用

爬取阳光热线问政平台所有页码中的内容标题以及状态http://wz.sun0769.com/html/top/report.shtml

阳光问政平台共151711条记录,好几百页,实现这几百页的数据爬取
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor #链接提取器
from scrapy.spiders import CrawlSpider, Rule# rule规则解析器
# 链接提取器:提取链接,可以根据指定的规则进行指定的连链接的提取
#     提取规则:allow='正则表达式'
    

# 规则解析器:获取链接提取到的链接,然后对其进行请求发送,根据指定的规则(callback)对请求的页面源码数据进行数据解析

class SunSpider(CrawlSpider):
    name = 'sun'
    start_urls = ['http://wz.sun0769.com/index.php/question/report?page=']
    link = LinkExtractor(allow=r'page=\d+')
    rules = (
        # 实例化一个rule对象,基于linkextractor
        Rule(link, callback='parse_item', follow=True),
    )   # fllow=true  将链接提取器继续作用到链接提取器提取的页码链接所对应 的页面中.如果fllow=Flase,得到的页面数据只是当前页面的显示的页码数据
    

    def parse_item(self, response):
        tr_list = response.xpath('/html//div[8]/table[2]//tr')
        for tr in tr_list:
            title = tr.xpath('./td[3]/a[1]/@title').extract_first()
            status = tr.xpath('./td[4]/span/text()').extract_first()
            print(title,status)

基于CrawlSpider的深度爬取:爬取所有页码中的详情数据

sun.py

import scrapy
from scrapy.linkextractors import LinkExtractor #链接提取器
from scrapy.spiders import CrawlSpider, Rule# rule规则解析器
from sun_spider.items import SunSpiderItem,SunSpiderItem_second

class SunSpider(CrawlSpider):
    name = 'sun'
    start_urls = ['http://wz.sun0769.com/index.php/question/report?page=']
    # href="http://wz.sun0769.com/html/question/201912/437515.shtml"
    link = LinkExtractor(allow=r'page=\d+')
    link_detail = LinkExtractor(allow=r'question/\d+/\d+/.shtml')


    rules = (
        # 实例化一个rule对象,基于linkextractor
        Rule(link, callback='parse_item', follow=True),
        Rule(link_detail, callback='parse_detail'),
    )   # fllow=true  将链接提取器继续作用到链接提取器提取的页码链接所对应 的页面中

    def parse_item(self, response):
        tr_list = response.xpath('/html//div[8]/table[2]//tr')
        for tr in tr_list:
            title = tr.xpath('./td[3]/a[1]/@title').extract_first()
            status = tr.xpath('./td[4]/span/text()').extract_first()
            num = tr.xpath('./td[1]/text()').extract_first()
            item = SunSpiderItem_second()
            item['title'] = title
            item['status'] = status
            item['num'] = num #用于mysql数据库的条件存储,num就是存储的条件
            if  num:
                yield item


    def parse_detail(self,response):
        content = response.xpath('/html/body/div[9]/table[2]/tbody/tr[1]/td//text()').extract()
        num = response.xpath('/html/body/div[9]/table[1]/tbody/tr/td[2]/span[2]/text()').extract_first()
        num = num.split(':')[-1]
        if num:
            content = ''.join(content)
            item = SunSpiderItem()
            item['content'] = content
            item['num'] = num
            yield item

items.py

import scrapy


class SunSpiderItem(scrapy.Item):
    # define the fields for your item here like:
    content = scrapy.Field()
    num = scrapy.Field()
class SunSpiderItem_second(scrapy.Item):
    # define the fields for your item here like:
    title = scrapy.Field()
    status = scrapy.Field()
    num = scrapy.Field()

pipelines.py

# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html

import pymysql
class SunSpiderPipeline(object):
    conn = None
    cursor = None

    def open_spider(self, spider):
        self.conn = pymysql.Connect(host='127.0.0.1', port=3306, user='root', password='123', db='spider',
                                    charset='utf8')

    def process_item(self, item, spider):

        if item.__class__.__name__ == 'SunSpiderItem':
            content = item['content']
            num = item['num']
            sql = f'insert into spider_crawl_sun values {(content)} where num={num}'
            print(num)
            # sql = 'insert into spider_crawl_sun values ("%s") where num=%s'%(content,num)
            self.cursor = self.conn.cursor()
            try:
                self.cursor.execute(sql)  # 执行正确的话就提交事务
                self.conn.commit()
            except Exception as e:
                self.conn.rollback()
                return item

        elif item.__class__.__name__=='SunSpiderItem_second':
            title = item['title']
            status = item['status']
            num = item['num']
            sql = 'insert into spider_crawl_sun values ("%s","%s","%s")'%(title,status,num)
            print(sql)
            self.cursor = self.conn.cursor()
            try:
                self.cursor.execute(sql)  # 执行正确的话就提交事务
                self.conn.commit()
            except Exception as e:

                self.conn.rollback()
                return item

    def close_spider(self, spider):
        self.cursor.close()
        self.conn.close()

11.提升scrapy爬取数据的效率

需要将如下五个步骤配置在配置文件中即可
增加并发:
    默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改CONCURRENT_REQUESTS = 100值为100,并发设置成了为100。

降低日志级别:
    在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:LOG_LEVEL = ‘INFO’

禁止cookie:
    如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:COOKIES_ENABLED = False

禁止重试:
    对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:RETRY_ENABLED = False

减少下载超时:
    如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:DOWNLOAD_TIMEOUT = 10 超时时间为10s
posted @ 2019-12-16 11:59  corei5tj  阅读(810)  评论(0编辑  收藏  举报