Python爬虫5-Scrapy框架

511 阅读36分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情

参考:

docs.scrapy.org/en/latest/i…

海燕:www.cnblogs.com/haiyan123/p…

简书:www.jianshu.com/p/43029ea38…

scrapy学习笔记(有示例版)www.jianshu.com/p/1e669c17c…

《Learning Scrapy》(中文版)www.jianshu.com/p/6c9baeb60…

崔庆才Scrapy:cuiqingcai.com/8364.html

一、Scrapy框架架构

1.Scrapy框架介绍:

Scrapy 是一个基于 Twisted 的异步处理框架,是纯 Python 实现的爬虫框架,其架构清晰,模块之间的耦合程度低,可扩展性极强,可以灵活完成各种需求。我们只需要定制开发几个模块就可以轻松实现一个爬虫。 为什么用Scrapy?

写一个爬虫,需要做很多的事情。比如:发送网络请求、数据解析、数据存储、反反爬虫机制(更换ip代理、设置请求头等)、异步请求等。这些工作如果每次都要自己从零开始写的话,比较浪费时间。因此Scrapy把一些基础的东西封装好了,在它上面写爬虫可以变的更加的高效(爬取效率和开发效率)。因此真正在公司里一些上了量的爬虫,都是使用Scrapy框架来解决。

Scrapy不适合干什么呢?

  • 如果网站中数据是ajax动态或js动态加载的数据,不适合用Scrapy,推荐用requests
  • 网站中有验证码/登录等高超的反爬虫技术加持,适合用requests

2.Scrapy架构图:

图1: image.png 图2: image.png

3.Scrapy框架模块功能:

  1. Scrapy Engine(引擎)Scrapy框架的核心部分。负责在SpiderItemPipelineDownloaderScheduler中间通信、传递数据等。
  2. Spider(爬虫):发送需要爬取的链接给引擎,最后引擎把其他模块请求回来的数据再发送给爬虫,爬虫就去解析想要的数据。这个部分是我们开发者自己写的,因为要爬取哪些链接,页面中的哪些数据是我们需要的,都是由程序员自己决定。
  3. Scheduler(调度器):负责接收引擎发送过来的请求,并按照一定的方式进行排列和整理,负责调度请求的顺序等。
  4. Downloader(下载器):负责接收引擎传过来的下载请求,然后去网络上下载对应的数据再交还给引擎。
  5. Item Pipeline(管道):负责将Spider(爬虫)传递过来的数据进行保存。具体保存在哪里,应该看开发者自己的需求。
  6. Downloader Middlewares(下载器中间件):下载器中间件,位于引擎和下载器之间的钩子框架,主要是处理引擎与下载器之间的请求及响应。
  7. Spider Middlewares(Spider中间件):蜘蛛中间件,位于引擎和蜘蛛之间的钩子框架,主要工作是处理Spider输入的响应和输出的结果及新的请求。

4.命令行工具

#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

scrapy genspider -h 查看scrapy genspider ...下面的所有命令 示例用法:

#全局命令:所有文件夹都使用的命令,可以不依赖与项目文件也可以执行
#项目的文件夹下执行的命令
1、scrapy startproject Myproject #创建项目
   cd Myproject
2、scrapy genspider baidu www.baidu.com  #创建爬虫程序,baidu是爬虫名,定位爬虫的名字
#写完域名以后默认会有一个url,
3、scrapy settings --get BOT_NAME  #获取配置文件
#全局:
4、scrapy runspider baidu.py
5、scrapy runspider AMAZON\spiders\amazon.py  #执行爬虫程序
   在项目下:scrapy crawl amazon  #指定爬虫名,定位爬虫程序来运行程序
    #robots.txt 反爬协议:在目标站点建一个文件,里面规定了哪些能爬,哪些不能爬
    # 有的国家觉得是合法的,有的是不合法的,这就产生了反爬协议
    # 默认是ROBOTSTXT_OBEY = True
    # 修改为ROBOTSTXT_OBEY = False  #默认不遵循反扒协议
6、scrapy shell https://www.baidu.com  #直接超目标站点发请求
     response
     response.status
     response.body
     view(response)
7、scrapy view https://www.taobao.com #如果页面显示内容不全,不全的内容则是ajax请求实现的,以此快速定位问题
8、scrapy version  #查看版本
9、scrapy version -v #查看scrapy依赖库锁依赖的版本
10、scrapy fetch --nolog http://www.logou.com    #获取响应的内容
11、scrapy fetch --nolog --headers http://www.logou.com  #获取响应的请求头
    (venv3_spider) E:\twisted\scrapy框架\AMAZON>scrapy fetch --nolog --headers http://www.logou.com
    > Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    > Accept-Language: en
    > User-Agent: Scrapy/1.5.0 (+https://scrapy.org)
    > Accept-Encoding: gzip,deflate
    >
    < Content-Type: text/html; charset=UTF-8
    < Date: Tue, 23 Jan 2018 15:51:32 GMT
    < Server: Apache
    >代表请求
    <代表返回
10、scrapy shell http://www.logou.com #直接朝目标站点发请求
11、scrapy check  #检测爬虫有没有错误
12、scrapy list  #所有的爬虫名
13、scrapy parse http://quotes.toscrape.com/ --callback parse #验证回调函函数是否成功执行
14、scrapy bench #压力测试

爬虫程序以后都在spiders文件夹下的爬虫.py中写

二、Scrapy快速入门

1.安装和文档:

  1. 安装:通过pip install scrapy即可安装。
  2. Scrapy官方文档:doc.scrapy.org/en/latest
  3. Scrapy中文文档:scrapy-chs.readthedocs.io/zh_CN/lates…

注意:

  1. ubuntu上安装scrapy之前,需要先安装以下依赖: sudo apt-get install python3-dev build-essential python3-pip libxml2-dev libxslt1-dev zlib1g-dev libffi-dev libssl-dev,然后再通过pip install scrapy安装。
  2. 如果在windows系统下,提示这个错误ModuleNotFoundError: No module named 'win32api',那么使用以下命令可以解决:pip install pypiwin32

2.快速入门:

2.1创建项目:

要使用Scrapy框架创建项目,需要通过命令来创建。首先进入到你想把这个项目存放的目录。然后使用以下命令创建:

scrapy startproject [项目名称]

2.2目录结构介绍:

image.png

以下介绍下主要文件的作用:

  1. items.py:用来存放爬虫爬取下来数据的模型。
  2. middlewares.py:用来存放各种中间件的文件。
  3. pipelines.py:用来将items的数据模型持久化存储。
  4. settings.py:本爬虫的一些配置信息(比如请求头、多久发送一次请求、ip代理池等)。
  5. scrapy.cfg:项目的配置文件。
  6. spiders包:以后所有的爬虫,都是存放到这个里面,还要通过genspider命令创建一个爬虫文件。

注意:一般创建爬虫文件时,以网站域名命名

默认只能在cmd中执行爬虫,如果想在pycharm中执行需要做:

#在项目目录下新建:start.py
from scrapy.cmdline import execute
# execute(['scrapy', 'crawl', 'amazon','--nolog'])  #不要日志打印
# execute(['scrapy', 'crawl', 'amazon'])

#我们可能需要在命令行为爬虫程序传递参数,就用下面这样的命令
#scrapy crawl amzaon -a keyword=iphone8
execute(['scrapy', 'crawl', 'amazon1','-a','keyword=iphone8','--nolog'])  #不要日志打印

2.3使用Scrapy框架爬取糗事百科段子:

1、创建一个爬虫:

scrapy gensipder qsbk_spider qiushibaike.com

进入qsbk项目文件夹下创建一个名字叫做qsbk_spider的爬虫, _allowed_domains = ['qiushibaike.com'] _会限制只能在qiushibaike.com这个域名下爬,一般我们可以注释掉。 注意:爬虫名字不能和项目名称一样。 2、爬虫原始代码解析:

import scrapy
class QsbkSpider(scrapy.Spider):
    name = 'qsbk'
    allowed_domains = ['qiushibaike.com']
    start_urls = ['http://qiushibaike.com/']
    def parse(self, response):
        pass

其实这些代码我们完全可以自己手动去写,而不用命令。只不过是不用命令,自己写这些代码比较麻烦。 要创建一个Spider,那么必须自定义一个类,继承自scrapy.Spider,然后在这个类中定义三个属性和一个方法。

  1. name:这个爬虫的名字,名字必须是唯一的。
  2. allow_domains:允许的域名。爬虫只会爬取这个域名下的网页,其他不是这个域名下的网页会被自动忽略(一般我们会注释掉)。
  3. start_urls:爬虫从这个变量中的url开始。
  4. parse:引擎会把下载器下载回来的数据扔给爬虫解析,爬虫再把数据传给这个parse方法。这个是个固定的写法。这个方法的作用有两个,第一个是提取想要的数据。第二个是生成下一个请求的url。

3、修改**settings.py**代码: 在做一个爬虫之前,一定要记得修改setttings.py中的设置。两个地方是强烈建议设置的。

  1. ROBOTSTXT_OBEY设置为False。默认是True。即遵守机器协议,那么在爬虫的时候,scrapy首先去找robots.txt文件,如果没有找到。则直接停止爬取。
  2. DEFAULT_REQUEST_HEADERS添加User-Agent。这个也是告诉服务器,我这个请求是一个正常的请求,不是一个爬虫。

4、完成的爬虫代码:

  • spider.QsbkSpider代码:
import scrapy
from qsbk.items import QsbkItem
class QsbkSpider(scrapy.Spider):
    name = 'qsbk'
    allowed_domains = ['qiushibaike.com']
    start_urls = ['https://www.qiushibaike.com/text/']
    
    def parse(self, response):
        outerbox = response.xpath("//div[@id='content-left']/div")
        items = []
        for box in outerbox:
            author = box.xpath(".//div[contains(@class,'author')]//h2/text()").extract_first().strip()
            content = box.xpath(".//div[@class='content']/span/text()").extract_first().strip()
            item = QsbkItem()
            item["author"] = author
            item["content"] = content
            items.append(item)
            return items

导入items找不到的问题:

  • items.py代码:
import scrapy

class QsbkItem(scrapy.Item):
     author = scrapy.Field()
     content = scrapy.Field()
  • pipelines.py代码:
import json
class AbcspiderPipeline(object):
     def __init__(self):
         self.items = []
	
     def open_spider(self,spider):
        print("开始爬虫--------")
        
     def process_item(self, item, spider):
         self.items.append(dict(item))
         print("="*40)
         return item
     
     def close_spider(self,spider):
         with open('qsbk.json','w',encoding='utf-8') as fp:
             json.dump(self.items,fp,ensure_ascii=False)
  • settings.py配置
#一个项目中可以有多个pipelines
ITEM_PIPELINES = {
   'qsbk.pipelines.QsbkPipeline': 300,		#优先级,值越小优先级越高
}
  • 运行scrapy项目

运行scrapy项目。需要在终端,进入项目所在的路径,然后scrapy crawl [爬虫名字]即可运行指定的爬虫。如果不想每次都在命令行中运行,那么可以把这个命令写在一个文件中。以后就在pycharm中执行运行这个文件就可以了。比如现在新创建一个文件叫做start.py,然后在这个文件中填入以下代码:

from scrapy import cmdline

cmdline.execute(["scrapy",'crawl','qsbk_spider'])
cmdline.execute(["scrapy",'crawl','qsbk_spider','--nolog'])	#不打印日志

3.Scrapy爬虫快速入门总结:

  1. response是一个scrapy.http.response.html.HtmlResponse对象。可以执行xpathcss语法来提取数据。
  2. 提取出来的数据,是一个Selector或者是一个SelectorList对象。如果想要获取其中的字符串。那么应该执行getall或者get方法。
  3. getall方法:获取Selector中的所有文本。返回的是一个列表。
  4. get方法:获取的是Selector中的第一个文本。返回的是一个str类型。
  5. 如果数据解析回来,要传给pipline处理。那么可以使用yield来返回。或者是收集所有的item。最后统一使用return返回。
  6. item:建议在items.py中定义好模型。以后就不要使用字典。
  7. pipeline:这个是专门用来保存数据的。其中有三个方法是会经常用的。
    • open_spider(self,spider):当爬虫被打开的时候执行。
    • process_item(self,item,spider):当爬虫有item传过来的时候会被调用。
    • close_spider(self,spider):当爬虫关闭的时候会被调用。 要激活piplilne,应该在settings.py中,设置ITEM_PIPELINES。示例如下:
ITEM_PIPELINES = {
   'qsbk.pipelines.QsbkPipeline': 300,
}

4.BaseItemExporter序列化

JsonItemExporterJsonLinesItemExporter: 保存json数据的时候,可以使用这两个类,让操作变得得更简单。 blog.csdn.net/weixin_4325…

  1. JsonItemExporter:这个是每次把数据添加到内存中组合成一个列表,最后统一写入到磁盘中。优点是,存储的数据是一个满足json规则的数据(列表里面每一项是一个字典)。缺点是如果数据量比较大,那么比较耗内存。
from scrapy.exporters import JsonItemExporter
class QsbkPipeline(object):
    def __init__(self):
    	self.fp = open("duanzi.json",'wb')
        self.exporter = JsonItemExporter(self.fp,ensure_ascii=False,encoding='utf-8')
        self.exporter.start_exporting()	
    
    def open_spider(self,spider):
        print('爬虫开始了...')
	
    def process_item(self, item, spider):
        self.exporter.export_item(item)
        return item
    
    def close_spider(self,spider):
        self.exporter.finish_exporting()
        self.fp.close()
        print('爬虫结束了...')
  1. JsonLinesItemExporter:这个是每次调用export_item的时候就把这个item存储到硬盘中。坏处是每一个字典是一行,整个文件不是一个满足json格式的文件。好处是每次处理数据的时候就直接存储到了硬盘中,这样不会耗内存,数据也比较安全。示例代码如下:
from scrapy.exporters import JsonLinesItemExporter
class QsbkPipeline(object):
    def __init__(self):
        self.fp = open("duanzi.json",'wb')
        self.exporter = JsonLinesItemExporter(self.fp,ensure_ascii=False,encoding='utf-8')
    def open_spider(self,spider):
        print('爬虫开始了...')
    def process_item(self, item, spider):
        self.exporter.export_item(item)
        return item
    def close_spider(self,spider):
        self.fp.close()
        print('爬虫结束了...')

5.Spiders爬虫

介绍:

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

Spiders会循环做如下事情

#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)

Spiders总共提供了五种类

  • scrapy.spiders.Spider #scrapy.Spider等同于scrapy.spiders.Spider
  • scrapy.spiders.CrawlSpider
  • scrapy.spiders.XMLFeedSpider
  • scrapy.spiders.CSVFeedSpider
  • scrapy.spiders.SitemapSpider

导入使用

import scrapy
class QsbkSpiderSpider(scrapy.Spider):
    name = 'qsbk_spider'    #当前爬虫文件名
    allowed_domains = ['qiushibaike.com']   #允许在该域名下爬虫
    start_urls = ['https://www.qiushibaike.com/text/page/1/']     #开始的url
    def parse(self, response):
        pass

类 scrapy.spiders.Spider

这是最简单的spider类,任何其他的spider类都需要继承它(包含你自己定义的)。 该类不提供任何特殊的功能,它仅提供了一个默认的start_requests方法默认从start_urls中读取url地 址发送requests请求,并且默认parse作为回调函数。

import scrapy
class AmazonSpider(scrapy.Spider):
    def __init__(self,keyword=None,*args,**kwargs):  
        #在start.py文件里面传进来的keyword,在这里接收了
        super(AmazonSpider,self).__init__(*args,**kwargs)
        self.keyword = keyword
    name = 'amazon'  # 必须唯一
    allowed_domains = ['www.amazon.cn']  # 允许域
    start_urls = ['http://www.amazon.cn/']  # 如果你没有指定发送的请求地址,会默认使用只一个
	#可以在项目的settings.py中配置
    custom_settings = {  # 自定制配置文件,自己设置了用自己的,没有就找父类的
        "BOT_NAME": 'HAIYAN_AMAZON',
        'REQUSET_HEADERS': {},
    }
    def start_requests(self):
        url = 'https://www.amazon.cn/s/ref=nb_sb_noss_1/461-4093573-7508641?'
        url+=urlencode({"field-keywords":self.keyword})
        print(url)
        yield  scrapy.Request(
            url,
            callback = self.parse_index,  #指定回调函数
            dont_filter = True,  #不去重,这个也可以自己定制
            # dont_filter = False,  #去重,这个也可以自己定制
            # meta={'a':1}  #meta代理的时候会用
        )
        #如果要想测试自定义的dont_filter,可多返回结果重复的即可

三、CrawlSpider

在上一个糗事百科的爬虫案例中。我们是自己在解析完整个页面后获取下一页的url,然后重新发送一个请求。有时候我们想要这样做,只要满足某个条件的url,都给我进行爬取。那么这时候我们就可以通过CrawlSpider来做全站数据爬取。 CrawlSpider继承自Spider,只不过是在之前的基础之上增加了新的功能,可以定义爬取的url的规则,以后scrapy碰到满足条件的url都进行爬取,而不用手动的yield Request

1.CrawlSpider爬虫:

1.1 创建CrawlSpider爬虫:

之前创建爬虫的方式是通过scrapy genspider [爬虫名字] [域名]的方式创建的。如果想要创建CrawlSpider爬虫,那么应该通过以下命令创建:

#创建项目
scrapy startproject [项目名称]

#创建爬虫,-t 指定爬虫模板为CrawlSpider
scrapy genspider -t crawl [爬虫名字] [域名]

1.2 LinkExtractors链接提取器:

使用LinkExtractors可以不用程序员自己提取想要的url,然后发送请求。这些工作都可以交给LinkExtractors,他会在所有爬的页面中找到满足规则的url,实现自动的爬取。以下对LinkExtractors类做一个简单的介绍:

class scrapy.linkextractors.LinkExtractor(
    allow = (),
    deny = (),
    allow_domains = (),
    deny_domains = (),
    deny_extensions = None,
    restrict_xpaths = (),
    tags = ('a','area'),
    attrs = ('href'),
    canonicalize = True,
    unique = True,
    process_value = None
)

主要参数讲解:

  • allow:允许的url。所有满足这个正则表达式的url都会被提取。
  • deny:禁止的url。所有满足这个正则表达式的url都不会被提取。
  • allow_domains:允许的域名。只有在这个里面指定的域名的url才会被提取。
  • deny_domains:禁止的域名。所有在这个里面指定的域名的url都不会被提取。
  • restrict_xpaths:严格的xpath。和allow共同过滤链接。

1.3 Rule规则类:

定义爬虫的规则类。以下对这个类做一个简单的介绍:

class scrapy.spiders.Rule(
    link_extractor, 
    callback = None, 
    cb_kwargs = None, 
    follow = None, 
    process_links = None, 
    process_request = None
)

主要参数讲解:

  • link_extractor:一个LinkExtractor对象,用于定义爬取规则。
  • callback:满足这个规则的url,应该要执行哪个回调函数。因为CrawlSpider使用了parse作为回调函数,因此不要覆盖parse作为回调函数自己的回调函数。
  • follow:指定根据该规则从response中提取的链接是否需要跟进。
  • process_links:从link_extractor中获取到链接后会传递给这个函数,用来过滤不需要爬取的链接。

1.4 微信小程序社区CrawlSpider案例

1.5 总结

需要使用LinkExtractorRule。这两个东西决定爬虫的具体走向。

  1. allow设置规则的方法:要能够限制在我们想要的url上面。不要跟其他的url产生相同的正则表达式即可。
  2. 什么情况下使用follow:如果在爬取页面的时候,需要将满足当前条件的url再进行跟进,那么就设置为True。否则设置为Fasle。
  3. 什么情况下该指定callback:如果这个url对应的页面,只是为了获取更多的url,并不需要里面的数据,那么可以不指定callback。如果想要获取url对应页面中的数据,那么就需要指定一个callback。

爬虫程序.py的基本结构

四、Scrapy Shell:

  1. 可以使我们可以在未启动spider爬虫的情况下尝试及调试代码(解析数据),即方便我们做一些数据提取的测试代码。
  2. 更为方便的是,我们也可以直接用来测试XPath或CSS表达式,而不用import导入相应模块。通过查看其运行的结果,方便了我们分析目标网页,并从中测试我们的表达式是否提取到了数据。
  3. 如果想要执行scrapy命令,那么毫无疑问,肯定是要先进入到scrapy所在的环境中。
  4. 如果想要读取某个项目的配置信息,那么应该先进入到这个项目中。再执行命令:scrapy shell

Scrapy内置的Selector选择器 在Scrapy中使用xpath或是CSS等,之所以不用再导入第三方包,是因为在Scrapy中已内置了相应的Selector选择器。 Selector有四个基本的方法:

  • xpath( )

我们通过书写xpath表达式,可使程序返回该表达式所对应的所有节点的selector list选择器列表,从而筛选我们想要定位的元素。

  • extract( )

序列化节点为Unicode字符串,并返回list列表,scrapy底层对extract进行了封装提供一个更友好的接口get() 获取单个匹配对象,getall() 获取一个匹配对象列表。

  • css( )

根据css表达式,返回该表达式所对应的所有节点的selector list选择器列表,语法和 BeautifulSoup4相同。

  • re( )

根据书写的正则表达式,对数据进行提取,返回Unicode字符串list列表。

使用步骤:

  1. cmd进入到scrapy 项目所在的目录中
  2. scrapy shell 目标网址
  3. 等待系统进入交互模式后,我们使用命令:response (查看目标状态响应状态)
  4. 在scrapy shell中输入命令:result = response.xpath('//tr[@class="even"]/td/a/text()').get()
  5. 输入命令:exit() 即可退出scrapy shell 终端模式。

五、Request和Response对象

www.osgeo.cn/scrapy/topi…

1.Response响应对象

class scrapy.http.Response(url[, status=200, headers=None, body=b'', 
                               flags=None, request=None])

Response对象一般是由scrapy给你自动构建的,因此开发者不需要关系如何创建Response对象,而是如何使用它,Response对象有很多属性,可以用来提取数据内容,主要属性如下:

  1. meta:从其他请求传过来的meta属性,可以用来保持多个请求之间的数据连接。
  2. encoding:返回当前字符串编码和解码格式。
  3. text:将返回的数据作为Unicode字符串返回。
  4. body:将返回的数据作为bytes字符串返回。
  5. xpath:xpath选择器。
  6. css:css选择器
  7. response.urljoin(url):response.urljoin(url)将url拼接成完整的路径,会根据当前地址下的主机端口等拼接。

2.发送POST请求

2.1 Request子类FormRequest

有时候我们想要在请求数据的时候发送POST请求,那么这时候需要使用Request的子类FormRequest来实现,如果想要在爬虫一开始的时候就发送POST请求,那么需要在爬虫类中重写start_request()方法,并且不再使用start_urls里的url

class scrapy.http.FormRequest(url[, formdata, ...])

formdata (dict or iterable of tuples) -- 是模拟HTML表单POST提交的数据和一对键值对字段,在你的爬虫中你可以返回一个FormRequest对象,这些数据将被分配给请求体。 基本用法:

def start_requests(self):
    return [FormRequest(url="http://www.example.com/post/action",
                        formdata={'name': 'John Doe', 'age': '27'},
                        callback=self.after_post)]

2.2 使用FormRequest.from_response()模拟用户登录

使用formRequest.from_response())模拟用户登录 网站通常通过 元素,例如与会话相关的数据或身份验证令牌(用于登录页)。当进行抓取时,希望这些字段自动预填充,并且只覆盖其中的几个字段,例如用户名和密码。你可以使用 FormRequest.from_response() 方法。

import scrapy
def authentication_failed(response):
    # TODO: Check the contents of the response and return True if it failed
    # or False if it succeeded.
    pass
class LoginSpider(scrapy.Spider):
    name = 'example.com'
    start_urls = ['http://www.example.com/users/login.php']
    def parse(self, response):
        return scrapy.FormRequest.from_response(
            response,
            formdata={'username': 'john', 'password': 'secret'},
            callback=self.after_login
        )
    def after_login(self, response):
        if authentication_failed(response):
            self.logger.error("Login failed")
            return
        # continue scraping with authenticated session...

参考:blog.csdn.net/qq_43546676…

2.3 模拟登陆豆瓣

先找到填写表单时发送的post请求的地址 可以看见其post的地址为:[https://accounts.douban.com/j/mobile/login/basic](https://accounts.douban.com/j/mobile/login/basic) 然后用postman模拟发送POST请求访问:[https://accounts.douban.com/j/mobile/login/basic](https://accounts.douban.com/j/mobile/login/basic) 代码如下:

def parse(self, response):
        data = {
            'ck':'',
            'ticket':'',
            'name': '********',    # 输入你自己的账号
            'password': '********',  # 输入你自己的密码
            'remember': 'true'
        }
        print("登入中...")
        return FormRequest(
            url='https://accounts.douban.com/j/mobile/login/basic',
            formdata=data,
            meta={'cookiejar':response.meta['cookiejar']},
 # 如果需要多次提交表单,且url一样,那么就必须加此参数dont_filter,防止被当成重复网页过滤掉了
            dont_filter=True,
            callback=self.after_login
        )
def after_login(self,response):
    ....

3.Request请求对象

Response对象在我们写爬虫爬取一页数据需要重新发送一个请求的时候调用,这个类需要传递一些参数,主要属性如下:

from scrapy.http import Request
class Request(object_ref):
    
    def __init__(self, url, callback=None, method='GET', headers=None, body=None,cookies=None, meta=None, encoding='utf-8', priority=0,
dont_filter=False, errback=None, flags=None):
        # url是要爬取的网址(必填)
        # callback是回调函数
        # method是请求的方式post还是get
        # headers是浏览器伪装的头信息
        # body是网页源代码信息
        # cookies是登入某网站后,网站在你电脑上保留的信息
        # meta要携带或者传递的信息
        # encoding是编码方式
        # priority用来设置访问网站的优先级
        # dont_filter是否允许重复爬取网站
  • url (string) -- 此请求的URL
  • callback (callable) -- 将以此请求的响应(一旦下载)作为第一个参数调用的函数。有关详细信息,请参阅 向回调函数传递附加数据 下面。如果请求未指定回调,则蜘蛛 [parse()](https://www.osgeo.cn/scrapy/topics/spiders.html#scrapy.spiders.Spider.parse) 将使用方法。请注意,如果在处理过程中引发异常,则改为调用errback。
  • method (string) -- 此请求的HTTP方法。默认为 'GET' .
  • meta (dict) -- 的初始值 [Request.meta](https://www.osgeo.cn/scrapy/topics/request-response.html#scrapy.http.Request.meta) 属性。如果给定,则将浅复制传入此参数的dict。
  • body (str or unicode) -- 请求主体。如果A unicode 传递,然后将其编码为 str 使用 encoding 通过(默认为 utf-8 )如果 body 如果未给定,则存储空字符串。无论此参数的类型如何,存储的最终值都将是 str (永远) unicodeNone
  • headers (dict) -- 此请求的头。dict值可以是字符串(对于单值头)或列表(对于多值头)。如果 None 作为值传递,HTTP头将不会被发送。
  • cookies (dict or list) -- 请求cookies。

the request cookies. These can be sent in two forms. 1.Using a dict:

request_with_cookies = Request(url="http://www.example.com",
                               cookies={'currency': 'USD', 'country': 'UY'})

2.Using a list of dicts:

request_with_cookies = Request(url="http://www.example.com",
                               cookies=[{'name': 'currency',
                                        'value': 'USD',
                                        'domain': 'example.com',
                                        'path': '/currency'}])
  • encoding (string) -- 此请求的编码(默认为 'utf-8' )此编码将用于对URL进行百分比编码并将正文转换为 str (如果给予 unicode
  • priority (int) -- 此请求的优先级(默认为 0 )调度程序使用优先级定义用于处理请求的顺序。优先级值较高的请求将更早执行。允许负值以表示相对较低的优先级。
  • dont_filter (boolean) -- 指示调度程序不应筛选此请求。当您希望多次执行相同的请求时,可以使用此选项忽略重复的筛选器。小心使用,否则会进入爬行循环。默认为 False .
  • errback (callable) -- 如果在处理请求时引发任何异常,则将调用的函数。这包括404 HTTP错误等失败的页面。它收到一个 Twisted Failure 实例作为第一个参数。有关详细信息,请参阅 使用errbacks捕获请求处理中的异常 下面。
  • flags (list) -- 发送到请求的标志可用于日志记录或类似用途。
  • cb_kwargs (dict) -- 具有任意数据的dict,将作为关键字参数传递到请求的回调。1.7版本之后推荐使用,meta方式逐渐被高版本遗弃。

请求成功后回调函数

def parse_page1(self, response):
    return scrapy.Request("http://www.example.com/some_page.html",
                          callback=self.parse_page2)

def parse_page2(self, response):
    # this would log http://www.example.com/some_page.html
    self.logger.info("Visited %s", response.url)	#日志

向回调函数传递数据

def parse(self, response):
    request = scrapy.Request('http://www.example.com/index.html',
                             callback=self.parse_page2,
                             cb_kwargs=dict(main_url=response.url))
    request.cb_kwargs['foo'] = 'bar'  # add more arguments for the callback
    yield request
    
def parse_page2(self, response, main_url, foo):
    yield dict(
        main_url=main_url,
        other_url=response.url,
        foo=foo,
    )

使用errbacks在请求过程中捕获异常

它接收一个Failures作为第一个参数,可以用来跟踪连接建立超时,DNS错误等,示例如下:

from scrapy.spidermiddlewares.httperror import HttpError
from twisted.internet.error import DNSLookupError
from twisted.internet.error import TimeoutError, TCPTimedOutError
class ErrbackSpider(scrapy.Spider):
    name = "errback_example"
    start_urls = [
        "http://www.httpbin.org/",              # HTTP 200 expected
        "http://www.httpbin.org/status/404",    # Not found error
        "http://www.httpbin.org/status/500",    # server issue
        "http://www.httpbin.org:12345/",        # non-responding host, timeout expected
        "http://www.httphttpbinbin.org/",       # DNS error expected
    ]
    def start_requests(self):
        for u in self.start_urls:
            yield scrapy.Request(u, callback=self.parse_httpbin,
                                    errback=self.errback_httpbin,
                                    dont_filter=True)
    def parse_httpbin(self, response):
        self.logger.info('Got successful response from {}'.format(response.url))
        # do something useful here...
    def errback_httpbin(self, failure):
        # log all failures
        self.logger.error(repr(failure))
        # in case you want to do something special for some errors,
        # you may need the failure's type:
        if failure.check(HttpError):
            # these exceptions come from HttpError spider middleware
            # you can get the non-200 response
            response = failure.value.response
            self.logger.error('HttpError on %s', response.url)
        elif failure.check(DNSLookupError):
            # this is the original request
            request = failure.request
            self.logger.error('DNSLookupError on %s', request.url)
        elif failure.check(TimeoutError, TCPTimedOutError):
            request = failure.request
            self.logger.error('TimeoutError on %s', request.url)

Request.meta

image.png 可以把item通过meta传递柜callback,在callback中通过responser.meta['item'] 取到item

image.png

特殊的键 Request.meta属性可以包含任何数据,但是有一些特殊的键可以被Scrapy及其内置扩展识别

  • [dont_redirect](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#std-reqmeta-dont_redirect)
  • [dont_retry](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#std-reqmeta-dont_retry)
  • [handle_httpstatus_list](https://docs.scrapy.org/en/latest/topics/spider-middleware.html#std-reqmeta-handle_httpstatus_list)
  • [handle_httpstatus_all](https://docs.scrapy.org/en/latest/topics/spider-middleware.html#std-reqmeta-handle_httpstatus_all)
  • [dont_merge_cookies](https://docs.scrapy.org/en/latest/topics/request-response.html#std-reqmeta-dont_merge_cookies)
  • [cookiejar](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#std-reqmeta-cookiejar)
  • [dont_cache](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#std-reqmeta-dont_cache)
  • [redirect_reasons](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#std-reqmeta-redirect_reasons)
  • [redirect_urls](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#std-reqmeta-redirect_urls)
  • [bindaddress](https://docs.scrapy.org/en/latest/topics/request-response.html#std-reqmeta-bindaddress)
  • [dont_obey_robotstxt](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#std-reqmeta-dont_obey_robotstxt)
  • [download_timeout](https://docs.scrapy.org/en/latest/topics/request-response.html#std-reqmeta-download_timeout)
  • [download_maxsize](https://docs.scrapy.org/en/latest/topics/settings.html#std-reqmeta-download_maxsize)
  • [download_latency](https://docs.scrapy.org/en/latest/topics/request-response.html#std-reqmeta-download_latency)
  • [download_fail_on_dataloss](https://docs.scrapy.org/en/latest/topics/request-response.html#std-reqmeta-download_fail_on_dataloss)
  • [proxy](https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#std-reqmeta-proxy)
  • ftp_user (See [FTP_USER](https://docs.scrapy.org/en/latest/topics/settings.html#std-setting-FTP_USER) for more info)
  • ftp_password (See [FTP_PASSWORD](https://docs.scrapy.org/en/latest/topics/settings.html#std-setting-FTP_PASSWORD) for more info)
  • [referrer_policy](https://docs.scrapy.org/en/latest/topics/spider-middleware.html#std-reqmeta-referrer_policy)
  • [max_retry_times](https://docs.scrapy.org/en/latest/topics/request-response.html#std-reqmeta-max_retry_times)

4.JSONRequest

jsonRequest类扩展了基 Request 类,具有处理JSON请求的功能。 classscrapy.http.JSONRequest(url[, ... data, dumps_kwargs]) 这个 JSONRequest 类向构造函数添加两个新参数。其余参数与 Request 在这里没有记录。 使用 JSONRequest 将设置 Content-Type 报头到 application/json 和 Accept 报头到 application/json, text/javascript, /; q=0.01 参数: data (JSON serializable object) -- 是需要对JSON编码并分配给主体的任何JSON可序列化对象。如果 Request.body 提供了参数。此参数将被忽略。如果 Request.body 未提供参数,并且提供了数据参数 Request.method 将被设置为 'POST' 自动地。 dumps_kwargs (dict) -- 将传递给基础的参数 json.dumps 方法,用于将数据序列化为JSON格式。 json请求使用示例 使用JSON负载发送JSON POST请求: data = { 'name1': 'value1', 'name2': 'value2', } yield JSONRequest(url='www.example.com/post/action', data=data)

案例

模拟登录人人网:

  1. 想要发送post请求,那么推荐使用scrapy.FormRequest方法。可以方便的指定表单数据。
  2. 如果想在爬虫一开始的时候就发送post请求,那么应该重写start_requests方法。在这个方法中,发送post请求。

模拟登陆果壳网

account.guokr.com/sign_in/

六、下载文件和图片

docs.scrapy.org/en/latest/t…

1.下载文件和图片

scrapy为下载item中包含的文件(比如爬取到产品时,同时也想保存对应的图片)提供了一个可重用的item pipelines。这些pipelines有些共同的方法和结构(我们称之为media pipeline)一般来说使用的较多的是Files Pipeline或者Images Pipeline

2.为什么要使用scrapy内置的下载文件的方法

  • 避免重新下载最近已经下载的文件
  • 指定存储文件的位置(文件系统目录、AmazonS3存储桶、Google云存储桶)
  • 将所有下载的图像转换为通用格式,如jpg或png
  • 可以方便的生成缩略图
  • 检查图像的宽度/高度以确保它们满足最小限制
  • 异步下载,效率非常高

这些管道还保留当前正在计划下载的文件URL的内部队列,并将到达的包含相同文件的响应连接到该队列。这样可以避免在多个项目共享同一文件时多次下载同一文件。

3.下载文件的File Pipeline

当使用File Pipeline下载文件时,按照下面步骤完成:

  1. 在items.py中定义两个属性分别是file_urlsfilesfile_urls是用来存储需要下载的文件url链接,需要给一个列表。
  2. 当文件下载完成后,会把文件下载相关信息存储到item的files属性中,比如下载路径、下载的url和文件的校验码等。
  3. 在配置文件settings.py汇总配置FILES_STORE = '/path/to/valid/dir',这个配置是用来设置文件下载下来的路径。
  4. 启动pipeline:在ITEM_PIPELINES中设置scrapy.pipelines.FilesPipeline:1

提示:

  • 使用文件的url进行SHA1 哈希操作生成文件名

例如这个图片url:www.example.com/image.jpg 它的SHA1哈希值是:3afec3b4765f8f0a07b78f98c07b83f013567a0a

  • 文件将会被下载和存储在下面文件夹中
<IMAGES_STORE>/full/3afec3b4765f8f0a07b78f98c07b83f013567a0a.jpg
//<IMAGES_STORE>使我们在settings.py中设置的存储路径

4.下载图片的Images Pipeline

Images Pipeline和Files Pipeline的使用很相似,只是默认的字段名不同。Images Pipeline。image_urls来表示一个item的图片URL,用images字段来表示下载的图片信息。使用Images Pipeline来处理图片文件的好处是,你可以配置一些额外的功能,比如生成缩略图和根据图片的大小来过滤图片。Images Pipeline使用Pillow来生成缩略图和将图像归一化为JPEG/RGB格式,所以你需要安装Pillow库才能使用。,Images Pipeline 下载图片时,步骤如下:

  1. 定义好一个Item,然后在这个Item中定义两个属性,分别是image_urlsimagesimage_urls是用来存储需要下载的图片的url链接的,需要给一个列表。
  2. 当图片下载完成后,会把图片下载相关信息存储到itemimages属性中,比如下载路径、下载的url和图片的校验码等。
  3. 在配置文件settings.py汇总配置IMAGES_STORE = '/path/to/valid/dir',这个配置是用来设置图片下载的存储路径。
  4. 启动pipeline:在ITEM_PIPELINES中设置scrapy.pipelines.ImagesPipeline:1

重写自定义下载路径

案例:汽车之家雷克萨斯LS图片

settings.py

ROBOTSTXT_OBEY = False
#设置请求头
DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36',
    'referer':'https://www.autohome.com.cn/341/',
}
ITEM_PIPELINES = {
   # 'autohomeLS.pipelines.AutohomelsPipeline': 300,
    #内建的图片下载管道
    # 'scrapy.pipelines.ImagesPipeline':1,
    #自定义的图片下载管道
    'autohomeLS.pipelines.AutohomeImagesPipeline':1,
}
#图片存储地址
IMAGES_STORE = os.path.join(os.path.dirname(os.path.dirname(__file__)),'images')
if not os.path.exists(IMAGES_STORE):
    os.mkdir(IMAGES_STORE)

pipelines.py

# -*- coding: utf-8 -*-
import os
from urllib import request
from scrapy.pipelines.images import ImagesPipeline
from autohomeLS import settings
#普通的下载方法
class AutohomelsPipeline(object):
    def __init__(self):
        self.path = os.path.join(os.path.dirname(os.path.dirname(__file__)),'images')
        if not os.path.exists(self.path):
            os.mkdir(self.path)
    def open_spider(self,spider):
        print("开始爬虫--------")
    def process_item(self, item, spider):
        category = item['category']
        urls = item['urls']
        category_path = os.path.join(self.path,category)
        if not os.path.exists(category_path):
            os.mkdir(category_path)
        for url in urls:
            image_name = url.split("__")[-1]
            print("---",image_name,"---")
            request.urlretrieve(url,os.path.join(category_path,image_name))
        return item
    def clsoe_spider(self,spider):
        print("结束爬虫--------")
#继承ImagesPipeline 的图片下载
class AutohomeImagesPipeline(ImagesPipeline):
    def get_media_requests(self, item, info):
# 需要获得category,因此重写get_media_requests()方法
# 为其[Request(x) for x in item.get(self.images_urls_field, [])] 对象request增加item属性
        request_objs = super(AutohomeImagesPipeline, self).get_media_requests(item, info)
        for request_obj in request_objs:
            request_obj.item = item
        return request_objs
    def file_path(self, request, response=None, info=None):
        path = super(AutohomeImagesPipeline, self).file_path(request, response=None, info=None)
        category = request.item.get('category')
        image_store = settings.IMAGES_STORE
        category_path = os.path.join(image_store,category)
        if not os.path.exists(category_path):
            os.mkdir(category_path)
        image_name = path.replace("full/",'')
        image_path = os.path.join(category_path,image_name)
        return image_path

items.py

class AutohomelsItem(scrapy.Item):
    # define the fields for your item here like:
    #1.普通的方法下载图片
    # category = scrapy.Field()
    # urls = scrapy.Field()
    #2.内置的Images Pipeline下载图片
    category = scrapy.Field()
    images = scrapy.Field()		#必须
    image_urls = scrapy.Field()		#必须

autohome_spider.py

# -*- coding: utf-8 -*-
import scrapy
from autohomeLS.items import AutohomelsItem
class AutohomeSpiderSpider(scrapy.Spider):
    name = 'autohome_spider'
    allowed_domains = ['car.autohome.com.cn']
    start_urls = ['https://car.autohome.com.cn/pic/series/341.html']
    def parse(self, response):
        ## SelectorList -> list  可切片
        uiboxs = response.xpath('//div[@class="column grid-16"]//div[@class="uibox"]')[1:]
        for uibox in uiboxs:
            category = uibox.xpath('.//div[1]/a/text()').get().strip()
            image_urls = uibox.xpath('.//ul/li/a/img/@src').getall()
            image_urls = list(map(lambda url:response.urljoin(url.strip()),image_urls))
            item = AutohomelsItem(category=category,image_urls=image_urls)
            print(item)
            yield item

总结

  • 可以同时使用Files Pipeline and Images Pipeline

七、中间件

1.下载器中间件Downloader Middleware

创建项目时自动帮我们在middlewares.py中创建了爬虫中间件类和下载器中间件类

from scrapy import signals
#爬虫中间件
class HttpbintestSpiderMiddleware(object):
    # Not all methods need to be defined. If a method is not defined,
    # scrapy acts as if the spider middleware does not modify the
    # passed objects.
    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s
    def process_spider_input(self, response, spider):
        # Called for each response that goes through the spider
        # middleware and into the spider.
        # Should return None or raise an exception.
        return None
    def process_spider_output(self, response, result, spider):
        # Called with the results returned from the Spider, after
        # it has processed the response.
        # Must return an iterable of Request, dict or Item objects.
        for i in result:
            yield i
    def process_spider_exception(self, response, exception, spider):
        # Called when a spider or process_spider_input() method
        # (from other spider middleware) raises an exception.
        # Should return either None or an iterable of Response, dict
        # or Item objects.
        pass
    def process_start_requests(self, start_requests, spider):
        # Called with the start requests of the spider, and works
        # similarly to the process_spider_output() method, except
        # that it doesn’t have a response associated.
        # Must return only requests (not items).
        for r in start_requests:
            yield r
    def spider_opened(self, spider):
        spider.logger.info('Spider opened: %s' % spider.name)

        #下载器中间件
class HttpbintestDownloaderMiddleware(object):
    # Not all methods need to be defined. If a method is not defined,
    # scrapy acts as if the downloader middleware does not modify the
    # passed objects.
    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s
    
    def process_request(self, request, spider):
        # Called for each request that goes through the downloader
        # middleware.
        # Must either:
        # - return None: continue processing this request
        # - or return a Response object
        # - or return a Request object
        # - or raise IgnoreRequest: process_exception() methods of
        #   installed downloader middleware will be called
        return None
    
    def process_response(self, request, response, spider):
        # Called with the response returned from the downloader.
        # Must either;
        # - return a Response object
        # - return a Request object
        # - or raise IgnoreRequest
        return response
    
    def process_exception(self, request, exception, spider):
        # Called when a download handler or a process_request()
        # (from other downloader middleware) raises an exception.
        # Must either:
        # - return None: continue processing this exception
        # - return a Response object: stops process_exception() chain
        # - return a Request object: stops process_exception() chain
        pass
    
    def spider_opened(self, spider):
        spider.logger.info('Spider opened: %s' % spider.name)

注意:需要在settings.py中打开中间件(一般关注下载器中间件)

image.png

下载器中间件是引擎和下载器之间通信的中间件,在这个中间件中我们可以设置代理,更换请求头等来达到反反爬虫的目的。 要写下载器中间件,通常要在中间件中实现3个方法: 一是process_request(self, request, spider),这个方法是在请求发送之前会执行。 二是process_response(self, request, response, spider),这个方法是数据下载到引擎之前执行。 三是 process_exception(self, request, exception, spider),这个方式是出现异常时执行

process_request(self, request, spider):

这个方法是在请求发送之前执行,一般可以在这里面设置随机代理ip,请求头等。

  • 参数:
    • request:发送请求的Request对象
    • spider:发送请求的spider对象
  • 返回值:
    • 返回None:如果返回None,scrapy将继续处理该request,执行其他中间件中的相应方法,直到合适的下载器处理函数被调用。
    • 返回Response对象:scrapy将不会调用任何其他的process_request()方法,将直接返回这个response对象,已经激活的中间件process_response()方法则会在每个response返回时被调用。
    • 返回Resquest对象:不再使用之前的request对象去下载数据,而是根据现在返回的request对象返回数据。
    • 如果这个方法抛出异常,则会调用process_exception()

process_response(self, request, response, spider):

这个方法是下载器下载的数据到引擎中间执行的方法。

  • 参数:
    • request:发送请求的Request对象
    • response:被处理的Response对象
    • spider:发送请求的spider对象
  • 返回值:
    • 返回Response对象:会将这个新的response对象传给其他中间件,最终传给爬虫。
    • 返回Request对象:下载器链接被切断,返回的request会重新被下载器调度下载。
    • 返回resquest对象:不再使用之前的request对象去下载数据,而是根据现在返回的request对象返回数据。
    • 如果这个方法抛出异常,则会调用request的errback()方法,如果未指定该方法则抛出异常。

中间件-设置随机请求头

请求头大全:www.useragentstring.com/ 爬虫在频繁访问一个页面的时候,这个请求头如果一直保持一致,那么很容易被服务器发现从而禁止这个请求头的访问。因此我们需要在访问这个页面之前随机的更改请求头,这样才可以避免爬虫被禁。随机更改请求头可以在中间件middlewares.py中实现,在请求发送给服务器之前从提前准备好的请求头列表中随机选择一个请求头,代码如下:

class DoubanmoviesDownloaderMiddleware(object):
    USER_AGENTS = [
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36",
        "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0",
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
        "Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.9.168 Version/11.50",
        "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; Tablet PC 2.0; .NET4.0E)",
        "Mozilla/5.0 (Windows; U; Windows NT 6.1; ) AppleWebKit/534.12 (KHTML, like Gecko) Maxthon/3.0 Safari/534.12",
        "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.33 Safari/534.3 SE 2.X MetaSr 1.0",
        "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 QQBrowser/6.9.11079.201",
    ]
    def process_request(self, request, spider):
        user_agent = random.choice(self.USER_AGENTS)
        request.headers["User-Agent"] = user_agent

image.png

中间件-设置IP代理

请求失败时,如果是IP限制了,我们一般在process_exception中设置代理IP,然后返回requests image.png

2.爬虫中间件Spider Middleware

3.cookie中间件

scrapy 通过meta中的cookiejar 保证一个请求链的cookie信息 www.cnblogs.com/themost/p/7… blog.csdn.net/weixin_3885…

4.简书网爬虫案例

爬取推荐作者专栏的所有文章

技术要点:

  • selenium+chromedriver集成到scrapy(即修改下载器中间件)

  • 存储到mysql数据库,存储如下字段:

  • CrawlSpider 提取符合条件的所有链接
  • xshell测试解析数据
  • scrapy从网页中爬取和解析数据是异步多线程的,而数据库pymysql的commit()和execute()在提交数据是同步的。所以scrapy的数据解析速度,要远高于数据的写入数据库的速度。如果数据写入过慢,会造成数据库写入的阻塞,影响数据库写入的效率。 因此我们需要使用scrapy内置的twisted异步IO框架,实现数据的异步写入。

爬虫分析:

  • 简书的文章详情页网址都是如下格式,其中47ecd39f0a13应该是文章pk

www.jianshu.com/p/47ecd39f0…

方案一:execute() commit() 同步写入数据库

settings.py

DOWNLOADER_MIDDLEWARES = {
   'jianshu.middlewares.JianshuDownloaderMiddleware': 543,
}
ITEM_PIPELINES = {
   'jianshu.pipelines.JianshuPipeline': 300,
}

items.py

class JianshuItem(scrapy.Item):
    title = scrapy.Field()
    content = scrapy.Field()
    author = scrapy.Field()
    avatar = scrapy.Field()
    pub_time = scrapy.Field()
    origin_url = scrapy.Field()
    read_count = scrapy.Field()
    like_count = scrapy.Field()
    word_count = scrapy.Field()
    subjects = scrapy.Field()

pipelines.py

import pymysql
class JianshuPipeline(object):
    DBPARAMS = {
            'host':'127.0.0.1',
            'port':3306,
            'user':'root',
            'password':'root',
            'database':'jianshu_spider_data',
            'charset':'utf8'
        }
    def __init__(self):
        self.connect = pymysql.connect(**self.DBPARAMS)
        self.cursor = self.connect.cursor()
        self._sql = None
    def process_item(self, item, spider):
        self.cursor.execute(self.sql,(item['title'],item['author'],item['avatar'],item['pub_time'],item['content'],
                                      item['read_count'],item['like_count'],item['word_count'],item['subjects'],item['origin_url'],))
        self.connect.commit()
        return item
    @property
    def sql(self):
        if not self._sql:
            self._sql = """
            insert into article(title,author,avatar,pub_time,content,origin_url,
            read_count,like_count,word_count,subjects) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
            """
            return self._sql
        return self._sql

middlewares.py

from scrapy import signals
import random,time
from selenium import webdriver
from scrapy.http.response.html import HtmlResponse
class JianshuDownloaderMiddleware(object):
    DRIVER_PATH = r"C:\programApps\chromedriver\chromedriver.exe"
    def __init__(self):
        self.driver = webdriver.Chrome(executable_path=self.DRIVER_PATH)
    def process_request(self, request, spider):
        self.driver.get(request.url)
        time.sleep(1)
        check_height = self.driver.execute_script("return document.body.scrollHeight;")
        while True:
            self.driver.execute_script('window.scrollBy(0,1000)')
            time.sleep(1)
            if check_height == self.driver.execute_script("return document.body.scrollHeight;"):
                break
            else:
                check_height = self.driver.execute_script("return document.body.scrollHeight;")
        time.sleep(1)
        source = self.driver.page_source
        response = HtmlResponse(url=self.driver.current_url,body=source,request=request,encoding="utf8")
        #scrapy将不会调用任何其他的`process_request()`方法,将直接返回这个response对象给下载器
        return response

jianshu_spider.py

# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from jianshu.items import JianshuItem
class JianshuSpiderSpider(CrawlSpider):
    name = 'jianshu_spider'
    allowed_domains = ['jianshu.com']
    #爬取简书某个专栏的所有文章
    start_urls = ['https://www.jianshu.com/u/ef4f2422125f?utm_source=desktop&utm_medium=index-users']
    rules = (
        #提取所有文章链接
        Rule(LinkExtractor(allow=r'.*/p/[0-9a-zA-Z]{12}'),callback='parse_aritcle'),
    )
    def parse_aritcle(self, response):
        item = JianshuItem()
        title = response.xpath("//section[@class='ouvJEz']/h1/text()").get().strip()
        info_box = response.xpath('//div[@class="_2mYfmT"]')
        avatar = info_box.xpath('.//a/img/@src').get().strip()     #头像是ajax加载的
        author = info_box.xpath('.//span[@class="FxYr8x"]/a/text()').get().strip()
        pub_time = response.xpath('//section[@class="ouvJEz"]//div[@class="s-dsoj"]/time/text()').get().strip()
        word_count = response.xpath('//section[@class="ouvJEz"]//div[@class="s-dsoj"]/span[2]/text()').get().strip()
        read_count = response.xpath('//section[@class="ouvJEz"]//div[@class="s-dsoj"]/span[3]/text()').get().strip()
        content = response.xpath("//section[1]/article").get()      #保留所有HTML标签内容
        like_count = "".join(response.xpath("//footer//div[@class='-pXE92']/div[2]/span//text()").getall())
        subjects = ",".join(response.xpath("//section//div[@class='_2Nttfz']//a/span/text()").getall())
        origin_url = response.url
        item = JianshuItem(
            title = title,
            content=content,
            author=author,
            avatar = avatar,
            pub_time=pub_time,
            origin_url=origin_url,
            read_count=read_count,
            like_count = like_count,
            word_count = word_count,
            subjects = subjects,
        )
        yield item

方案二:Twisted异步写入数据库

步骤:

  1. 导入adbapi from twisted.enterprise import adbapi
  2. 生成数据库连接池; self.dbpool = adbapi.ConnectionPool(dbapiName='pymysql',**self.DBPARAMS) self.DBPARAMS是数据库连接参数,需要指定cursorclassDictCursor
  3. 执行数据数据库插入操作; defer = self.dbpool.runInteraction(self.insert_item,item) self.insert_item是执行插入操作的函数,item是爬虫yield传递的数据
  4. 打印错误信息,并排错. defer.addErrback(self.handle_error,item,spider)添加错误信息处理函数

其他的地方都不用变,只需要在pipelines.py中写一个异步存储到数据库的类即可

from twisted.enterprise import adbapi
class JianshuTwistedPipeline(object):
    DBPARAMS = {
        'host': '127.0.0.1',
        'port': 3306,
        'user': 'root',
        'password': 'root',
        'database': 'jianshu_spider_data',
        'charset': 'utf8',
        'cursorclass':pymysql.cursors.DictCursor
    }
    def __init__(self):
        self.dbpool = adbapi.ConnectionPool(dbapiName='pymysql',**self.DBPARAMS)
        self._sql = None
    def process_item(self, item, spider):
        defer = self.dbpool.runInteraction(self.insert_item,item)
        defer.addErrback(self.handle_error,item,spider)
    def insert_item(self,cursor,item):
        cursor.execute(self.sql,
                            (item['title'], item['author'], item['avatar'], item['pub_time'], item['content'],
                             item['read_count'], item['like_count'], item['word_count'], item['subjects'],
                             item['origin_url'],))
		#在execute()之后,不需要再进行commit(),连接池内部会进行提交的操作。
    def handle_error(self,error,item,spider):
        print("="*30,"error","="*30)
        print(error)
        print("=" * 30, "error", "=" * 30)
    @property
    def sql(self):
        if not self._sql:
            self._sql = """
            insert into article(title,author,avatar,pub_time,content,origin_url,
            read_count,like_count,word_count,subjects) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
            """
            return self._sql
        return self._sql

项目bug分析:

  • 专栏作者所有文章无法滚动到最底部
  • 无法下载所有文章

八、scrapy源码解析

Scrapy五大功能组件

image.png 我们使用Scrapy可能好奇,为什么我们没有实例化对象 也没有调用方法,就可以指挥下载器去下载spider中start_urls指定的初始url,并返回response 然后解析response之后返回item交给管道处理呢? 其实都是引擎在居中调度,引擎通过接收的数据对象 触发相应的事件

image.png

settings.py配置

我们一般不需要在Scrapy爬虫的请求头里设置cookie,因为打开COOKIES_ENABLED之后请求过程中产生的cookie会自动放到Scrapy的session里面,每次请求都会携带

image.png

Scrapy默认打开的是16个线程,可以自己指定线程数量

image.png

scrapy框架的twisted模块

参考: 使用twisted模拟出Scrapy的基本功能: www.cnblogs.com/ssyfj/p/924… scrapy源码解析前戏,Twisted框架学习笔记: www.jianshu.com/p/dc588fe9a…

scrapy框架内部网络请求(爬虫)和下载内容(pipelines)就是使用twisted异步非租塞模块

1.依赖twisted 内部基于事件循环的机制实现爬虫的并发 非租塞:不等待 发起连接请求,不等待连接再去连接下一个,发送一个之后马上发送下一个 异步:回调 体现就是通知 只要发送成功回来就自动通知 事件循环:循环socket任务,检测socket状态 是否连接成功 是否返回结果 白话:单线程同时可以向多个目标发起http请求 官方:基于事件循环的异步非租塞模块 1.twisted与requests的区别? requests是一个Python实现的可以伪造浏览器发送Http请求的模块 封装socket发送请求 2.twisted是基于事件循环的异步非租塞框架

  • 封装socket发送请求
  • 单线程完成并发操作 不等待直接去发,不管是否成功,只管发
  • 非租塞 不等待
  • 异步 回调
  • 事件循环:一直循环去检查状态