Scrapy框架
- 官网链接:点击
- Scrapy一个开源和协作的框架,其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的,使用它可以以快速、简单、可扩展的方式从网站中提取所需的数据。
- 但目前Scrapy的用途十分广泛,可用于如数据挖掘、监测和自动化测试等领域,也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。
- Scrapy 是基于twisted框架开发而来,twisted是一个流行的事件驱动的python网络框架。因此Scrapy使用了一种非阻塞(又名异步)的代码来实现并发。
一、框架安装和基础使用
1、安装
- linux mac os:
- pip install scrapy
- win:
- pip install wheel
- 下载twisted:https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
- pip install 下载好的框架.whl
- pip install pywin32
- pip install scrapy
2、基础使用
- 创建一个工程:scrapy startproject 工程名称(例如:scrapy startproject qiubaiPro) 在工程目录下创建一个爬虫文件:
- cd 工程 (例如:cd qiubaiPro)
- scrapy genspider 爬虫文件的名称 起始url (例如:scrapy genspider qiubai www.qiushibaike.com)
- scrapy crawl 爬虫名称 :该种执行形式会显示执行的日志信息,可以通过配置文件 LOG_LEVEL='ERROR' 显示指定类型的日志信息 【推荐】
- scrapy crawl 爬虫名称 --nolog:该种执行形式不会显示执行的日志信息
3、日志信息
- 日志等级(种类):
- ERROR:错误
- WARNING:警告
- INFO:一般信息
- DEBUG:调试信息(默认)
- 指定输入某一种日志信息:
- settings.py文件中配置:LOG_LEVEL ='ERROR'
- 将日志信息存储到指定文件中,而并非显示在终端里:
- settings.py文件中配置:LOG_FILE ='log.txt'
4、持久化存储
- 基于终端指令:
- 保证parse方法返回一个可迭代类型的对象(存储解析到的页面内容)
- 使用终端指令完成数据存储到制定磁盘文件中的操作
- scrapy crawl 爬虫文件名称–o 磁盘文件.后缀
- scrapy crawl qiubai -o qiubai.csv --nolog
- scrapy crawl qiubai -o qiubai.json --nolog
- scrapy crawl qiubai -o qiubai.xml --nolog
- 基于管道:
- items:数据结构模板文件,定义数据属性,存储解析到的页面数据
- pipelines:管道文件,接收数据(items),处理持久化存储的相关操作
""" 目录结构: project_name/ scrapy.cfg: project_name/ __init__.py items.py middlewares.py pipelines.py settings.py spiders/ __init__.py qiubai.py scrapy.cfg 项目的主配置信息。(真正爬虫相关的配置信息在settings.py文件中) items.py 设置数据存储模板,用于结构化数据,如:Django的Model pipelines 数据持久化处理 settings.py 配置文件,如:递归的层数、并发数,延迟下载等 spiders 爬虫目录,如:创建文件,编写爬虫解析规则 """ ##########################################spiders/qiubai.py import scrapy class QiubaiSpider(scrapy.Spider): #爬虫文件的名称:就是爬虫源文件的一个唯一标识 name = 'qiubai' #允许的域名:用来限定start_urls列表中哪些url可以进行请求发送 #一般不用直接注释了 #allowed_domains = ['https://www.qiushibaike.com/'] #起始的url列表:该列表中存放的url会被scrapy自动进行请求的发送 start_urls = ['http://www.qiushibaike.com/text/'] # 解析方法:访问起始URL并获取结果后的回调函数 # 该函数的response参数就是向起始的url发送请求后,获取的响应对象 # parse方法的返回值必须为可迭代对象或者NUll # 用作于数据解析:response参数表示的就是请求成功后对应的响应对象 ##############基于终端指令:(有返回值)############## """ 基于终端指令 1.保证parse方法返回一个可迭代类型的对象(存储解析到的页面内容) 2.使用终端指令完成数据存储到制定磁盘文件中的操作 scrapy crawl qiubai -o qiubai.csv --nolog """ # def parse(self, response): # # 建议大家使用xpath进行指定内容的解析(框架集成了xpath解析的接口) # # xpath为response中的方法,可以将xpath表达式直接作用于该函数中 # odiv = response.xpath('//div[@class="col1 old-style-col1"]/div') # content_list = [] # 用于存储解析到的数据 # for div in odiv: # # #xpath返回的是列表,但是列表元素一定是Selector类型的对象 # #extract可以将Selector对象中data参数存储的字符串提取出来 # # author = div.xpath('./div[@class="author clearfix"]/a/h2/text()')[0].extract() # # author = div.xpath('.//h2/text()').extract_first() # # author = div.xpath('.//h2/text()')[0].extract() # # # 列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来 # # author = div.xpath('.//h2/text()').extract() # # author = ''.join(author) # # author = div.xpath('.//h2/text()').extract()[0] # # # 列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来 # # span 下面有br标签得用//text() # content = div.xpath('.//div[@class="content"]/span//text()').extract() # content = ''.join(content) # # # 将解析到的内容封装到字典中 # dic = { # '作者': author, # '内容': content # } # # 将数据存储到content_list这个列表中 # content_list.append(dic) # # return content_list ########基于管道:########## """ 基于管道 对应文件的修改(spiders/qiubai.py,items.py,pipelines.py,settings.py) scrapy crawl qiubai --nolog 代码实现流程: 爬虫文件爬取到数据后,需要将数据存储到items对象中。 使用yield关键字将items对象提交给pipelines管道文件进行持久化处理 在管道文件中的process_item方法中接收爬虫文件提交过来的item对象,然后编写持久化存储的代码将item对象中存储的数据进行持久化存储 settings.py配置文件中开启管道 """ from qiubaiPro.items import QiubaiproItem def parse(self, response): div_list = response.xpath('//div[@id="content-left"]/div') for div in div_list: author = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span/h2/text()').extract_first() content = div.xpath('./a[1]/div/span//text()').extract() content = ''.join(content) item = QiubaiproItem() item['author'] = author item['content'] = content yield item # 将item提交给了管道 ##########################################pipelines.py ########基于管道的文件存储 class QiubaiproPipeline(object): fp = None # # 构造方法 # def __init__(self): # self.fp = None # 定义一个文件描述符属性 # 整个爬虫过程中,该方法只会在开始爬虫的时候被调用一次 def open_spider(self, spider): print('开始爬虫') self.fp = open('./qiubai_pipe.txt', 'w', encoding='utf-8') # 该方法就可以接受爬虫文件中提交过来的item对象,并且对item对象中存储的页面数据进行持久化存储 # 参数:item表示的就是接收到的item对象 # 每当爬虫文件向管道提交一次item,则该方法就会被执行一次 # 因为该方法会被执行调用多次,所以文件的开启和关闭操作写在了另外两个只会各自执行一次的方法中。 def process_item(self, item, spider): # print('process_item 被调用!!!') # 取出item对象中存储的数据值 author = item['author'] content = item['content'] # 持久化存储 self.fp.write(author + ":" + content + '\n\n\n') return item # 就会传递给下一个即将被执行的管道类 # 该方法只会在爬虫结束的时候被调用一次 def close_spider(self, spider): print('爬虫结束') self.fp.close() ########基于mysql的管道存储 import pymysql class QiubaiproPipelineByMysql(object): conn = None cursor = None def open_spider(self,spider): print('开始爬虫') #链接数据库 self.conn = pymysql.Connect(host='127.0.0.1',port=3306,user='root',password='123456',db='qiubai') #编写向数据库中存储数据的相关代码 def process_item(self, item, spider): #1.链接数据库 #2.执行sql语句 sql = 'insert into qiubai values("%s","%s")'%(item['author'],item['content']) self.cursor = self.conn.cursor() try: self.cursor.execute(sql) self.conn.commit() except Exception as e: print(e) self.conn.rollback() #3.提交事务 return item def close_spider(self,spider): print('爬虫结束') self.cursor.close() self.conn.close() ########基于redis的管道存储 import redis class QiubaiproPipelineByRedis(object): conn = None def open_spider(self,spider): print('开始爬虫') self.conn = redis.Redis(host='127.0.0.1',port=6379) def process_item(self, item, spider): dict = { 'author':item['author'], 'content':item['content'] } # 写入redis中 self.conn.lpush('data', dict) return item ##########################################items.py import scrapy class QiubaiproItem(scrapy.Item): # define the fields for your item here like: author = scrapy.Field() content = scrapy.Field() # pass ##########################################settings.py(19行,22行,68行开启管道) USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' # Obey robots.txt rules ROBOTSTXT_OBEY = False LOG_LEVEL='ERROR' #任意位置 显示指定类型的日志信息 #下列结构为字典,字典中的键值表示的是即将被启用执行的管道文件和其执行的优先级。 #字典中的两组键值分别表示会执行管道文件中对应的两个管道类中的process_item方法,实现两种不同形式的持久化操作。 ITEM_PIPELINES = { 'qiubaiPro.pipelines.QiubaiproPipeline': 300, # 300表示为优先级,值越小优先级越高 #'qiubaiPro.pipelines.QiubaiproPipelineByMysql': 200, #200表示为优先级,值越小优先级越高 'qiubaiPro.pipelines.QiubaiproPipelineByRedis': 100, #100表示为优先级,值越小优先级越高 } """ 不同形式的持久化操作 主要就是管道文件pipelines.py和配置文件settings.py -需要在管道文件中编写对应平台的管道类 -在配置文件中对自定义的管道类进行生效操作 """
""" 多个url页面数据爬取 1.对应文件的修改(spiders/qiubai.py,items.py,pipelines.py,settings.py) 2.使用终端指令完成数据存储到制定磁盘文件中的操作 scrapy crawl qiubai --nolog """ # -*- coding: utf-8 -*- import scrapy from qiubaiByPages.items import QiubaibypagesItem class QiubaiSpider(scrapy.Spider): name = 'qiubai' #allowed_domains = ['www.qiushibaike.com/text'] start_urls = ['https://www.qiushibaike.com/text/'] #设计一个通用的url模板 url = 'https://www.qiushibaike.com/text/page/%d/' pageNum = 1 def parse(self, response): div_list = response.xpath('//*[@id="content-left"]/div') for div in div_list: author = div.xpath('./div[@class="author clearfix"]/a[2]/h2/text()').extract_first() content = div.xpath('.//div[@class="content"]/span/text()').extract_first() #创建一个items对象,将解析到数据值存储到items对象中 item = QiubaibypagesItem() item['author'] = author item['content'] = content #将item提交给管道 yield item #请求的手动发送 #13表示的是最后一页的页码 if self.pageNum <= 13: print('爬取到了第%d页的页面数据'%self.pageNum) self.pageNum += 1 new_url = format(self.url % self.pageNum) #callback:将请求获取到的页面数据进行解析 yield scrapy.Request(url=new_url,callback=self.parse) """ items.py """ # -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html import scrapy class QiubaibypagesItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() author = scrapy.Field() content = 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://doc.scrapy.org/en/latest/topics/item-pipeline.html class QiubaibypagesPipeline(object): fp = None def open_spider(self,spider): print('开始爬虫') self.fp = open('./qiubai.txt','w',encoding='utf-8') def process_item(self, item, spider): self.fp.write(item['author']+":"+item['content']) return item def close_spider(self,spider): self.fp.close() print('爬虫结束') """ settings.py 19行 22行 68行 开启管道 """ # Crawl responsibly by identifying yourself (and your website) on the user-agent #USER_AGENT = 'qiubaiPro (+http://www.yourdomain.com)' USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' # Obey robots.txt rules ROBOTSTXT_OBEY = False ITEM_PIPELINES = { 'qiubaiByPages.pipelines.QiubaibypagesPipeline': 300, }
""" - 面试题:如果最终需要将爬取到的数据值一份存储到磁盘文件,一份存储到数据库中,则应该如何操作scrapy? - 答:管道文件中的代码为 #该类为管道类,该类中的process_item方法是用来实现持久化存储操作的。 class DoublekillPipeline(object): def process_item(self, item, spider): #持久化操作代码 (方式1:写入磁盘文件) return item #如果想实现另一种形式的持久化操作,则可以再定制一个管道类: class DoublekillPipeline_db(object): def process_item(self, item, spider): #持久化操作代码 (方式1:写入数据库) return item 在settings.py开启管道操作代码为: #下列结构为字典,字典中的键值表示的是即将被启用执行的管道文件和其执行的优先级。 ITEM_PIPELINES = { 'doublekill.pipelines.DoublekillPipeline': 300, 'doublekill.pipelines.DoublekillPipeline_db': 200, } #上述代码中,字典中的两组键值分别表示会执行管道文件中对应的两个管道类中的process_item方法,实现两种不同形式的持久化操作。 """
import sys,os sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')
scrapy框架 - 什么是框架? - 就是一个集成了很多功能并且具有很强通用性的一个项目模板。 - 如何学习框架? - 专门学习框架封装的各种功能的详细用法。 - 什么是scrapy? - 爬虫中封装好的一个明星框架。功能:高性能的持久化存储,异步的数据下载,高性能的数据解析,分布式 - scrapy框架的基本使用 - 环境的安装: - mac or linux:pip install scrapy - windows: - pip install wheel - 下载twisted,下载地址为http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted - 安装twisted:pip install Twisted‑17.1.0‑cp36‑cp36m‑win_amd64.whl - pip install pywin32 - pip install scrapy 测试:在终端里录入scrapy指令,没有报错即表示安装成功! - 创建一个工程:scrapy startproject xxxPro - cd xxxPro - 在spiders子目录中创建一个爬虫文件 - scrapy genspider spiderName www.xxx.com - 执行工程: - scrapy crawl spiderName - scrapy数据解析 - scrapy持久化存储 - 基于终端指令: - 要求:只可以将parse方法的返回值存储到本地的文本文件中 - 注意:持久化存储对应的文本文件的类型只可以为:'json', 'jsonlines', 'jl', 'csv', 'xml', 'marshal', 'pickle - 指令:scrapy crawl xxx -o filePath - 好处:简介高效便捷 - 缺点:局限性比较强(数据只可以存储到指定后缀的文本文件中) - 基于管道: - 编码流程: - 数据解析 - 在item类中定义相关的属性 - 将解析的数据封装存储到item类型的对象 - 将item类型的对象提交给管道进行持久化存储的操作 - 在管道类的process_item中要将其接受到的item对象中存储的数据进行持久化存储操作 - 在配置文件中开启管道 - 好处: - 通用性强。 - 面试题:将爬取到的数据一份存储到本地一份存储到数据库,如何实现? - 管道文件中一个管道类对应的是将数据存储到一种平台 - 爬虫文件提交的item只会给管道文件中第一个被执行的管道类接受 - process_item中的return item表示将item传递给下一个即将被执行的管道类 - 基于Spider的全站数据爬取 - 就是将网站中某板块下的全部页码对应的页面数据进行爬取 - 需求:爬取校花网中的照片的名称 - 实现方式: - 将所有页面的url添加到start_urls列表(不推荐) - 自行手动进行请求发送(推荐) - 手动请求发送: - yield scrapy.Request(url,callback):callback专门用做于数据解析 - 五大核心组件 引擎(Scrapy) 用来处理整个系统的数据流处理, 触发事务(框架核心) 调度器(Scheduler) 用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址 下载器(Downloader) 用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的) 爬虫(Spiders) 爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面 项目管道(Pipeline) 负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。 - 请求传参 - 使用场景:如果爬取解析的数据不在同一张页面中。(深度爬取) - 需求:爬取boss的岗位名称,岗位描述 - 图片数据爬取之ImagesPipeline - 基于scrapy爬取字符串类型的数据和爬取图片类型的数据区别? - 字符串:只需要基于xpath进行解析且提交管道进行持久化存储 - 图片:xpath解析出图片src的属性值。单独的对图片地址发起请求获取图片二进制类型的数据 - ImagesPipeline: - 只需要将img的src的属性值进行解析,提交到管道,管道就会对图片的src进行请求发送获取图片的二进制类型的数据,且还会帮我们进行持久化存储。 - 需求:爬取站长素材中的高清图片 - 使用流程: - 数据解析(图片的地址) - 将存储图片地址的item提交到制定的管道类 - 在管道文件中自定制一个基于ImagesPipeLine的一个管道类 - get_media_request - file_path - item_completed - 在配置文件中: - 指定图片存储的目录:IMAGES_STORE = './imgs_bobo' - 指定开启的管道:自定制的管道类 - 中间件 - 下载中间件 - 位置:引擎和下载器之间 - 作用:批量拦截到整个工程中所有的请求和响应 - 拦截请求: - UA伪装:process_request - 代理IP:process_exception:return request - 拦截响应: - 篡改响应数据,响应对象 - 需求:爬取网易新闻中的新闻数据(标题和内容) - 1.通过网易新闻的首页解析出五大板块对应的详情页的url(没有动态加载) - 2.每一个板块对应的新闻标题都是动态加载出来的(动态加载) - 3.通过解析出每一条新闻详情页的url获取详情页的页面源码,解析出新闻内容 - CrawlSpider:类,Spider的一个子类 - 全站数据爬取的方式 - 基于Spider:手动请求 - 基于CrawlSpider - CrawlSpider的使用: - 创建一个工程 - cd XXX - 创建爬虫文件(CrawlSpider): - scrapy genspider -t crawl xxx www.xxxx.com - 链接提取器: - 作用:根据指定的规则(allow)进行指定链接的提取 - 规则解析器: - 作用:将链接提取器提取到的链接进行指定规则(callback)的解析 #需求:爬取sun网站中的编号,新闻标题,新闻内容,标号 - 分析:爬取的数据没有在同一张页面中。 - 1.可以使用链接提取器提取所有的页码链接 - 2.让链接提取器提取所有的新闻详情页的链接 - 分布式爬虫 - 概念:我们需要搭建一个分布式的机群,让其对一组资源进行分布联合爬取。 - 作用:提升爬取数据的效率 - 如何实现分布式? - 安装一个scrapy-redis的组件 - 原生的scarapy是不可以实现分布式爬虫,必须要让scrapy结合着scrapy-redis组件一起实现分布式爬虫。 - 为什么原生的scrapy不可以实现分布式? - 调度器不可以被分布式机群共享 - 管道不可以被分布式机群共享 - scrapy-redis组件作用: - 可以给原生的scrapy框架提供可以被共享的管道和调度器 - 实现流程 - 创建一个工程 - 创建一个基于CrawlSpider的爬虫文件 - 修改当前的爬虫文件: - 导包:from scrapy_redis.spiders import RedisCrawlSpider - 将start_urls和allowed_domains进行注释 - 添加一个新属性:redis_key = 'sun' 可以被共享的调度器队列的名称 - 编写数据解析相关的操作 - 将当前爬虫类的父类修改成RedisCrawlSpider - 修改配置文件settings - 指定使用可以被共享的管道: ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 400 } - 指定调度器: # 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用scrapy-redis组件自己的调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据 SCHEDULER_PERSIST = True - 指定redis服务器: - redis相关操作配置: - 配置redis的配置文件: - linux或者mac:redis.conf - windows:redis.windows.conf - 代开配置文件修改: - 将bind 127.0.0.1进行删除 - 关闭保护模式:protected-mode yes改为no - 结合着配置文件开启redis服务 - redis-server 配置文件 - 启动客户端: - redis-cli - 执行工程: - scrapy runspider xxx.py - 向调度器的队列中放入一个起始的url: - 调度器的队列在redis的客户端中 - lpush xxx www.xxx.com - 爬取到的数据存储在了redis的proName:items这个数据结构中
二、post请求和请求传参meta
1、post请求发送
- 问题:在之前代码中,我们从来没有手动的对start_urls列表中存储的起始url进行过请求的发送,但是起始url的确是进行了请求的发送,那这是如何实现的呢?
- 解答:其实是因为爬虫文件中的爬虫类继承到了Spider父类中的start_requests(self)这个方法,该方法就可以对start_urls列表中的url发起请求
- 【注意】start_requests方法默认的实现,是对起始的url发起get请求,如果想发起post请求,则需要子类重写该方法。
2、请求传参
- 在某些情况下,我们爬取的数据不在同一个页面中,例如,我们爬取一个电影网站,电影的名称,评分在一级页面,而要爬取的其他电影详情在其二级子页面中。这时我们就需要用到请求传参。
""" - 问题:在之前代码中,我们从来没有手动的对start_urls列表中存储的起始url进行过请求的发送,但是起始url的确是进行了请求的发送,那这是如何实现的呢? - 解答:其实是因为爬虫文件中的爬虫类继承到了Spider父类中的start_requests(self)这个方法,该方法就可以对start_urls列表中的url发起请求: def start_requests(self): for u in self.start_urls: yield scrapy.Request(url=u,callback=self.parse) 【注意】该方法默认的实现,是对起始的url发起get请求,如果想发起post请求,则需要子类重写该方法。 -方法: 重写start_requests方法,让其发起post请求: def start_requests(self): #请求的url post_url = 'http://fanyi.baidu.com/sug' # post请求参数 formdata = { 'kw': 'wolf', } # 发送post请求 yield scrapy.FormRequest(url=post_url, formdata=formdata, callback=self.parse) """ """ post请求发送 """ import scrapy #需求:百度翻译中指定词条对应的翻译结果进行获取 class PostdemoSpider(scrapy.Spider): name = 'postDemo' #allowed_domains = ['www.baidu.com'] start_urls = ['https://fanyi.baidu.com/sug'] #该方法其实是父类中的一个方法:该方法可以对star_urls列表中的元素进行get请求的发送 #发起post: #1.将Request方法中method参数赋值成post #2.FormRequest()可以发起post请求(推荐) def start_requests(self): print('start_requests()') #post请求的参数 data = { 'kw': 'dog', } for url in self.start_urls: #formdata:请求参数对应的字典 yield scrapy.FormRequest(url=url,formdata=data,callback=self.parse) #发起get: # def start_requests(self): # for u in self.start_urls: # yield scrapy.Request(url=url, callback=self.parse) def parse(self, response): print(response.text) """ 1.递归爬取解析多页页面数据 - 需求:将糗事百科所有页码的作者和段子内容数据进行爬取切持久化存储 - 需求分析:每一个页面对应一个url,则scrapy工程需要对每一个页码对应的url依次发起请求,然后通过对应的解析方法进行作者和段子内容的解析。 实现方案: 1.将每一个页码对应的url存放到爬虫文件的起始url列表(start_urls)中。(不推荐) 2.使用Request方法手动发起请求。(推荐) """ import scrapy from moviePro.items import MovieproItem class MovieSpider(scrapy.Spider): name = 'movie' #allowed_domains = ['www.id97.com'] start_urls = ['http://www.id97.com/movie'] #专门用于解析二级子页面中的数据值 def parseBySecondPage(self,response): actor = response.xpath('/html/body/div[1]/div/div/div[1]/div[1]/div[2]/table/tbody/tr[1]/td[2]/a/text()').extract_first() language = response.xpath('/html/body/div[1]/div/div/div[1]/div[1]/div[2]/table/tbody/tr[6]/td[2]/text()').extract_first() longTime = response.xpath('/html/body/div[1]/div/div/div[1]/div[1]/div[2]/table/tbody/tr[8]/td[2]/text()').extract_first() #取出Request方法的meta参数传递过来的字典(response.meta) item = response.meta['item'] item['actor'] = actor item['language'] = language item['longTime'] = longTime #将item提交给管道 yield item def parse(self, response): #名称,类型,导演,语言,片长 div_list = response.xpath('/html/body/div[1]/div[1]/div[2]/div') for div in div_list: name = div.xpath('.//div[@class="meta"]/h1/a/text()').extract_first() #如下xpath方法返回的是一个列表,切列表元素为4 kind = div.xpath('.//div[@class="otherinfo"]//text()').extract() #将kind列表转化成字符串 kind = "".join(kind) url = div.xpath('.//div[@class="meta"]/h1/a/@href').extract_first() print(kind) #创建items对象 item = MovieproItem() item['name'] = name item['kind'] = kind #问题:如何将剩下的电影详情数据存储到item对象(meta) #需要对url发起请求,获取页面数据,进行指定数据解析 #meta参数只可以赋值一个字典(将item对象先封装到字典) yield scrapy.Request(url=url,callback=self.parseBySecondPage,meta={'item':item}) import scrapy from bossPro.items import BossproItem class BossSpider(scrapy.Spider): name = 'boss' # allowed_domains = ['www.xxx.com'] start_urls = ['https://www.zhipin.com/job_detail/?query=python&city=101010100&industry=&position='] url = 'https://www.zhipin.com/c101010100/?query=python&page=%d' page_num = 2 #回调函数接受item def parse_detail(self,response): item = response.meta['item'] job_desc = response.xpath('//*[@id="main"]/div[3]/div/div[2]/div[2]/div[1]/div//text()').extract() job_desc = ''.join(job_desc) # print(job_desc) item['job_desc'] = job_desc yield item #解析首页中的岗位名称 def parse(self, response): li_list = response.xpath('//*[@id="main"]/div/div[3]/ul/li') for li in li_list: item = BossproItem() job_name = li.xpath('.//div[@class="info-primary"]/h3/a/div[1]/text()').extract_first() item['job_name'] = job_name detail_url = 'https://www.zhipin.com'+li.xpath('.//div[@class="info-primary"]/h3/a/@href').extract_first() #对详情页发请求获取详情页的页面源码数据 #手动请求的发送 #请求传参:meta={},可以将meta字典传递给请求对应的回调函数 yield scrapy.Request(detail_url,callback=self.parse_detail,meta={'item':item}) #分页操作 if self.page_num <= 3: new_url = format(self.url%self.page_num) self.page_num += 1 yield scrapy.Request(new_url,callback=self.parse)
3、ImagesPipeline
""" - 图片数据爬取之ImagesPipeline - 基于scrapy爬取字符串类型的数据和爬取图片类型的数据区别? - 字符串:只需要基于xpath进行解析且提交管道进行持久化存储 - 图片:xpath解析出图片src的属性值。单独的对图片地址发起请求获取图片二进制类型的数据 - ImagesPipeline: - 只需要将img的src的属性值进行解析,提交到管道,管道就会对图片的src进行请求发送获取图片的二进制类型的数据,且还会帮我们进行持久化存储。 - 需求:爬取站长素材中的高清图片 - 使用流程: - 数据解析(图片的地址) - 将存储图片地址的item提交到制定的管道类 - 在管道文件中自定制一个基于ImagesPipeLine的一个管道类 - get_media_request - file_path - item_completed - 在配置文件中: - 指定图片存储的目录:IMAGES_STORE = './imgs_bobo' - 指定开启的管道:自定制的管道类 """ # -*- coding: utf-8 -*- import scrapy from imgsPro.items import ImgsproItem class ImgSpider(scrapy.Spider): name = 'img' # allowed_domains = ['www.xxx.com'] start_urls = ['http://sc.chinaz.com/tupian/'] def parse(self, response): div_list = response.xpath('//div[@id="container"]/div') for div in div_list: #注意:使用伪属性 src = div.xpath('./div/a/img/@src2').extract_first() item = ImgsproItem() item['src'] = src yield item """ items.py """ # -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html import scrapy class ImgsproItem(scrapy.Item): # define the fields for your item here like: src = 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://doc.scrapy.org/en/latest/topics/item-pipeline.html # class ImgsproPipeline(object): # def process_item(self, item, spider): # return item from scrapy.pipelines.images import ImagesPipeline import scrapy class imgsPileLine(ImagesPipeline): #就是可以根据图片地址进行图片数据的请求 def get_media_requests(self, item, info): yield scrapy.Request(item['src']) #指定图片存储的路径 def file_path(self, request, response=None, info=None): imgName = request.url.split('/')[-1] return imgName def item_completed(self, results, item, info): return item #返回给下一个即将被执行的管道类 """ settings.py """ # Crawl responsibly by identifying yourself (and your website) on the user-agent #USER_AGENT = 'qiubaiPro (+http://www.yourdomain.com)' USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' # Obey robots.txt rules ROBOTSTXT_OBEY = False LOG_LEVEL = 'ERROR' #下列结构为字典,字典中的键值表示的是即将被启用执行的管道文件和其执行的优先级。 ITEM_PIPELINES = { 'imgsPro.pipelines.imgsPileLine': 300, } #指定图片存储的目录 IMAGES_STORE = './imgs'
4、Scrapy核心组件
- 引擎(Scrapy):
- 用来处理整个系统的数据流处理, 触发事务(框架核心)
- 调度器(Scheduler):
- 用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
- 下载器(Downloader):
- 用于下载网页内容,并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
- 爬虫(Spiders):
- 爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
- 项目管道(Pipeline):
- 负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
5、Scrapy运行流程大概如下:
- 引擎从调度器中取出一个链接(URL)用于接下来的抓取
- 引擎把URL封装成一个请求(Request)传给下载器
- 下载器把资源下载下来,并封装成应答包(Response)
- 爬虫解析Response
- 解析出实体(Item),则交给实体管道进行进一步的处理
- 解析出的是链接(URL),则把URL交给调度器等待抓取
6、如何提高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
#爬虫文件 import scrapy from xiaohua.items import XiaohuaItem class XiahuaSpider(scrapy.Spider): name = 'xiaohua' allowed_domains = ['www.521609.com'] start_urls = ['http://www.521609.com/daxuemeinv/'] pageNum = 1 url = 'http://www.521609.com/daxuemeinv/list8%d.html' def parse(self, response): li_list = response.xpath('//div[@class="index_img list_center"]/ul/li') for li in li_list: school = li.xpath('./a/img/@alt').extract_first() img_url = li.xpath('./a/img/@src').extract_first() item = XiaohuaItem() item['school'] = school item['img_url'] = 'http://www.521609.com' + img_url yield item if self.pageNum < 10: self.pageNum += 1 url = format(self.url % self.pageNum) #print(url) yield scrapy.Request(url=url,callback=self.parse) #item文件 import scrapy class XiaohuaItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() school=scrapy.Field() img_url=scrapy.Field() #管道文件 import json import os import urllib.request class XiaohuaPipeline(object): def __init__(self): self.fp = None def open_spider(self,spider): print('开始爬虫') self.fp = open('./xiaohua.txt','w') def download_img(self,item): url = item['img_url'] fileName = item['school']+'.jpg' if not os.path.exists('./xiaohualib'): os.mkdir('./xiaohualib') filepath = os.path.join('./xiaohualib',fileName) urllib.request.urlretrieve(url,filepath) print(fileName+"下载成功") def process_item(self, item, spider): obj = dict(item) json_str = json.dumps(obj,ensure_ascii=False) self.fp.write(json_str+'\n') #下载图片 self.download_img(item) return item def close_spider(self,spider): print('结束爬虫') self.fp.close() #配置文件 USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' # Obey robots.txt rules ROBOTSTXT_OBEY = False # Configure maximum concurrent requests performed by Scrapy (default: 16) CONCURRENT_REQUESTS = 100 #增加并发 COOKIES_ENABLED = False #禁止cookie LOG_LEVEL = 'ERROR' #降低日志级别 RETRY_ENABLED = False #禁止重试 DOWNLOAD_TIMEOUT = 3 #减少下载超时 # Configure a delay for requests for the same website (default: 0) # See https://doc.scrapy.org/en/latest/topics/settings.html#download-delay # See also autothrottle settings and docs # The download delay setting will honor only one of: #CONCURRENT_REQUESTS_PER_DOMAIN = 16 #CONCURRENT_REQUESTS_PER_IP = 16 DOWNLOAD_DELAY = 3
三、中间件和cookie
1、UA池:User-Agent池
- 作用:尽可能多的将scrapy工程中的请求伪装成不同类型的浏览器身份。
- # 在中间件类中进行导包 from scrapy.contrib.downloadermiddleware.useragent import UserAgentMiddleware
- # 封装一个基于UserAgentMiddleware的类,且重写该类的process_requests方法
- 操作流程:
- 在下载中间件中拦截请求
- 将拦截到的请求的请求头信息中的UA进行篡改伪装
- 在配置文件中开启下载中间件
2、代理池
- 作用:尽可能多的将scrapy工程中的请求的IP设置成不同的。
- 操作流程:
- 在下载中间件中拦截请求
- 将拦截到的请求的IP修改成某一代理IP
- 在配置文件中开启下载中间件
3、下载中间件(Downloader Middlewares)
- 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件。
- 作用:引擎将请求传递给下载器过程中, 下载中间件可以对请求进行一系列处理。比如设置请求的 User-Agent,设置代理等
- 作用:在下载器完成将Response传递给引擎中,下载中间件可以对响应进行一系列处理。比如进行gzip解压等。
- 我们主要使用下载中间件处理请求,一般会对请求设置随机的User-Agent ,设置随机的代理。目的在于防止爬取网站的反爬虫策略。
""" - 中间件 -配置文件中进行下载中间件的开启。 - 下载中间件 - 位置:引擎和下载器之间 - 作用:批量拦截到整个工程中所有的请求和响应 - 拦截请求: - UA伪装:process_request - 代理IP:process_exception:return request - 拦截响应: - 篡改响应数据,响应对象 - 需求:爬取网易新闻中的新闻数据(标题和内容) - 1.通过网易新闻的首页解析出五大板块对应的详情页的url(没有动态加载) - 2.每一个板块对应的新闻标题都是动态加载出来的(动态加载) - 3.通过解析出每一条新闻详情页的url获取详情页的页面源码,解析出新闻内容 """ # -*- coding: utf-8 -*- import scrapy class MiddleSpider(scrapy.Spider): #爬取百度 name = 'middle' # allowed_domains = ['www.xxxx.com'] start_urls = ['http://www.baidu.com/s?wd=ip'] def parse(self, response): page_text = response.text with open('./ip.html','w',encoding='utf-8') as fp: fp.write(page_text) """ middlewares.py """ from scrapy import signals import random class MiddleproDownloaderMiddleware(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. user_agent_list = [ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 " "(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1", "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 " "(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 " "(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 " "(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 " "(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 " "(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 " "(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 " "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 " "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24" ] PROXY_http = [ '153.180.102.104:80', '195.208.131.189:56055', ] PROXY_https = [ '120.83.49.90:9000', '95.189.112.214:35508', ] #拦截请求 def process_request(self, request, spider): #UA伪装 request.headers['User-Agent'] = random.choice(self.user_agent_list) #为了验证代理的操作是否生效 # request.meta['proxy'] = 'http://183.146.213.198:80' 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): if request.url.split(':')[0] == 'http': #代理 request.meta['proxy'] = 'http://'+random.choice(self.PROXY_http) else: request.meta['proxy'] = 'https://' + random.choice(self.PROXY_https) return request #将修正之后的请求对象进行重新的请求发送 """ settings.py 19行 22行 56行 开启下载中间件 """ # Crawl responsibly by identifying yourself (and your website) on the user-agent #USER_AGENT = 'qiubaiPro (+http://www.yourdomain.com)' USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' LOG_LEVEL = 'ERROR' # Obey robots.txt rules ROBOTSTXT_OBEY = False DOWNLOADER_MIDDLEWARES = { 'middlePro.middlewares.MiddleproDownloaderMiddleware': 543, }
4、selenium在scrapy中的使用流程
- a):重写爬虫文件的构造方法,在该方法中使用selenium实例化一个浏览器对象(因为浏览器对象只需要被实例化一次)
- b):重写爬虫文件的closed(self,spider)方法,在其内部关闭浏览器对象。该方法是在爬虫结束时被调用
- c):重写下载中间件的process_response方法,让该方法对响应对象进行拦截,并篡改response中存储的页面数据
- d):在配置文件中开启下载中间件
- 原理分析: 当引擎将url对应的请求提交给下载器后,下载器进行网页数据的下载, 然后将下载到的页面数据,封装到response中,提交给引擎,引擎将response在转交给Spiders。 Spiders接受到的response对象中存储的页面数据里是没有动态加载的新闻数据的。 要想获取动态加载的新闻数据,则需要在下载中间件中对下载器提交给引擎的response响应对象进行拦截, 切对其内部存储的页面数据进行篡改,修改成携带了动态加载出的新闻数据, 然后将被篡改的response对象最终交给Spiders进行解析操作。
""" selenium在scrapy中的应用 from scrapy.http import HtmlResponse #参数介绍: #拦截到响应对象(下载器传递给Spider的响应对象) #request:响应对象对应的请求对象 #response:拦截到的响应对象 #spider:爬虫文件中对应的爬虫类的实例 def process_response(self, request, response, spider): #响应对象中存储页面数据的篡改 if request.url in['http://news.163.com/domestic/','http://news.163.com/world/','http://news.163.com/air/','http://war.163.com/']: spider.bro.get(url=request.url) js = 'window.scrollTo(0,document.body.scrollHeight)' spider.bro.execute_script(js) time.sleep(2) #一定要给与浏览器一定的缓冲加载数据的时间 #页面数据就是包含了动态加载出来的新闻数据对应的页面数据 page_text = spider.bro.page_source #篡改响应对象 return HtmlResponse(url=spider.bro.current_url,body=page_text,encoding='utf-8',request=request) else: return response """ # -*- coding: utf-8 -*- import scrapy from selenium import webdriver from wangyiPro.items import WangyiproItem class WangyiSpider(scrapy.Spider): name = 'wangyi' # allowed_domains = ['www.cccom'] start_urls = ['https://news.163.com/'] models_urls = [] #存储五个板块对应详情页的url #解析五大板块对应详情页的url #实例化一个浏览器对象(实例化一次) def __init__(self): self.bro = webdriver.Chrome(executable_path='/Users/chromedriver') def parse(self, response): li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li') alist = [3,4,6,7,8] for index in alist: model_url = li_list[index].xpath('./a/@href').extract_first() self.models_urls.append(model_url) #依次对每一个板块对应的页面进行请求 for url in self.models_urls:#对每一个板块的url进行请求发送 yield scrapy.Request(url,callback=self.parse_model) #每一个板块对应的新闻标题相关的内容都是动态加载 def parse_model(self,response): #解析每一个板块页面中对应新闻的标题和新闻详情页的url # response.xpath() 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() new_detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first() item = WangyiproItem() item['title'] = title #对新闻详情页的url发起请求 yield scrapy.Request(url=new_detail_url,callback=self.parse_detail,meta={'item':item}) def parse_detail(self,response):#解析新闻内容 content = response.xpath('//*[@id="endText"]//text()').extract() content = ''.join(content) item = response.meta['item'] item['content'] = content yield item # 必须在整个爬虫结束后,关闭浏览器 def closed(self,spider): self.bro.quit() """ items.py """ import scrapy class WangyiproItem(scrapy.Item): # define the fields for your item here like: title = scrapy.Field() content = scrapy.Field() """ pipelines.py """ class WangyiproPipeline(object): def process_item(self, item, spider): print(item) return item """ middlewares.py """ # -*- coding: utf-8 -*- # Define here the models for your spider middleware # # See documentation in: # https://doc.scrapy.org/en/latest/topics/spider-middleware.html from scrapy import signals from scrapy.http import HtmlResponse from time import sleep class WangyiproDownloaderMiddleware(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. def process_request(self, request, spider): return None #该方法拦截五大板块对应的响应对象,进行篡改 def process_response(self, request, response, spider):#spider爬虫对象 bro = spider.bro#获取了在爬虫类中定义的浏览器对象 #挑选出指定的响应对象进行篡改 #通过url指定request #通过request指定response if request.url in spider.models_urls: bro.get(request.url) #五个板块对应的url进行请求 sleep(3) page_text = bro.page_source #包含了动态加载的新闻数据 #response #五大板块对应的响应对象 #针对定位到的这些response进行篡改 #实例化一个新的响应对象(符合需求:包含动态加载出的新闻数据),替代原来旧的响应对象 #如何获取动态加载出的新闻数据? #基于selenium便捷的获取动态加载数据 new_response = HtmlResponse(url=request.url,body=page_text,encoding='utf-8',request=request) return new_response else: #response #其他请求对应的响应对象 return response """ settings.py """ ITEM_PIPELINES = { 'wangyiPro.pipelines.WangyiproPipeline': 300, } LOG_LEVEL = 'ERROR' ROBOTSTXT_OBEY = False DOWNLOADER_MIDDLEWARES = { 'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543, }
5、cookie
- 登录后,获取该用户个人主页这个二级页面的页面数据
""" cookie:豆瓣网个人登录,获取该用户个人主页这个二级页面的页面数据。 """ # -*- coding: utf-8 -*- import scrapy class DoubanSpider(scrapy.Spider): name = 'douban' #allowed_domains = ['www.douban.com'] start_urls = ['https://www.douban.com/accounts/login'] #重写start_requests方法 def start_requests(self): #将请求参数封装到字典 data = { 'source': 'index_nav', 'form_email': '15027900535', 'form_password': 'bobo@15027900535' } for url in self.start_urls: yield scrapy.FormRequest(url=url,formdata=data,callback=self.parse) #针对个人主页页面数据进行解析操作 def parseBySecondPage(self,response): fp = open('second.html', 'w', encoding='utf-8') fp.write(response.text) #可以对当前用户的个人主页页面数据进行指定解析操作 def parse(self, response): #登录成功后的页面数据进行存储 fp = open('main.html','w',encoding='utf-8') fp.write(response.text) #获取当前用户的个人主页 url = 'https://www.douban.com/people/185687620/' yield scrapy.Request(url=url,callback=self.parseBySecondPage)
四、CrawlSpider
- CrawlSpider其实是Spider的一个子类,除了继承到Spider的特性和功能外,还派生除了其自己独有的更加强大的特性和功能。其中最显著的功能就是”LinkExtractors链接提取器“。Spider是所有爬虫的基类,其设计原则只是为了爬取start_url列表中网页,而从爬取到的网页中提取出的url进行继续的爬取工作使用CrawlSpider更合适。
- 爬取全站方法1:基于Scrapy框架中的Spider的递归爬取进行实现(Request模块递归回调parse方法)【手动请求的发送】。
- 爬取全站方法2:基于CrawlSpider的自动爬取进行实现(更加简洁和高效)。【推荐】
1、使用
- 创建scrapy工程:scrapy startproject projectName
- 创建爬虫文件:scrapy genspider -t crawl spiderName www.xxx.com (scrapy genspider -t crawl 爬虫名称 起始url)
- 此指令对比以前的指令多了 "-t crawl",表示创建的爬虫文件是基于CrawlSpider这个类的,而不再是Spider这个基类。
-
执行爬虫程序: scrapy crawl 爬虫名称 :该种执行形式会显示执行的日志信息 scrapy crawl 爬虫名称 --nolog:该种执行形式不会显示执行的日志信息
2、CrawlSpider整体爬取流程
- a):爬虫文件首先根据起始url,获取该url的网页内容
- b):链接提取器会根据指定提取规则将步骤a中网页内容中的链接进行提取
- c):规则解析器会根据指定解析规则将链接提取器中提取到的链接中的网页内容根据指定的规则进行解析
- d):将解析数据封装到item中,然后提交给管道进行持久化存储
- LinkExtractor:链接提取器。- 作用:提取response中符合规则的链接。
- Rule : 规则解析器。根据链接提取器中提取到的链接,根据指定规则提取解析器链接网页中的内容。
""" - CrawlSpider:类,Spider的一个子类 - 全站数据爬取的方式 - 基于Spider:手动请求 - 基于CrawlSpider - CrawlSpider的使用: - 创建一个工程 - cd XXX - 创建爬虫文件(CrawlSpider): - scrapy genspider -t crawl xxx www.xxxx.com - 链接提取器: - 作用:根据指定的规则(allow)进行指定链接的提取 - 规则解析器: - 作用:将链接提取器提取到的链接进行指定规则(callback)的解析 #需求:爬取sun网站中的编号,新闻标题,新闻内容,标号 - 分析:爬取的数据没有在同一张页面中。 - 1.可以使用链接提取器提取所有的页码链接 - 2.让链接提取器提取所有的新闻详情页的链接 """ # -*- coding: utf-8 -*- import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from sunPro.items import SunproItem,DetailItem #需求:爬取sun网站中的编号,新闻标题,新闻内容,标号 class SunSpider(CrawlSpider): name = 'sun' # allowed_domains = ['www.xxx.com'] start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page='] #链接提取器:根据指定规则(allow="正则")进行指定链接的提取 link = LinkExtractor(allow=r'type=4&page=\d+') link_detail = LinkExtractor(allow=r'question/\d+/\d+\.shtml') rules = ( #规则解析器:将链接提取器提取到的链接进行指定规则(callback)的解析操作 Rule(link, callback='parse_item', follow=True), #follow=True:可以将链接提取器 继续作用到 连接提取器提取到的链接 所对应的页面中 Rule(link_detail,callback='parse_detail') ) #http://wz.sun0769.com/html/question/201907/421001.shtml #http://wz.sun0769.com/html/question/201907/420987.shtml #解析新闻编号和新闻的标题 #如下两个解析方法中是不可以实现请求传参! #无法将两个解析方法解析的数据存储到同一个item中,可以以此存储到两个item def parse_item(self, response): #注意:xpath表达式中不可以出现tbody标签 tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr') for tr in tr_list: new_num = tr.xpath('./td[1]/text()').extract_first() new_title = tr.xpath('./td[2]/a[2]/@title').extract_first() item = SunproItem() item['title'] = new_title item['new_num'] = new_num yield item #解析新闻内容和新闻编号 def parse_detail(self,response): new_id = response.xpath('/html/body/div[9]/table[1]//tr/td[2]/span[2]/text()').extract_first() new_content = response.xpath('/html/body/div[9]/table[2]//tr[1]//text()').extract() new_content = ''.join(new_content) # print(new_id,new_content) item = DetailItem() item['content'] = new_content item['new_id'] = new_id yield item """ items.py """ # -*- coding: utf-8 -*- import scrapy class SunproItem(scrapy.Item): # define the fields for your item here like: title = scrapy.Field() new_num = scrapy.Field() class DetailItem(scrapy.Item): new_id = scrapy.Field() content = scrapy.Field() """ pipelines.py """ class SunproPipeline(object): def process_item(self, item, spider): #如何判定item的类型 #将数据写入数据库时,如何保证数据的一致性,可以通过列表页和详情页的编号一致来判断 if item.__class__.__name__ == 'DetailItem': print(item['new_id'],item['content']) pass else: print(item['new_num'],item['title']) return item """ settings.py """ ITEM_PIPELINES = { 'sunPro.pipelines.SunproPipeline': 300, } LOG_LEVEL = 'ERROR' ROBOTSTXT_OBEY = False
""" CrawlSpider #链接提取器就可以根据正则表达式在页面中提取指定的链接,提取到的链接会全部交给规则解析器 LinkExtractor:顾名思义,链接提取器。 - 作用:提取response中符合规则的链接。 LinkExtractor( allow=r'Items/',# 满足括号中“正则表达式”的值会被提取,如果为空,则全部匹配。 deny=xxx, # 满足正则表达式的则不会被提取。 restrict_xpaths=xxx, # 满足xpath表达式的值会被提取 restrict_css=xxx, # 满足css表达式的值会被提取 deny_domains=xxx, # 不会被提取的链接的domains。 ) #规则解析器接受了链接提取器发送的链接后,就会对这些链接发起请求,获取链接对应的页面内容,就会根据指定的规则对页面内容中指定的数据值进行解析 Rule : 规则解析器。根据链接提取器中提取到的链接,根据指定规则提取解析器链接网页中的内容。 Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True) - 参数介绍: 参数1:指定链接提取器 参数2:指定规则解析器解析数据的规则(回调函数) 参数3:是否将链接提取器继续作用到链接提取器提取出的链接网页中。当callback为None,参数3的默认值为true。 rules=( ):指定不同规则解析器。一个Rule对象表示一种提取规则。 """ # -*- coding: utf-8 -*- import scrapy from newsPro.items import NewsproItem from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class Chinanews(CrawlSpider): name = 'chinanews' # allowed_domains = ['http://www.chinanews.com/scroll-news/news1.html'] #中国新闻网(滚动新闻) start_urls = ['http://www.chinanews.com/scroll-news/news1.html', 'http://finance.chinanews.com/cj/gd.shtml' ] custom_settings = { "ITEM_PIPELINES": {'newsPro.pipelines.NewsproPipeline': 300} } link = LinkExtractor(allow=r'http://www.chinanews.com/scroll-news/news\d+.html') link2 = LinkExtractor(allow=r'http://finance.chinanews.com/cj/gd.shtml') # 中国新闻网(财经新闻) rules = ( Rule(link, callback='parse_first', follow=True), Rule(link2, callback='parse_two', follow=True), ) n=0 def parse_first(self, response): li_list = response.xpath('/html/body//div[@class="content_list"]/ul/li') # print(len(li_list)) for li in li_list: url = li.xpath('./div[@class="dd_bt"]/a/@href').extract_first() url="http:%s"%url # print(url) yield scrapy.Request(url=url, callback=self.parse_con,) def parse_two(self, response): li_list = response.xpath('/html/body//div[@class="content_list"]/ul/li') # print(len(li_list)) for li in li_list: url = li.xpath('./div[@class="dd_bt"]/a/@href').extract_first() url="http://finance.chinanews.com%s"%url # print(url) yield scrapy.Request(url=url, callback=self.parse_con,) def parse_con(self, response): # self.n+=1 # print(self.n) # print(response) div= response.xpath('/html/body//div[@id="cont_1_1_2"]') title =div.xpath('./h1/text()').extract_first() ctime_s =div.xpath('//div[@class="left-t"]/text()').extract_first().split("来源:") editor =div.xpath('./div[@class="left_name"]/div[@class="left_name"]/text()').extract_first() source =ctime_s[-1] ctime = ctime_s[0] content_list =div.xpath('./div[@class="left_zw"]/p/text()').extract() content = "".join(content_list) # print('=======', title, '=======') # print('=======', ctime,'=======') # print('=======', source, '=======') # print('=======', editor, '=======') # print('=======',content, '=======') item = NewsproItem() item['title'] =title.strip() item['source'] =source.strip() item['content'] =content.strip() item['ctime'] =ctime.strip() item['editor'] =editor.strip() yield item
五、基于scrapy-redis的分布式爬虫
1、分布式爬虫
- 概念:多台机器上可以执行同一个爬虫程序,实现网站数据的分布爬取。
- 原生的scrapy框架是否可以自己实现分布式?
- 不可以。
- 原因1:因为多台机器上部署的scrapy会各自拥有各自的调度器,这样就使得多台机器无法分配start_urls列表中的url。(多台机器无法共享同一个调度器)
- 原因2:多台机器爬取到的数据无法通过同一个管道对数据进行统一的数据持久出存储。(多台机器无法共享同一个管道)
- 安装scrapy-redis组件:
- pip install scrapy-redis
- scrapy-redis组件:
- scrapy-redis是基于scrapy框架开发出的一套组件,其作用就是可以让scrapy实现分布式爬虫。
- 使用scrapy-redis组件中封装好的调度器,将所有的url存储到该指定的调度器中,从而实现了多台机器的调度器共享。
- 使用scrapy-redis组件中封装好的管道,将每台机器爬取到的数据存储通过该管道存储到redis数据库中,从而实现了多台机器的管道共享。
- 实现方式:下述两种实现流程是统一的
- 基于该组件的RedisSpider类
- 基于该组件的RedisCrawlSpider类
""" 1.redis配置文件的配置: - 注释该行:bind 127.0.0.1,表示可以让其他ip访问redis - 将yes该为no:protected-mode no,表示可以让其他ip操作redis,关闭保护模式 2.redis服务器的开启: - 基于配置配置文件 - win:redis.windows.conf linux:redis.conf 3.修改爬虫文件中的相关代码: - 将爬虫类的父类修改成基于RedisSpider或者RedisCrawlSpider。注意:如果原始爬虫文件是基于Spider的,则应该将父类修改成RedisSpider, 如果原始爬虫文件是基于CrawlSpider的,则应该将其父类修改成RedisCrawlSpider。 from scrapy_redis.spiders import RedisCrawlSpider from scrapy_redis.spiders import RedisSpider class WangyiSpider(RedisSpider): pass - 注释或者删除start_urls列表,切加入redis_key属性,属性值为scrapy-redis组件中调度器队列的名称 start_url修改成redis_key = 'wangyi' 调度器队列名称 4.在配置文件中进行相关配置,开启使用scrapy-redis组件中封装好的管道 ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 400 } 5.在配置文件中进行相关配置,开启使用scrapy-redis组件中封装好的调度器 # 使用scrapy-redis组件的去重队列 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用scrapy-redis组件自己的调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 是否允许暂停 SCHEDULER_PERSIST = True 6.在配置文件中进行爬虫程序链接redis的配置: REDIS_HOST = 'redis服务的ip地址' REDIS_PORT = 6379 REDIS_ENCODING = ‘utf-8’ REDIS_PARAMS = {‘password’:’123456’} 7.开启redis服务器:redis-server 配置文件 8.开启redis客户端:redis-cli 9.运行爬虫文件: cd 到存放xxx.py的spiders目录 scrapy runspider SpiderFile 10.向调度器队列中扔入一个起始url(在redis客户端中操作):lpush redis_key属性值 起始url (lpush 调度器队列的名称 “起始url”)lpush wangyi https://news.163.com ctrl + c 停止爬取 keys * 查看 lrange wangyi:items 0 -1 查看 llen wangyi:items 查看数量 """ """ redis实现分布式基本流程 """ # -*- coding: utf-8 -*- import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from redisPro.items import RedisproItem #导入scrapy-redis中的模块 from scrapy_redis.spiders import RedisCrawlSpider from redisPro.items import RedisproItem class QiubaiSpider(RedisCrawlSpider): name = 'qiubai' #allowed_domains = ['https://www.qiushibaike.com/pic/'] #start_urls = ['https://www.qiushibaike.com/pic/'] #调度器队列的名称 redis_key = 'qiubaispider' #表示跟start_urls含义是一样 #【注意】近期糗事百科更新了糗图板块的反爬机制,更新后该板块的页码链接/pic/page/2/s=5135066, # 末尾的数字每次页面刷新都会变化,因此正则不可写为/pic/page/\d+/s=5135066而应该修改成/pic/page/\d+ link = LinkExtractor(allow=r'/pic/page/\d+') rules = ( Rule(link, callback='parse_item', follow=True), ) def parse_item(self, response): #将图片的url进行解析 div_list = response.xpath('//*[@id="content-left"]/div') for div in div_list: img_url = div.xpath('./div[@class="thumb"]/a/img/@src').extract_first() item = RedisproItem() item['img_url'] = img_url yield item """ items.py """ # -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html import scrapy class RedisproItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() img_url = 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://doc.scrapy.org/en/latest/topics/item-pipeline.html class RedisproPipeline(object): def process_item(self, item, spider): return item """ settings.py 19行 22行 ...管道 ...调度器 """ # Crawl responsibly by identifying yourself (and your website) on the user-agent #USER_AGENT = 'qiubaiPro (+http://www.yourdomain.com)' USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' # Obey robots.txt rules ROBOTSTXT_OBEY = False ITEM_PIPELINES = { #'redisPro.pipelines.RedisproPipeline': 300, 'scrapy_redis.pipelines.RedisPipeline': 400, } # 使用scrapy-redis组件的去重队列 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用scrapy-redis组件自己的调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 是否允许暂停 SCHEDULER_PERSIST = True #如果redis服务器不在自己本机,则需要配置redis服务的ip和端口 REDIS_HOST = '172.20.10.7' #redis数据库所在的机器上的ip地址 REDIS_PORT = 6379 #REDIS_PARAMS = {‘password’:’123456’} ################################################################################################## # -*- coding: utf-8 -*- import scrapy from selenium import webdriver from wangyiPro.items import WangyiproItem from scrapy_redis.spiders import RedisSpider class WangyiSpider(RedisSpider): name = 'wangyi' #allowed_domains = ['www.xxxx.com'] #start_urls = ['https://news.163.com'] redis_key = 'wangyi' def __init__(self): #实例化一个浏览器对象(实例化一次) self.bro = webdriver.Chrome(executable_path='/Users/bobo/Desktop/chromedriver') #必须在整个爬虫结束后,关闭浏览器 def closed(self,spider): print('爬虫结束') self.bro.quit() def parse(self, response): lis = response.xpath('//div[@class="ns_area list"]/ul/li') indexs = [3,4,6,7] li_list = [] #存储的就是国内,国际,军事,航空四个板块对应的li标签对象 for index in indexs: li_list.append(lis[index]) #获取四个板块中的链接和文字标题 for li in li_list: url = li.xpath('./a/@href').extract_first() title = li.xpath('./a/text()').extract_first() #print(url+":"+title) #对每一个板块对应的url发起请求,获取页面数据(标题,缩略图,关键字,发布时间,url) yield scrapy.Request(url=url,callback=self.parseSecond,meta={'title':title}) def parseSecond(self,response): div_list = response.xpath('//div[@class="data_row news_article clearfix "]') #print(len(div_list)) for div in div_list: head = div.xpath('.//div[@class="news_title"]/h3/a/text()').extract_first() url = div.xpath('.//div[@class="news_title"]/h3/a/@href').extract_first() imgUrl = div.xpath('./a/img/@src').extract_first() tag = div.xpath('.//div[@class="news_tag"]//text()').extract() tags = [] for t in tag: t = t.strip(' \n \t') tags.append(t) tag = "".join(tags) #获取meta传递过来的数据值title title = response.meta['title'] #实例化item对象,将解析到的数据值存储到item对象中 item = WangyiproItem() item['head'] = head item['url'] = url item['imgUrl'] = imgUrl item['tag'] = tag item['title'] = title #对url发起请求,获取对应页面中存储的新闻内容数据 yield scrapy.Request(url=url,callback=self.getContent,meta={'item':item}) #print(head+":"+url+":"+imgUrl+":"+tag) def getContent(self,response): #获取传递过来的item item = response.meta['item'] #解析当前页面中存储的新闻数据 content_list = response.xpath('//div[@class="post_text"]/p/text()').extract() content = "".join(content_list) item['content'] = content yield item """ items.py """ # -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html import scrapy class WangyiproItem(scrapy.Item): # define the fields for your item here like: head = scrapy.Field() url = scrapy.Field() imgUrl = scrapy.Field() tag = scrapy.Field() title = scrapy.Field() content = 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://doc.scrapy.org/en/latest/topics/item-pipeline.html class WangyiproPipeline(object): def process_item(self, item, spider): print(item['title']+':'+item['content']) return item """ middlewares.py """ # -*- coding: utf-8 -*- # Define here the models for your spider middleware # # See documentation in: # https://doc.scrapy.org/en/latest/topics/spider-middleware.html from scrapy import signals from scrapy.http import HtmlResponse import time from scrapy.contrib.downloadermiddleware.useragent import UserAgentMiddleware import random #UA池代码的编写(单独给UA池封装一个下载中间件的一个类) #1,导包UserAgentMiddlware类 class RandomUserAgent(UserAgentMiddleware): def process_request(self, request, spider): #从列表中随机抽选出一个ua值 ua = random.choice(user_agent_list) #ua值进行当前拦截到请求的ua的写入操作 request.headers.setdefault('User-Agent',ua) #批量对拦截到的请求进行ip更换 class Proxy(object): def process_request(self, request, spider): #对拦截到请求的url进行判断(协议头到底是http还是https) #request.url返回值:http://www.xxx.com h = request.url.split(':')[0] #请求的协议头 if h == 'https': ip = random.choice(PROXY_https) request.meta['proxy'] = 'https://'+ip else: ip = random.choice(PROXY_http) request.meta['proxy'] = 'http://' + ip class WangyiproDownloaderMiddleware(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. 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 #拦截到响应对象(下载器传递给Spider的响应对象) #request:响应对象对应的请求对象 #response:拦截到的响应对象 #spider:爬虫文件中对应的爬虫类的实例 def process_response(self, request, response, spider): #响应对象中存储页面数据的篡改 if request.url in['http://news.163.com/domestic/','http://news.163.com/world/','http://news.163.com/air/','http://war.163.com/']: spider.bro.get(url=request.url) js = 'window.scrollTo(0,document.body.scrollHeight)' spider.bro.execute_script(js) time.sleep(2) #一定要给与浏览器一定的缓冲加载数据的时间 #页面数据就是包含了动态加载出来的新闻数据对应的页面数据 page_text = spider.bro.page_source #篡改响应对象 return HtmlResponse(url=spider.bro.current_url,body=page_text,encoding='utf-8',request=request) else: return response PROXY_http = [ '153.180.102.104:80', '195.208.131.189:56055', ] PROXY_https = [ '120.83.49.90:9000', '95.189.112.214:35508', ] user_agent_list = [ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 " "(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1", "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 " "(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 " "(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 " "(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 " "(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 " "(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5", "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 " "(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 " "(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 " "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 " "(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24" ] """ settings.py 19行 22行 56开启下载中间件 ...管道 """ # Crawl responsibly by identifying yourself (and your website) on the user-agent #USER_AGENT = 'qiubaiPro (+http://www.yourdomain.com)' USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' # Obey robots.txt rules ROBOTSTXT_OBEY = False DOWNLOADER_MIDDLEWARES = { 'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543, 'wangyiPro.middlewares.RandomUserAgent': 542, 'wangyiPro.middlewares.Proxy': 541, } ITEM_PIPELINES = { #'wangyiPro.pipelines.WangyiproPipeline': 300, 'scrapy_redis.pipelines.RedisPipeline': 400, } #配置redis服务的ip和端口 REDIS_HOST = '172.20.10.7' #redis数据库所在的机器上的ip地址 REDIS_PORT = 6379 #REDIS_PARAMS = {‘password’:’123456’} # 使用scrapy-redis组件的去重队列 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用scrapy-redis组件自己的调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 是否允许暂停 SCHEDULER_PERSIST = True
""" - 分布式爬虫 - 概念:我们需要搭建一个分布式的机群,让其对一组资源进行分布联合爬取。 - 作用:提升爬取数据的效率 - 如何实现分布式? - 安装一个scrapy-redis的组件 - 原生的scarapy是不可以实现分布式爬虫,必须要让scrapy结合着scrapy-redis组件一起实现分布式爬虫。 - 为什么原生的scrapy不可以实现分布式? - 调度器不可以被分布式机群共享 - 管道不可以被分布式机群共享 - scrapy-redis组件作用: - 可以给原生的scrapy框架提供可以被共享的管道和调度器 - 实现流程 - 创建一个工程 - 创建一个基于CrawlSpider的爬虫文件 - 修改当前的爬虫文件: - 导包:from scrapy_redis.spiders import RedisCrawlSpider - 将start_urls和allowed_domains进行注释 - 添加一个新属性:redis_key = 'sun' 可以被共享的调度器队列的名称 - 编写数据解析相关的操作 - 将当前爬虫类的父类修改成RedisCrawlSpider - 修改配置文件settings - 指定使用可以被共享的管道: ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 400 } - 指定调度器: # 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用scrapy-redis组件自己的调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据 SCHEDULER_PERSIST = True - 指定redis服务器: - redis相关操作配置: - 配置redis的配置文件: - linux或者mac:redis.conf - windows:redis.windows.conf - 代开配置文件修改: - 将bind 127.0.0.1进行删除 - 关闭保护模式:protected-mode yes改为no - 结合着配置文件开启redis服务 - redis-server 配置文件 - 启动客户端: - redis-cli - 执行工程: - cd xxx - scrapy runspider xxx.py - 向调度器的队列中放入一个起始的url: - 调度器的队列在redis的客户端中 - lpush xxx www.xxx.com - 爬取到的数据存储在了redis的proName:items这个数据结构中 """ # -*- coding: utf-8 -*- import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from fbsPro.items import FbsproItem from scrapy_redis.spiders import RedisCrawlSpider class FbsSpider(RedisCrawlSpider): name = 'fbs' # allowed_domains = ['www.xxx.com'] # start_urls = ['http://www.xxx.com/'] redis_key = 'sun' rules = ( Rule(LinkExtractor(allow=r'type=4&page=\d+'), callback='parse_item', follow=True), ) def parse_item(self, response): tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr') for tr in tr_list: new_num = tr.xpath('./td[1]/text()').extract_first() new_title = tr.xpath('./td[2]/a[2]/@title').extract_first() item = FbsproItem() item['title'] = new_title item['new_num'] = new_num yield item """ items.py """ # -*- coding: utf-8 -*- import scrapy class FbsproItem(scrapy.Item): # define the fields for your item here like: title = scrapy.Field() new_num = scrapy.Field() # pass """ settings.py """ ROBOTSTXT_OBEY = False USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36' #指定管道 ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 400 } #指定调度器 # 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用scrapy-redis组件自己的调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据 SCHEDULER_PERSIST = True #指定redis REDIS_HOST = '127.0.0.1' #redis远程服务器的ip(修改) REDIS_PORT = 6379
六、增量式爬虫
概念:通过爬虫程序监测某网站数据更新的情况,以便可以爬取到该网站更新出的新数据。
如何进行增量式的爬取工作:
- 在发送请求之前判断这个URL是不是之前爬取过
- 在解析内容后判断这部分内容是不是之前爬取过
- 写入存储介质时判断内容是不是已经在介质中存在
分析: 不难发现,其实增量爬取的核心是去重, 至于去重的操作在哪个步骤起作用,只能说各有利弊。前两种思路需要根据实际情况取一个(也可能都用)。第一种思路适合不断有新页面出现的网站,比如说小说的新章节,每天的最新新闻等等;第二种思路则适合页面内容会更新的网站。第三个思路是相当于是最后的一道防线。这样做可以最大程度上达到去重的目的。
去重方法:
- 将爬取过程中产生的url进行存储,存储在redis的set中。当下次进行数据爬取时,首先对即将要发起的请求对应的url在存储的url的set中做判断,如果存在则不进行请求,否则才进行请求。
- 对爬取到的网页内容进行唯一标识的制定,然后将该唯一表示存储至redis的set中。当下次爬取到网页数据的时候,在进行持久化存储之前,首先可以先判断该数据的唯一标识在redis的set中是否存在,在决定是否进行持久化存储。
""" 增量式爬虫 - 概念:监测网站数据更新的情况,只会爬取网站最新更新出来的数据。 - 分析: - 指定一个起始url - 基于CrawlSpider获取其他页码链接 - 基于Rule将其他页码链接进行请求 - 从每一个页码对应的页面源码中解析出每一个电影详情页的URL - 核心:检测电影详情页的url之前有没有请求过 - 将爬取过的电影详情页的url存储 - 存储到redis的set数据结构 - 对详情页的url发起请求,然后解析出电影的名称和简介 - 进行持久化存储 """ # -*- coding: utf-8 -*- import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from redis import Redis from moviePro.items import MovieproItem class MovieSpider(CrawlSpider): name = 'movie' # allowed_domains = ['www.ccc.com'] start_urls = ['https://www.4567tv.tv/frim/index1.html'] rules = ( Rule(LinkExtractor(allow=r'/frim/index1-\d+\.html'), callback='parse_item', follow=True), ) # 创建redis链接对象 conn = Redis(host='127.0.0.1', port=6379) #用于解析每一个页码对应页面中的电影详情页的url def parse_item(self, response): li_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li') for li in li_list: # 获取详情页的url detail_url = 'https://www.4567tv.tv' + li.xpath('./div/a/@href').extract_first() # 将详情页的url存入redis的set中 ex = self.conn.sadd('urls', detail_url) if ex == 1: print('该url没有被爬取过,可以进行数据的爬取') yield scrapy.Request(url=detail_url, callback=self.parst_detail) else: print('数据还没有更新,暂无新数据可爬取!') # 解析详情页中的电影名称和类型,进行持久化存储 def parst_detail(self, response): item = MovieproItem() item['name'] = response.xpath('/html/body/div[1]/div/div/div/div[2]/h1/text()').extract_first() item['desc'] = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/span[2]//text()').extract() item['desc'] = ''.join(item['desc']) yield item """ items.py """ # -*- coding: utf-8 -*- import scrapy class MovieproItem(scrapy.Item): # define the fields for your item here like: name = scrapy.Field() desc = scrapy.Field() # pass """ pipelines.py """ from redis import Redis class MovieproPipeline(object): conn = None def open_spider(self,spider): self.conn = spider.conn def process_item(self, item, spider): dic = { 'name':item['name'], 'desc':item['desc'] } # print(dic) self.conn.lpush('movieData',dic) return item """ settings.py """ ITEM_PIPELINES = { 'moviePro.pipelines.MovieproPipeline': 300, } ROBOTSTXT_OBEY = False LOG_LEVEL = 'ERROR' USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36'
# 去重1-爬虫文件 import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from redis import Redis from incrementPro.items import IncrementproItem class MovieSpider(CrawlSpider): name = 'movie' # allowed_domains = ['www.xxx.com'] start_urls = ['http://www.4567tv.tv/frim/index7-11.html'] rules = ( Rule(LinkExtractor(allow=r'/frim/index7-\d+\.html'), callback='parse_item', follow=True), ) #创建redis链接对象 conn = Redis(host='127.0.0.1',port=6379) def parse_item(self, response): li_list = response.xpath('//li[@class="p1 m1"]') for li in li_list: #获取详情页的url detail_url = 'http://www.4567tv.tv'+li.xpath('./a/@href').extract_first() #将详情页的url存入redis的set中 ex = self.conn.sadd('urls',detail_url) if ex == 1: print('该url没有被爬取过,可以进行数据的爬取') yield scrapy.Request(url=detail_url,callback=self.parst_detail) else: print('数据还没有更新,暂无新数据可爬取!') #解析详情页中的电影名称和类型,进行持久化存储 def parst_detail(self,response): item = IncrementproItem() item['name'] = response.xpath('//dt[@class="name"]/text()').extract_first() item['kind'] = response.xpath('//div[@class="ct-c"]/dl/dt[4]//text()').extract() item['kind'] = ''.join(item['kind']) yield item # 去重1-管道文件 from redis import Redis class IncrementproPipeline(object): conn = None def open_spider(self,spider): self.conn = Redis(host='127.0.0.1',port=6379) def process_item(self, item, spider): dic = { 'name':item['name'], 'kind':item['kind'] } print(dic) self.conn.lpush('movieData',dic) return item ############################################# # 去重2-爬虫文件 import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from incrementByDataPro.items import IncrementbydataproItem from redis import Redis import hashlib class QiubaiSpider(CrawlSpider): name = 'qiubai' # allowed_domains = ['www.xxx.com'] start_urls = ['https://www.qiushibaike.com/text/'] rules = ( Rule(LinkExtractor(allow=r'/text/page/\d+/'), callback='parse_item', follow=True), Rule(LinkExtractor(allow=r'/text/$'), callback='parse_item', follow=True), ) #创建redis链接对象 conn = Redis(host='127.0.0.1',port=6379) def parse_item(self, response): div_list = response.xpath('//div[@id="content-left"]/div') for div in div_list: item = IncrementbydataproItem() item['author'] = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span[2]/h2/text()').extract_first() item['content'] = div.xpath('.//div[@class="content"]/span/text()').extract_first() #将解析到的数据值生成一个唯一的标识进行redis存储 source = item['author']+item['content'] source_id = hashlib.sha256(source.encode()).hexdigest() #将解析内容的唯一表示存储到redis的data_id中 ex = self.conn.sadd('data_id',source_id) if ex == 1: print('该条数据没有爬取过,可以爬取......') yield item else: print('该条数据已经爬取过了,不需要再次爬取了!!!') # 去重2-管道文件 from redis import Redis class IncrementbydataproPipeline(object): conn = None def open_spider(self, spider): self.conn = Redis(host='127.0.0.1', port=6379) def process_item(self, item, spider): dic = { 'author': item['author'], 'content': item['content'] } # print(dic) self.conn.lpush('qiubaiData', dic) return item