爬虫之Scrapy框架

一 介绍(爬虫界的django)

    Scrapy一个开源和协作的框架,其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的,使用它可以以快速、简单、可扩展的方式从网站中提取所需的数据。但目前Scrapy的用途十分广泛,可用于如数据挖掘、监测和自动化测试等领域,也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。

    Scrapy 是基于twisted框架开发而来,twisted是一个流行的事件驱动的python网络框架。因此Scrapy使用了一种非阻塞(又名异步)的代码来实现并发。整体架构大致如下

Components:

  1. 引擎(EGINE)

    引擎负责控制系统所有组件之间的数据流,并在某些动作发生时触发事件。有关详细信息,请参见上面的数据流部分。

  2. 调度器(SCHEDULER)
    用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL的优先级队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
  3. 下载器(DOWLOADER)
    用于下载网页内容, 并将网页内容返回给EGINE,下载器是建立在twisted这个高效的异步模型上的
  4. 爬虫(SPIDERS)
    SPIDERS是开发人员自定义的类,用来解析responses,并且提取items,或者发送新的请求
  5. 项目管道(ITEM PIPLINES)
    在items被提取后负责处理它们,主要包括清理、验证、持久化(比如存到数据库)等操作
  6. 下载器中间件(Downloader Middlewares)
    位于Scrapy引擎和下载器之间,主要用来处理从EGINE传到DOWLOADER的请求request,已经从DOWNLOADER传到EGINE的响应response,你可用该中间件做以下几件事
    1. process a request just before it is sent to the Downloader (i.e. right before Scrapy sends the request to the website);
    2. change received response before passing it to a spider;
    3. send a new Request instead of passing received response to a spider;
    4. pass response to a spider without fetching a web page;
    5. silently drop some requests.
  7. 爬虫中间件(Spider Middlewares)
    位于EGINE和SPIDERS之间,主要工作是处理SPIDERS的输入(即responses)和输出(即requests)

官网链接:https://docs.scrapy.org/en/latest/topics/architecture.html

总结:

#1  通用的网络爬虫框架,爬虫界的django
#2 scrapy执行流程
    5大组件
        -引擎(EGINE):大总管,负责控制数据的流向
        -调度器(SCHEDULER):由它来决定下一个要抓取的网址是什么,去重
        -下载器(DOWLOADER):用于下载网页内容, 并将网页内
        容返回给EGINE,下载器是建立在twisted这个高效的异步模型上的
        -爬虫(SPIDERS):开发人员自定义的类,用来解析responses,并且提取items,或者发送新的请求request
        -项目管道(ITEM PIPLINES):在items被提取后负责处理它们,主要包括清理、验证、持久化(比如存到数据库)等操作
    2大中间件
        -爬虫中间件:位于EGINE和SPIDERS之间,主要工作是处理SPIDERS的输入和输出(用的很少)
        -下载中间件:引擎和下载器之间,加代理,加头,集成selenium        
        
# 3 开发者只需要在固定的位置写固定的代码即可(写的最多的spider)

二 安装

#1 pip3 install scrapy(mac,linux)
#2 windows上(80%能成功,少部分人成功不了,安装不了用下面方法)
    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
# 3 装完就有scrapy命令
    -D:\Python36\Scripts\scrapy.exe  # 用于创建项目

三 命令行工具

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

#2 有两种命令:其中Project-only必须切到项目文件夹下才能执行,而Global的命令则不需要
    Global commands:
        startproject #创建项目
        genspider    #创建爬虫程序
        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

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
示范用法

四 scrapy 创建项目,创建爬虫,运行爬虫

1 创建爬虫项目(得到相应的目录下创建): scrapy startproject 项目名

# scrapy startproject firstscrapy

 之后用pycharm打开

 2 创建爬虫(得先进入爬虫项目根目录,再执行)

# scrapy genspider 爬虫名 爬虫地址
    scrapy genspider chouti dig.chouti.com
# 执行就会在spider文件夹下创建出一个py文件,名字叫chouti

3 运行爬虫(在项目根目录下运行)

scrapy crawl chouti   # 带运行日志
scrapy crawl chouti --nolog  # 不带日志

遇到问题:

Signature of method 'BaiduSpider.parse()' does not match signature of the base method in class 'Spider'

 解决方法:

在parse方法的response参数后添加*args, **kwargs

4 支持右键执行爬虫(右键执行新建的main.py)

# 在项目路径下新建一个main.py
    from scrapy.cmdline import execute
    execute(['scrapy','crawl','chouti','--nolog'])

如图:

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

    # 目录介绍
    firstscrapy  # 项目名字
        firstscrapy #
            -spiders # 所有的爬虫文件放在里面
                -baidu.py # 一个个的爬虫(以后基本上都在这写东西)
                -chouti.py
            -middlewares.py # 中间件(爬虫,下载中间件都写在这)
            -pipelines.py   # 持久化相关写在这(items.py中类的对象)
            -main.py        # 自己加的,执行爬虫
            -items.py       # 一个一个的类,
            -settings.py    # 配置文件
        scrapy.cfg          # 上线相关

文件说明:

  • scrapy.cfg  项目的主配置信息,用来部署scrapy时使用,爬虫相关的配置信息在settings.py文件中。
  • items.py    设置数据存储模板,用于结构化数据,如:Django的Model
  • pipelines    数据处理行为,如:一般结构化的数据持久化
  • settings.py 配置文件,如:递归的层数、并发数,延迟下载等。强调:配置文件的选项必须大写否则视为无效,正确写法USER_AGENT='xxxx'
  • spiders      爬虫目录,如:创建文件,编写爬虫规则

 六 spiders中爬虫程序使用第三方解析

import scrapy
from scrapy.http.request import Request
from bs4 import BeautifulSoup


class ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    allowed_domains = ['dig.chouti.com']
    start_urls = ['http://dig.chouti.com/']

    # # #使用第三方解析
    # def parse(self, response, *args, **kwargs):
    #     # print(response.text)
    #     # 解析数据(第一种方案,自己解析,bs4,lxml)
    #     soup = BeautifulSoup(response.text, 'lxml')
    #     divs = soup.findAll(class_='link-title link-statistics')
    #     for div in divs:
    #         print(div.text)

    # 想继续爬取其他网址
    def parse(self, response, *args, **kwargs):
        # 以后解析都在这
        print(response.status)
        # 假设解析出一个网址(继续爬取)
        return Request('https://www.baidu.com/', dont_filter=True)  # 死循环

继续爬的示例:(爬取cnblogs博客整站信息,持续下一页爬)

持续爬取下一页原理:

# 我们每爬一页就用css选择器来查询,是否存在下一页链接,
# 存在:则拼接出下一页链接,继续爬下一页链接,然后把下一页链接提交给当前爬取的函数parse,继续爬取,继续查找下一页,知道找不到下一页,说明所有页面已经爬完,那结束爬虫

第一版代码:

spiders/cnblogs.py

import scrapy
from firstscrapy.items import CnblogsMysqlItem

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'
    allowed_domains = ['www.cnblogs.com']
    start_urls = ['http://www.cnblogs.com/']

    def parse(self, response):
        # 爬取cnblogs文章,把标题连接地址和文章内容保存到mysql,连续爬取n页
        article_list = response.css('.post-item')
        for article in article_list:
            item = CnblogsMysqlItem()
            title = article.css('.post-item-title::text').extract_first()
            title_url = article.css('.post-item-title::attr(href)').extract_first()
            summary = article.css('.post-item-summary::text').extract_first()
            # 将解析到的数据封装至items对象中,注意:不支持item.title = title方式
            item['title'] = title
            item['title_url'] = title_url
            item['summary'] = summary
            yield item

        # 查找当前页是否存在下一页
        pager = response.xpath('//div[@class="pager"]')
        next_page = pager.xpath('.//a[last()]/text()').extract_first()
        # print(next_page)
        if next_page is ">":
            next_page_url = pager.xpath('.//a[last()]/@href').extract_first()
            next_page_url_full = 'https://www.cnblogs.com%s' % next_page_url
            # print(next_page_url_full)
            yield scrapy.Request(next_page_url_full, callback=self.parse)

items.py

import scrapy


class CnblogsMysqlItem(scrapy.Item):
    title = scrapy.Field()
    title_url = scrapy.Field()
    summary = scrapy.Field()

pipelines.py

from itemadapter import ItemAdapter


class CnblogsFilePipeline(object):
    # 下列都是在重写父类的方法:
    # 开始爬虫时,执行一次
    def open_spider(self, spider):
        self.file = open('cnblogs.txt', 'w', encoding='utf-8')

    # 因为该方法会被执行调用多次,所以文件的开启和关闭操作写在了另外两个只会各自执行一次的方法中。
    def process_item(self, item, spider):
        # 持久化存储到文件中
        self.file.write(item['title'] + '\n')
        self.file.write(item['title_url'] + '\n')
        self.file.write(item['summary'] + '\n')
        return item

    # 结束爬虫时,执行一次
    def close_spider(self, spider):
        self.file.close()


import pymysql


class CnblogsMysqlPipeline(object):
    def open_spider(self, spider):
        self.conn = pymysql.connect(host='127.0.0.1', user='root', password="123456",
                                    database='cnblogs', port=3306)

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

    def process_item(self, item, spider):
        cursor = self.conn.cursor()
        sql = 'insert into c_cnblogs (title,title_url,summary)values(%s,%s,%s) '
        cursor.execute(sql, [item['title'], item['title_url'], item['summary']])
        self.conn.commit()
        return item

settings.py

# 开启管道
ITEM_PIPELINES = {
    'firstscrapy.pipelines.CnblogsFilePipeline': 300,
    'firstscrapy.pipelines.CnblogsMysqlPipeline': 305,
    # 'firstscrapy.pipelines.ChoutiFilePipeline': 300,  #300表示为优先级,值越小优先级越高
    # 'firstscrapy.pipelines.ChoutiMysqlPipeline': 305,
}

第二版代码

spiders/cnblogs.py

import scrapy
from cnblogs.items import CnblogsMysqlItem


class CnblogSpider(scrapy.Spider):
    name = 'cnblog'
    allowed_domains = ['www.cnblogs.com']
    start_urls = ['http://www.cnblogs.com/']
    '''
    爬取原则:scrapy默认是先进先出
        -深度优先:详情页先爬   队列,先进去先出来
        -广度优先:每一页先爬   栈  ,后进先出
    '''

    def parse(self, response):
        # 爬取cnblogs文章,把标题连接地址和文章内容保存到mysql,连续爬取n页
        article_list = response.css('.post-item')
        for article in article_list:
            item = CnblogsMysqlItem()
            title = article.css('.post-item-title::text').extract_first()
            title_url = article.css('.post-item-title::attr(href)').extract_first()
            summary = article.css('.post-item-summary::text').extract_first()
            # 将解析到的数据封装至items对象中,注意:不支持item.title = title方式
            item['title'] = title
            item['title_url'] = title_url
            item['summary'] = summary
            # 要继续爬取详情
            # callback如果不写,默认回调到parse方法
            # 如果写了,响应回来的对象就会调到自己写的解析方法中
            # meta将参数传到回调函数
            yield scrapy.Request(title_url, callback=self.parser_detail, meta={'item': item})

        # 查找当前页是否存在下一页
        pager = response.xpath('//div[@class="pager"]')
        next_page = pager.xpath('.//a[last()]/text()').extract_first()
        # print(next_page)
        if next_page is ">":
            next_page_url = pager.xpath('.//a[last()]/@href').extract_first()
            next_page_url_full = 'https://www.cnblogs.com%s' % next_page_url
            print(next_page_url_full)
            yield scrapy.Request(next_page_url_full, callback=self.parse)
            # yield scrapy.Request(next_page_url_full, callback=self.parse)

    def parser_detail(self, response):

        item = response.meta.get('item')
        # item哪里来
        content = response.css('#cnblogs_post_body').extract_first()
        item['content'] = content
        print(item)
        yield item
spiders/cnblogs.py

items.py

import scrapy


class CnblogsMysqlItem(scrapy.Item):
    title = scrapy.Field()
    title_url = scrapy.Field()
    summary = scrapy.Field()
    content = scrapy.Field()
items.py

pipelines.py

class CnblogsFilePipeline(object):
    # 下列都是在重写父类的方法:
    # 开始爬虫时,执行一次
    def open_spider(self, spider):
        self.file = open('cnblogs.txt', 'w', encoding='utf-8')

    # 因为该方法会被执行调用多次,所以文件的开启和关闭操作写在了另外两个只会各自执行一次的方法中。
    def process_item(self, item, spider):
        # 持久化存储到文件中
        self.file.write(item['title'] + '\n')
        self.file.write(item['title_url'] + '\n')
        self.file.write(item['summary'] + '\n')
        self.file.write(item['content'] + '\n')
        return item

    # 结束爬虫时,执行一次
    def close_spider(self, spider):
        self.file.close()


import pymysql


class CnblogsMysqlPipeline(object):
    def open_spider(self, spider):
        self.conn = pymysql.connect(host='127.0.0.1', user='root', password="123456",
                                    database='cnblogs', port=3306)

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

    def process_item(self, item, spider):
        cursor = self.conn.cursor()
        sql = 'insert into c_cnblogs (title,title_url,summary, content)values(%s,%s,%s,%s) '
        cursor.execute(sql, [item['title'], item['title_url'], item['summary'], item['content']])
        self.conn.commit()
        return item
pipelines.py

settings.py

# 开启管道
ITEM_PIPELINES = {
    'cnblogs.pipelines.CnblogsFilePipeline': 300,
    'cnblogs.pipelines.CnblogsMysqlPipeline': 305,
}
settings.py

补充:scrapy的请求传参

# 把要传递的数据放到meta中
yield Request(urlmeta={'item':item})
# 在response对象中取出来
item=response.meta.get('item')

七 spiders使用自带解析

### 使用自带解析
    '''
    xpath:
        -response.xpath('xpath语法').extract()
        -response.xpath('xpath语法').extract_first()
        -response.xpath('//a[contains(@class,"link-title")]/text()').extract()  # 取文本
        -response.xpath('//a[contains(@class,"link-title")]/@href').extract()  #取属性
    -css
        -response.css('.link-title::text').extract()  # 取文本
        -response.css('.link-title::attr(href)').extract_first()  # 取属性
    '''
    def parse(self, response):
        # 只有css和xpath
        # title_list=response.css('.link-title')
        # title_list=response.xpath('.//a[contains(@class,"link-title")]/text()').extract()
        # title_list=response.xpath('.//a[contains(@class,"link-title")]').extract()
        # print(len(title_list))
        # print(title_list)

        # 解析出所有的标题和图片地址
        div_list=response.xpath('//div[contains(@class,"link-item")]')
        for div in div_list:
            #extract() 取出列表,即便有一个也是列表
            #extract_first() 取出第一个值
            # title=div.css('.link-title::text').extract()  #
            # title=div.css('.link-title::text').extract_first()
            # url=div.css('.link-title::attr(href)').extract()[0]
            # url=div.css('.link-title::attr(href)').extract_first()
            url=div.xpath('.//a[contains(@class,"link-title")]/@href').extract_first()
            # print(title)
            print(url)

自带解析器使用方法详解

response.selector.css()
response.selector.xpath()
可简写为
response.css()
response.xpath()

#1 //与/
response.xpath('//body/a/')#
response.css('div a::text')

>>> response.xpath('//body/a') #开头的//代表从整篇文档中寻找,body之后的/代表body的儿子
[]
>>> response.xpath('//body//a') #开头的//代表从整篇文档中寻找,body之后的//代表body的子子孙孙
[<Selector xpath='//body//a' data='<a href="image1.html">Name: My image 1 <'>, <Selector xpath='//body//a' data='<a href="image2.html">Name: My image 2 <'>, <Selector xpath='//body//a' data='<a href="
image3.html">Name: My image 3 <'>, <Selector xpath='//body//a' data='<a href="image4.html">Name: My image 4 <'>, <Selector xpath='//body//a' data='<a href="image5.html">Name: My image 5 <'>]

#2 text
>>> response.xpath('//body//a/text()')
>>> response.css('body a::text')

#3、extract与extract_first:从selector对象中解出内容
>>> response.xpath('//div/a/text()').extract()
['Name: My image 1 ', 'Name: My image 2 ', 'Name: My image 3 ', 'Name: My image 4 ', 'Name: My image 5 ']
>>> response.css('div a::text').extract()
['Name: My image 1 ', 'Name: My image 2 ', 'Name: My image 3 ', 'Name: My image 4 ', 'Name: My image 5 ']

>>> response.xpath('//div/a/text()').extract_first()
'Name: My image 1 '
>>> response.css('div a::text').extract_first()
'Name: My image 1 '

#4、属性:xpath的属性加前缀@
>>> response.xpath('//div/a/@href').extract_first()
'image1.html'
>>> response.css('div a::attr(href)').extract_first()
'image1.html'

#4、嵌套查找
>>> response.xpath('//div').css('a').xpath('@href').extract_first()
'image1.html'

#5、设置默认值
>>> response.xpath('//div[@id="xxx"]').extract_first(default="not found")
'not found'

#4、按照属性查找
response.xpath('//div[@id="images"]/a[@href="image3.html"]/text()').extract()
response.css('#images a[@href="image3.html"]/text()').extract()

#5、按照属性模糊查找
response.xpath('//a[contains(@href,"image")]/@href').extract()
response.css('a[href*="image"]::attr(href)').extract()

response.xpath('//a[contains(@href,"image")]/img/@src').extract()
response.css('a[href*="imag"] img::attr(src)').extract()

response.xpath('//*[@href="image1.html"]')
response.css('*[href="image1.html"]')

#6、正则表达式
response.xpath('//a/text()').re(r'Name: (.*)')
response.xpath('//a/text()').re_first(r'Name: (.*)')

#7、xpath相对路径
>>> res=response.xpath('//a[contains(@href,"3")]')[0]
>>> res.xpath('img')
[<Selector xpath='img' data='<img src="image3_thumb.jpg">'>]
>>> res.xpath('./img')
[<Selector xpath='./img' data='<img src="image3_thumb.jpg">'>]
>>> res.xpath('.//img')
[<Selector xpath='.//img' data='<img src="image3_thumb.jpg">'>]
>>> res.xpath('//img') #这就是从头开始扫描
[<Selector xpath='//img' data='<img src="image1_thumb.jpg">'>, <Selector xpath='//img' data='<img src="image2_thumb.jpg">'>, <Selector xpath='//img' data='<img src="image3_thumb.jpg">'>, <Selector xpa
th='//img' data='<img src="image4_thumb.jpg">'>, <Selector xpath='//img' data='<img src="image5_thumb.jpg">'>]

#8、带变量的xpath
>>> response.xpath('//div[@id=$xxx]/a/text()',xxx='images').extract_first()
'Name: My image 1 '
>>> response.xpath('//div[count(a)=$yyy]/@id',yyy=5).extract_first() #求有5个a标签的div的id
'images'

八 settings.py

使用示例

1 默认情况,scrapy会去遵循爬虫协议
2 修改配置文件参数,强行爬取,不遵循协议
    -ROBOTSTXT_OBEY = False
3 USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36'
4 LOG_LEVEL='ERROR'   #因为之前的启动参数是--nolog,这个不太好,在settings中添加LOG_LEVEL参数后,可以在终端打印错误级别的信息

参数详解:

#==>第一部分:基本配置<===
#1、项目名称,默认的USER_AGENT由它来构成,也作为日志记录的日志名
BOT_NAME = 'Amazon'

#2、爬虫应用路径
SPIDER_MODULES = ['Amazon.spiders']
NEWSPIDER_MODULE = 'Amazon.spiders'

#3、客户端User-Agent请求头(这里改是改全局的)
#USER_AGENT = 'Amazon (+http://www.yourdomain.com)'

#4、是否遵循爬虫协议
# Obey robots.txt rules
ROBOTSTXT_OBEY = False

#5、是否支持cookie,cookiejar进行操作cookie,默认开启
#COOKIES_ENABLED = False

#6、Telnet用于查看当前爬虫的信息,操作爬虫等...使用telnet ip port ,然后通过命令操作
#TELNETCONSOLE_ENABLED = False
#TELNETCONSOLE_HOST = '127.0.0.1'
#TELNETCONSOLE_PORT = [6023,]

#7、Scrapy发送HTTP请求默认使用的请求头
#DEFAULT_REQUEST_HEADERS = {
#   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
#   'Accept-Language': 'en',
#}



#===>第二部分:并发与延迟<===
#1、下载器总共最大处理的并发请求数,默认值16
#CONCURRENT_REQUESTS = 32

#2、每个域名能够被执行的最大并发请求数目,默认值8
#CONCURRENT_REQUESTS_PER_DOMAIN = 16

#3、能够被单个IP处理的并发请求数,默认值0,代表无限制,需要注意两点
#I、如果不为零,那CONCURRENT_REQUESTS_PER_DOMAIN将被忽略,即并发数的限制是按照每个IP来计算,而不是每个域名
#II、该设置也影响DOWNLOAD_DELAY,如果该值不为零,那么DOWNLOAD_DELAY下载延迟是限制每个IP而不是每个域
#CONCURRENT_REQUESTS_PER_IP = 16

#4、如果没有开启智能限速,这个值就代表一个规定死的值,代表对同一网址延迟请求的秒数
#DOWNLOAD_DELAY = 3

#===>第三部分:智能限速/自动节流:AutoThrottle extension<===
#一:介绍
from scrapy.contrib.throttle import AutoThrottle #http://scrapy.readthedocs.io/en/latest/topics/autothrottle.html#topics-autothrottle
设置目标:
1、比使用默认的下载延迟对站点更好
2、自动调整scrapy到最佳的爬取速度,所以用户无需自己调整下载延迟到最佳状态。用户只需要定义允许最大并发的请求,剩下的事情由该扩展组件自动完成

#二:如何实现?
在Scrapy中,下载延迟是通过计算建立TCP连接到接收到HTTP包头(header)之间的时间来测量的。
注意,由于Scrapy可能在忙着处理spider的回调函数或者无法下载,因此在合作的多任务环境下准确测量这些延迟是十分苦难的。 不过,这些延迟仍然是对Scrapy(甚至是服务器)繁忙程度的合理测量,而这扩展就是以此为前提进行编写的。

#三:限速算法
自动限速算法基于以下规则调整下载延迟
#1、spiders开始时的下载延迟是基于AUTOTHROTTLE_START_DELAY的值
#2、当收到一个response,对目标站点的下载延迟=收到响应的延迟时间/AUTOTHROTTLE_TARGET_CONCURRENCY
#3、下一次请求的下载延迟就被设置成:对目标站点下载延迟时间和过去的下载延迟时间的平均值
#4、没有达到200个response则不允许降低延迟
#5、下载延迟不能变的比DOWNLOAD_DELAY更低或者比AUTOTHROTTLE_MAX_DELAY更高

#四:配置使用
#开启True,默认False
AUTOTHROTTLE_ENABLED = True
#起始的延迟
AUTOTHROTTLE_START_DELAY = 5
#最小延迟
DOWNLOAD_DELAY = 3
#最大延迟
AUTOTHROTTLE_MAX_DELAY = 10
#每秒并发请求数的平均值,不能高于 CONCURRENT_REQUESTS_PER_DOMAIN或CONCURRENT_REQUESTS_PER_IP,调高了则吞吐量增大强奸目标站点,调低了则对目标站点更加”礼貌“
#每个特定的时间点,scrapy并发请求的数目都可能高于或低于该值,这是爬虫视图达到的建议值而不是硬限制
AUTOTHROTTLE_TARGET_CONCURRENCY = 16.0
#调试
AUTOTHROTTLE_DEBUG = True
CONCURRENT_REQUESTS_PER_DOMAIN = 16
CONCURRENT_REQUESTS_PER_IP = 16



#===>第四部分:爬取深度与爬取方式<===
#1、爬虫允许的最大深度,可以通过meta查看当前深度;0表示无深度
# DEPTH_LIMIT = 3

#2、爬取时,0表示深度优先Lifo(默认);1表示广度优先FiFo

# 后进先出,深度优先
# DEPTH_PRIORITY = 0
# SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleLifoDiskQueue'
# SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.LifoMemoryQueue'
# 先进先出,广度优先

# DEPTH_PRIORITY = 1
# SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleFifoDiskQueue'
# SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.FifoMemoryQueue'

#3、调度器队列
# SCHEDULER = 'scrapy.core.scheduler.Scheduler'
# from scrapy.core.scheduler import Scheduler

#4、访问URL去重
# DUPEFILTER_CLASS = 'step8_king.duplication.RepeatUrl'



#===>第五部分:中间件、Pipelines、扩展<===
#1、Enable or disable spider middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
#    'Amazon.middlewares.AmazonSpiderMiddleware': 543,
#}

#2、Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
   # 'Amazon.middlewares.DownMiddleware1': 543,
}

#3、Enable or disable extensions
# See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
#EXTENSIONS = {
#    'scrapy.extensions.telnet.TelnetConsole': None,
#}

#4、Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
   # 'Amazon.pipelines.CustomPipeline': 200,
}



#===>第六部分:缓存<===
"""

1. 启用缓存
   目的用于将已经发送的请求或相应缓存下来,以便以后使用

   from scrapy.downloadermiddlewares.httpcache import HttpCacheMiddleware
   from scrapy.extensions.httpcache import DummyPolicy
   from scrapy.extensions.httpcache import FilesystemCacheStorage
   """
   # 是否启用缓存策略
   # HTTPCACHE_ENABLED = True

# 缓存策略:所有请求均缓存,下次在请求直接访问原来的缓存即可
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
# 缓存策略:根据Http响应头:Cache-Control、Last-Modified 等进行缓存的策略
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.RFC2616Policy"

# 缓存超时时间
# HTTPCACHE_EXPIRATION_SECS = 0

# 缓存保存路径
# HTTPCACHE_DIR = 'httpcache'

# 缓存忽略的Http状态码
# HTTPCACHE_IGNORE_HTTP_CODES = []

# 缓存存储的插件
# HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

#===>第七部分:线程池<===
REACTOR_THREADPOOL_MAXSIZE = 10

#Default: 10
#scrapy基于twisted异步IO框架,downloader是多线程的,线程数是Twisted线程池的默认大小(The maximum limit for Twisted Reactor thread pool size.)

#关于twisted线程池:
http://twistedmatrix.com/documents/10.1.0/core/howto/threading.html

#线程池实现:twisted.python.threadpool.ThreadPool
twisted调整线程池大小:
from twisted.internet import reactor
reactor.suggestThreadPoolSize(30)

#scrapy相关源码:
D:\python3.6\Lib\site-packages\scrapy\crawler.py

#补充:
windows下查看进程内线程数的工具:
    https://docs.microsoft.com/zh-cn/sysinternals/downloads/pslist
    或
    https://pan.baidu.com/s/1jJ0pMaM
    

```
命令为:
pslist |findstr python

```

linux下:top -p 进程id

#===>第八部分:其他默认配置参考<===
D:\python3.6\Lib\site-packages\scrapy\settings\default_settings.py

 九 scrapy的持久化存储(重点)

持久化方式一:parser函数必须返回列表套字典的形式(用的少)

spiders/chouti.py

import scrapyclass ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    allowed_domains = ['dig.chouti.com']
    start_urls = ['http://dig.chouti.com/']


    # 持久化方案1
    def parse(self, response):
        ll = []
        div_list = response.xpath('//div[contains(@class,"link-item")]')
        for div in div_list:
            title = div.css('.link-title::text').extract_first()
            url = div.css('.link-title::attr(href)').extract_first()
            photo_url = div.css('.image-scale::attr(src)').extract_first()
            # 持久化:方案一(用的少)parser必须返回列表套字典的形式
            ll.append({'title': title, 'url': url, 'photo_url': photo_url})
        return ll

在pycharm的terminal终端执行命令进行持久化操作(存储到文件),支持格式有('json', 'jsonlines', 'jl','csv', 'xml', 'marshal', 'pickle')

E:\django_project\firstscrapy>scrapy crawl chouti -o chouti.csv   # -o:表示导出

持久化方式二:高级,pipline item存储(mysql,redis,文件)

方式二持久化流程:

# 持久化流程:
    1.爬虫文件爬取到数据后,需要将数据封装到items对象中。
    2.使用yield关键字将items对象提交给pipelines管道进行持久化操作。
    3.在管道文件中的process_item方法中接收爬虫文件提交过来的item对象,然后编写持久化存储的代码将item对象中存储的数据进行持久化存储
    4.settings.py配置文件中开启管道

1 在spiders/items.py中写一个类

import scrapy


class ChoutiFileItem(scrapy.Item):
    title = scrapy.Field()    # 存储抽屉新热榜抽屉的标题
    url = scrapy.Field()      # 存储抽屉标题的url
    photo_url = scrapy.Field() # 存储抽屉图片的url


class ChoutiMysqlItem(scrapy.Item):
    title = scrapy.Field()
    url = scrapy.Field()
    photo_url = scrapy.Field()

2 在spinders/chouti.py中导入,实例化,把数据放进去

import scrapy
# 导入item类
from firstscrapy.items import ChoutiFileItem


class ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    allowed_domains = ['dig.chouti.com']
    start_urls = ['http://dig.chouti.com/']

    # 持久化方案二
    def parse(self, response):

        div_list = response.xpath('//div[contains(@class,"link-item")]')
        for div in div_list:
            item = ChoutiFileItem()   # 实例化item对象
            title = div.css('.link-title::text').extract_first()
            url = div.css('.link-title::attr(href)').extract_first()
            photo_url = div.css('.image-scale::attr(src)').extract_first()
            if not photo_url:
                photo_url = ''

            # 将解析到的数据封装至items对象中,注意:不支持item.title = title方式
            item['title'] = title
            item['url'] = url
            item['photo_url'] = photo_url

            yield item  # 提交item到管道文件(pipelines.py)

3 在pipelines.py中写ChoutiFilePipeline

# open_spider(开始的时候)
# close_spider(结束的时候)
# process_item(在这持久化)

代码:

from itemadapter import ItemAdapter


class ChoutiFilePipeline(object):
    # 下列都是在重写父类的方法:
    # 开始爬虫时,执行一次
    def open_spider(self, spider):
        self.file = open('chouti.txt', 'w', encoding='utf-8')

    # 因为该方法会被执行调用多次,所以文件的开启和关闭操作写在了另外两个只会各自执行一次的方法中。
    def process_item(self, item, spider):
        # 持久化存储到文件中
        self.file.write(item['title'] + '\n')
        self.file.write(item['url'] + '\n')
        self.file.write(item['photo_url'] + '\n')
        return item

    # 结束爬虫时,执行一次
    def close_spider(self, spider):
        self.file.close()


import pymysql


class ChoutiMysqlPipeline(object):
    def open_spider(self, spider):
        self.conn = pymysql.connect(host='127.0.0.1', user='root', password="123456",
                                    database='chouti', port=3306)

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

    def process_item(self, item, spider):
        cursor = self.conn.cursor()
        sql = 'insert into article (title,url,photo_url)values(%s,%s,%s) '
        cursor.execute(sql, [item['title'], item['url'], item['photo_url']])
        self.conn.commit()
        return item

4 在setting中配置(数字越小,级别越高)

# 开启管道
ITEM_PIPELINES = {
   'firstscrapy.pipelines.ChoutiFilePipeline': 300,  #300表示为优先级,值越小优先级越高
   'firstscrapy.pipelines.ChoutiMysqlPipeline': 305,
}

 

posted @ 2020-08-05 14:40  耗油炒白菜  阅读(241)  评论(0编辑  收藏  举报