就是一个集成了很多功能并且具有很强通用性的一个项目模板。
专门学习框架封装的各类功能的详解用法。
爬虫中封装好的一个明星框架。
功能:
高性能的持久化存储
异步的数据下载
高性能的数据解析
分布式
环境的安装: pip install scrapy
测试安装成功: 在终端里录入scrapy指令,没有报错即安装成功。
创建一个工程:
scrapy startproject **Pro
cd **Pro
PS D:\pycharm 工作空间> cd 实训/实训05/ PS D:\pycharm 工作空间\实训\实训05> scrapy startproject firstBlood
在spiders子目录中创建一个爬虫文件:
scrapy genspider spiderName www.***.com
PS D:\pycharm 工作空间\实训\实训05> cd firstBlood/ PS D:\pycharm 工作空间\实训\实训05\firstBlood> scrapy genspider first www.***.com
执行工程:
scrapy crawl spiderName
PS D:\pycharm 工作空间\实训\实训05\firstBlood> scrapy crawl first
scrapy crawl spiderName --nolog (只显示print打印的结果,不显示日志)
1.基于终端指令:
要求:
只可以将parse方法的返回值存储到本地的文件夹当中
parse方法中一定要有返回值。
注意:持久化存储对应的文本文件的类型只可以是:'json', 'jsonlines', 'ji', 'csv', 'xml', 'marshal', 'pickle'
指令:scrapy crawl *** -o filePath
scrapy crawl qiutu -o ./qiutu.csv
优点: 简洁、高效、便捷
弊端:局限性比较强(数据只可以存储到指定后缀的文本文件中)
# -*- coding: utf-8 -*- import scrapy class QiutuSpider(scrapy.Spider): name = 'qiutu' #allowed_domains = ['www.///.com'] start_urls = ['https://www.shiwens.com/mingju?lx=%E7%88%B1%E6%83%85'] def parse(self, response, *args): div_list = response.xpath('/html/body/div[2]/div[1]/div[2]/div[@class="cont mj_wrap"]') count_list = [] for div in div_list: content = div.xpath('./a[1]/text()').extract_first() poet = "——"+div.xpath('./a[2]/text()')[0].extract() dic = { "content": content, "poet": poet } count_list.append(dic) return count_list #一定要有返回值
2.基于管道:
编码流程(注释的都是精华):
数据解析
在item类中定义相关属性
import scrapy class QiutuproItem(scrapy.Item): # define the fields for your item here like: content = scrapy.Field() poet = scrapy.Field()
将解析的数据封装存储到item类型的对象
from qiutuPro.items import QiutuproItem item = QiutuproItem() item["content"] = content item["poet"] = poet
将item类型的对象提交给管道进行持久化存储的操作
yield item
在管道类的process_item中要将其接收到的item对象中存储的数据进行持久化存储的操作
class QiutuproPipeline(object):#自动添加object类 fp = None #重写父类的一个方法:该方法只在开始安排爬虫时,调用一次 def open_spider(self, spider): print("开始爬虫......") self.fp = open('./qiutu.txt', 'w', encoding='utf-8') def process_item(self, item, spider): #专门用来处理item类型对象 #该方法可以接受爬虫文件提交过来的item对象 #该方法每接受到一个item对象就会被调用一次 content = item['content'] poet = item['poet'] self.fp.write(content+poet+'\n') return item def close_spider(self,spider): print("结束爬虫!!!") self.fp.close()
在配置文件中开启管道
ITEM_PIPELINES = { 'qiutuPro.pipelines.QiutuproPipeline': 300, }//300表示的是优先级,数值越小优先级越高
好处:通用性强
注意:
在编码流程的第三步中,需进行导入操作,即:from qiutuPro.item import qiutuProItem
在settings.py的文件中的ITEM_PIPELINES字典中,键对应的值越小,优先被执行
补充:
cls 或 clear 或 ctrl + L :清屏
在settings.py文件中需执行三个操作:
把 ROBOTSTXT_OBEY = Ture 改成: ROBOTSTXT_OBEY = False
添加:LOG_LEVEL = "ERROR" (作用:只有当出错时,才显示日志信息)
把USER_AGENT赋值为我们电脑的user_agent,进行伪装。
ROBOTSTXT_OBEY = False LOG_LEVEL = "ERROR" USER_AGENT = '******'
问:将爬取到的数据一份存储到本地一份存储到数据库,如何实现?
答:
管道文件中的一个管道类对应的是将数据库存储到一种平台
爬虫文件提交的item只会给管道文件中的第一个被执行的管道类接受
process_item中的return item 表示将item传递给下一个即将被执行的管道类
下面是对pipelines.py文件的修改:
from itemadapter import ItemAdapter import pymysql class QiutuproPipeline(object):#自动添加object类 fp = None #重写父类的一个方法:该方法只在开始安排爬虫时,调用一次 def open_spider(self, spider): print("开始爬虫......") self.fp = open('./qiutu.txt', 'w', encoding='utf-8') def process_item(self, item, spider): #专门用来处理item类型对象 #该方法可以接受爬虫文件提交过来的item对象 #该方法每接受到一个item对象就会被调用一次 content = item['content'] poet = item['poet'] self.fp.write(content+poet+'\n') return item def close_spider(self,spider): print("结束爬虫!!!") self.fp.close() #管道文件中的一个管道类对应将一组数据存储到一个平台或一个载体中 class mysqlpileLine(object): conn = None cursor = None def open_spider(self,spider): self.conn = pymysql.Connect(host='127.0.0.1', port=3306, user='root', password='123456', db='qiutu', charset='utf8') def process_item(self,item,spider): try: self.cursor.executel('insert into qiutu values("%s","%s")'%(item['content'], item['poet'])) self.conn.commit() except Exception as e: print(e) self.conn.rollback() def close_spider(self,spider): self.cursor.close() self.conn.close()
爬虫文件1:
import scrapy class FirstSpider(scrapy.Spider): name = 'first' #爬虫文件的名称:就是爬虫源文件的一个唯一标识。 allowed_domains = ['www.***.com'] #允许的域名:用来限定start_urls列表中哪些url可以进行请求发送。(通常会被注释掉) start_urls = ['http://www.***.com/'] #起始的url列表:该列表中存放的url会被scrapy自动进行请求发送 #用作数据解析:response参数表示的就是请求成功后对应的响应对象 def parse(self, response): a = response.xpath('//*[@id="content"]/div/div[1]/div/div/table[1]//td[2]/div/p/text()').extract() print(a)
重难点:
''' 1.xpath返回的是一个列表,但和之前学过的xpath有相同的地方有不同的地方 2.xpath返回列表中的元素是Selector类型的对象 3.extract可以将Selector对象中data参数存储的字符串提取出来 4.列表调用了extract之后,则表示将列表中的每一个Selector对象对应的dada参数的值提取出来放到列表当中 5.若确定xpath返回的列表当中只有一个元素,则可以将xpath('***')[0].extract 改为 xpath('***').extract_first() 6.可能会出现 函数parse()中的response标红的情况,只需在括号里添加:*arg 7.xpath():不能识别font、tbody标签。 '''
本质:就是将网站中某模板下的全部页面对应的页面数据进行爬取
实现方式:
将所有页面的url添加到start_urls列表(不推荐)
自行手动进行添加(推荐)
手动请求发送:yield scrapy.Request(url,callback)//专门用做于数据解析
这里我们通过 yield
来发起一个请求,并通过 callback
参数为这个请求添加回调函数,在请求完成之后会将响应作为参数传递给回调函数。
示例(爬取豆瓣排行榜中电影的的名称)
import scrapy class MovienameSpider(scrapy.Spider): name = 'movieName' #allowed_domains = ['www.aaa.com'] start_urls = ['https://movie.douban.com/top250'] url = 'https://movie.douban.com/top250?start=%d&filter=' page_num = 25 def parse(self, response, *args): li_list = response.xpath('//*[@id="content"]/div/div[1]/ol/li') for li in li_list: name = li.xpath('./div[1]/div[2]/div[1]/a/span[1]/text()').extract_first() movie_url = li.xpath('./div[1]/div[2]/div[1]/a/@href').extract_first() if self.page_num <= 225: new_url = format(self.url%self.page_num) self.page_num += 25 yield scrapy.Request(url=new_url, callback=self.parse)
引擎(Scrapy)
用来处理整个系统的数据流处理,触发事务(框架核心)
调度器(Scheduler)
用来接受引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回。可以想象成一个url(爬取网页的网址或者说是链接)的优先队列,由它来决定下一个要爬取的网址是什么,同时去除重复的网址。
下载器(DOwnloader)
用来下载网页内容,并将网页内容返回给spider(Scrapy下载器是建立在twisted这个高效的异步模型上的)
爬虫(Spiders)
爬虫是主要干活的,用于从特定的网页中提取自己需要的信息,即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续爬取下一个页面
项目管道(Pipeline)
负责处理爬虫从网页中抽取的实体,主要功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
使用场景:如果解析的数据不在同一张页面中(即:需要深度爬取)
示例:爬取boss的岗位名称,岗位描述(注释即精华)
# -*- coding: utf-8 -*- import scrapy from bossPro.items import BossproItem class BossSpider(scrapy.Spider): name = 'boss' #allowed_domains = ['www.123.com'] start_urls = ['https://search.51job.com/list/000000,000000,0000,00,9,99,Python,2,1.html'] page = 2 url = 'https://search.51job.com/list/000000,000000,0000,00,9,99,Python,2,%d.html' def parse_detail(self, response, *args): item = response.meta['item'] list = response.xpath('/html/body/div[3]/div[2]/div[3]/div[1]/div/p//text()').extract() print("list:", list) job_detail = ''.join(list) print(job_detail) item['job_detail'] = job_detail yield item def parse(self, response, *args): # div_list = response.xpath('/html/body/div[2]/div[3]/div/div[2]/div[4]/div[1]/div') div_list = response.xpath('/html/body/div[2]/div[3]/div[1]/div[2]/div[4]/div[1]/div') print("div_list", div_list) item = BossproItem() for div in div_list: job_name = div.xpath('./a/p/span[1]/text()').extract_first() print(job_name) item['job_name'] = job_name job_detail_url = div.xpath('./a/@href').extract_first() yield scrapy.Request(url=job_detail_url, callback=self.parse_detail, meta={'item': item}) if self.page <= 3: page_url = format(self.url%self.page) self.page += 1 yield scrapy.Request(url=page_url, callback=self.parse)
基于scrapy爬取字符串类型的数据和爬取图片类型的数据区别?
字符串:只需要基于xpath进行解析且提交管道进行持久化存储
图片:xpath解析出图片src的属性值进行解析。单独的对图片地址发起请求获取图片二进制类型的数据
ImagesPipline
只需要将img的src的属性值进行解析,提交到管道,管道就会对图片的src进行请求的发送获取图片的二进制类型的数据,且还会帮我们进行持久化存储
使用流程:
数据解析(图片的地址)
将存储图片地址的item提交到制定的管道类
在管道文件夹中自定制一个基于ImagesPipline的一个管道类
get_media_resquest()//对指定的url发起请求
file_path()//定义图片的名称
item_completed//将item传递给下一个类使用
在配置文件中:
添加 指定图片存储的目录:IMAGES_STORE = './imgs_dingjifen'
指定开启的管道:自定制的管道类
IMAGES_STORE = './imgs_dingjifen'
from scrapy.pipelines.images import ImagesPipeline import scrapy # class ZhanzhangproPipeline: # def process_item(self, item, spider): # return item class imgs_bobo_pileline(ImagesPipeline): # 就是可以根据图片地址进行图片数据的请求 def get_media_requests(self, item, info): yield scrapy.Request(item['src']) #指定图片名称 #图片存储路径须在settings中自行设定 def file_path(self, request, response=None, info=None, item=None): imgName = item['alt'] + '.jpg' return imgName #返回给下一个即将被执行的管道类 def item_completed(self, results, item, info): return item
下载中间件
位置:引擎和下载器之间
作用:批量拦截到整个工程中所有的请求和响应
拦截请求:
1. UA伪装:process_request
2. 代理IP:process_exception:return request
拦截响应:篡改响应数据,响应对象
1.网易新闻(wangyiPro)
爬虫中间件
位置:引擎和Spider之间
补充:
中间件在middlewares.py文件中
也需要在settings.py文件中开启中间件(即:取消DOWNLOADER_MIDDLEWARES注释)
在middlewares.py文件中,可以把不用的类和方法都去掉
middlewares.py文件设置如下:
from scrapy import signals # useful for handling different item types with a single interface from itemadapter import is_item, ItemAdapter import random class BaiduproDownloaderMiddleware: # UA池的封装: user_agent_list = [ "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 " "(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1", "Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 " "(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 " "(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6", "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 " ] # 可被选用的代理IP 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) return None #拦截所有的响应 def process_response(self, request, response, spider): 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 #将修正之后的请求对象进行重新的请求发送
全站数据爬取的方式:
基于Spider:手动请求
基于CrawlSpider
CrawlSpider的使用:
创建一个工程
cd ***
创建爬虫文件(CrawlSpider):
scrapy genspider -t crawl 文件名 www.123.com
链接提取器:
作用:根据指定的规则(allow)进行指定链接的提取
规则解析器:
作用:将链接提取器提取到的链接进行指定规则(callback)的解析
爬虫文件如下:(注释均是精华)
import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class SunSpider(CrawlSpider): name = 'sun' #allowed_domains = ['www.123.com'] start_urls = ['https://movie.douban.com/top250'] #链接提取器:根据指定规则(allow="正则")进行指定链接的提取 link = LinkExtractor(allow=r'?start=\d+&filter=') #规则解析器:将链接提取器提取到的链接进行指定规则(callback)的解析操作 #follow=True:可以将链接提取器继续做作用到链接提取器提取到链接所对应的页面中 rules = ( Rule(link, callback='parse_item', follow=False) ) def parse_item(self, response): print(response)
示例:
爬虫文件(sun):
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.123.com'] start_urls = ['https://movie.douban.com/top250'] #链接提取器:根据指定规则(allow="正则")进行指定链接的提取 link = LinkExtractor(allow=r'链接正则') link_detail = LinkExtractor(allow=r'链接正则') #规则解析器:将链接提取器提取到的链接进行指定规则(callback)的解析操作 #follow=True:可以将链接提取器继续做作用到链接提取器提取到链接所对应的页面中 rules = ( Rule(link, callback='parse_item', follow=True), Rule(link_detail, callback='parse_detail', follow=True) ) #如下两个解析方法中是不可以实现请求传参的 #无法将两个解析方法解析的数据存储到同一个item中,可以依次存储到两个item #解析新闻编号和新闻标题 def parse_item(self, response): tr_list = response.xpath("") #注意:xpath表达式中不可以出现tbody标签 for tr in tr_list: new_num = tr.xpath("").extract_first() new_title = tr.xpath("").extract_first() item = SunproItem() item['title'] = new_title item['new_num'] = new_num yield item #解析新闻内容和新闻编号 def parse_detail(self, response): new_id = response.xpath("").extract_first() new_content = response.xpath("").extract() new_content = ''.join(new_content) item = DetailItem() item['content'] = new_content item['new_id'] = new_id yield item
items:
import scrapy class SunproItem(scrapy.Item): title = scrapy.Field() new_num = scrapy.Field() class DetailItem(scrapy.Item): new_id = scrapy.Field() content = scrapy.Field()
pipelines:
class SunproPipeline: def process_item(self, item, spider): #如何判断item类型 #将数据写入数据时,如何保证数据的一致性 if item.__class__.__name__ == 'DetailItem': print(item['new_id'], item['content']) else: print(item['new_num'], item['title']) return item
概念:我们需要搭建一个分布式机群,让其对一组资源进行分布联合爬取。
作用:提升爬取数据的效率
如何实现分布式?
安装一个scrapy-redis的组件
原生的scrapy是不可以实现分布式爬虫,必须要让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_HOST = 'redis服务的ip地址' REDIS_PORT = 6379 REDIS_ENCODING = 'utf-8' REDIS_PARAMS = {'password':'123456'}
redis相关操作配置:
配置redis的配置文件:
linux或max:redis.conf
windows:redis.windows.conf
打开配置文件修改:
将bind 127.0.0.1进行删除或注释
关闭保护模式:protected-mode yes 改 no
结合着配置文件开启redis服务
redis-server 配置文件
启动客户端:
redis-cli
执行工程:
scrapy runspider ***.py
向调度器的队列中放入一个起始的url:
调度器的队列在redis的客户端中
lpush ***(redis_key) www.///.com
概念:检测网站数据更新的情况,只用爬取网站最新更新出来的数据
分析:
指定一个起始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 zlsPro.items import ZlsproItem class ZlsSpider(CrawlSpider): name = 'zls' # allowed_domains = ['www.///.com'] start_urls = ['http://www.4567kp.com/frim/index1.html'] rules = ( Rule(LinkExtractor(allow=r'/frim/index1-\d+\.html'), callback='parse_item', follow=False), ) # 创建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 = ZlsproItem() 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
(管道)
# -*- coding: utf-8 -*- # Define your item pipelines here # # Don't forget to add your pipeline to the ITEM_PIPELINES setting # See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html # useful for handling different item types with a single interface from itemadapter import ItemAdapter from redis import Redis class ZlsproPipeline(object): conn = None fp = None def open_spider(self, spider): self.conn = spider.conn self.fp = open("电影.txt", "w", encoding='utf-8') def process_item(self, item, spider): dic = { 'name': item['name'], 'desc': item['desc'] } self.fp.write(item['name']+'\n'+item['desc']+"\n\n\n") self.conn.set('movieData', str(dic)) return item def close_spider(self, spider): self.fp.close()
yield 和return
都有返回数据的作用,最主要的不同在于yield在返回值后还可以继续运行接下来的代码,而return在返回后就不在执行代码。
在scrapy中,爬取的数据量往往十分巨大,如果使用return和list存储之后在一次性返回将带来巨大的内存消耗。而yield可以在返回一组数据后再处理下一组数据,大大减少了内存的浪费。
终止终端快捷键:ctrl c