08_scrapy&持久化存储&管道操作&手动请求发送
scrapy框架
简介
简介:所谓的框架其实就是一个被集成了很多功能且具有很强通用性的一个项目模板。
学习:学习是学好框架中集成好的各种功能、特性
进阶:逐步的探索框架的底层
scrapy:是一个专门用于异步爬虫的框架
- 高性能的数据解析、请求发送,持久化存储,全栈数据爬取,中间件,分布式……
环境的安装
-
mac、linum: pip install scrapy
-
windows:
测试:cmd中scrapy按下回车,如果没有报错说明安装成功。
scrapy的基本使用
-
创建一个工程
-
cd ProName
-
创建爬虫源文件:
- scrapy genspider spiderName www.xxx.com
- 编写对应的代码在爬虫文件中
-
执行工程
- scrapy crawl spiderName
- 执行工程后,默认会输出工程所有的日志信息
- 指定类型日志的输出:
- settings.py:加入LOG_LEVEL=‘ERROR’
配置文件settings.py
- 指定日志类型:LOG_LEVEL=‘ERROR’
- 禁止robots:
ROBOTSTXT_OBEY = False
- UA伪装 USER_AGENT
爬虫文件spiderName.py内容阐述
python">import scrapy
class FirstSpider(scrapy.Spider):
#爬虫文件名称:当前源文件的唯一标识
name = 'first'
#允许的域名,一般注释
#allowed_domains = ['www.baidu.com']
#起始的url列表:只可以存储url
#作用:列表中存储的url都会被进行get请求的发送
start_urls = ['https://www.baidu.com/','https://www.sogou.com']
#用于数据解析
#parse方法调用的次数完全取决于请求的次数
#参数response:表示的就是服务器返回的响应对象
def parse(self, response):
pass
- name:爬虫文件名称,该文件的唯一标识
- start_urls:起始url列表,存储的都是url,url可以被自动进行get请求的发送
- parse方法:请求后的数据解析操作
scrapy数据解析
-
使用:response.xpath(‘xpath表达式’)
-
scrapy封装的xpath和etree中的xpath区别
- scrapy中的xpath直接将定位到的标签中存储的值或者属性值取出,返回的是Selector对象,且相关的数据值是存储在Selector对象的data属性中,需要调用extract、extract_first()取出字符串数据
-
三种获得数据的xpath方式
python"> def parse(self, response): #数据解析名称和内容 li_list = response.xpath('//*[@id="list"]/ul/li') for li in li_list: #下面解析出来的内容不是字符串数据,说明和etree中的xpath使用方式不同 #xpath返回的列表中存储的不是字符串而是Selector对象,其实我们想要的字符串数据被存储在了该对象的data属性中 # title = li.xpath('./div[2]/a/text()')[0] # #<Selector xpath='./div[2]/a/text()' data='因为她是你的母亲'> # content = li.xpath('./div[1]/text()')[0] #将Selector对象data属性值取出 #1. extract()就是将data属性值取出(几乎不用) title = li.xpath('./div[2]/a/text()')[0].extract() #因为她是你的母亲 content = li.xpath('./div[1]/text()')[0].extract() #2. extract_first():将列表中的第一个列表元素表示的Selector对象中的data值取出 title = li.xpath('./div[2]/a/text()').extract_first() #因为她是你的母亲 content = li.xpath('./div[1]/text()').extract_first() #3. 直接使用列表调用extract():可以将列表中的每一个列表元素表示的Selector中的data取出 title = li.xpath('./div[2]/a/text()').extract() #['因为她是你的母亲'] content = li.xpath('./div[1]/text()').extract()
持久化存储
基于终端指令的持久化存储
-
要求:该种方式只可以将parse方法的返回值存储到本地指定后缀的文本文件中
-
执行指令:
scrapy crawl spiderName -o filePath
-
例如:
python"> #将解析到的数据进行持久化存储:基于终端指令的持久化存储 def parse(self, response): all_data = [] # 数据解析名称和内容 li_list = response.xpath('//*[@id="list"]/ul/li') for li in li_list: # extract_first():将列表中的第一个列表元素表示的Selector对象中的data值取出 title = li.xpath('./div[2]/a/text()').extract_first() content = li.xpath('./div[1]/text()').extract_first() dic = { 'title':title, 'content':content } all_data.append(dic) return all_data
随后命令行输入:
scrapy crawl duanzi -o duanzi.csv
-
局限性
指定后缀文件:‘json’, ‘jsonlines’, ‘jl’, ‘csv’, ‘xml’, ‘marshal’, ‘pickle’
基于管道的持久化存储(重点)
-
在爬虫文件中进行数据解析
获得每个title和content
-
在items.py中定义相关属性
- 步骤1中解析出了几个字段的数据,在此就定义几个属性
python">class WangziproItem(scrapy.Item): #Field()定义好的属性当做是一个万能类型的属性 title = scrapy.Field() content = scrapy.Field()
-
在爬虫文件中将解析到的数据存储封装到Item类型的对象中
例如:
WangziproItem
python">from wangziPro.items import WangziproItem def parse(self, response): ... #实例化一个item类型的对象,将解析到的数据存储到该对象中 item = WangziproItem() #不可以通过。的形式调用属性 item['title'] = title item['content'] = content
-
将Item类型的对象提交给管道
上接
yield item
-
在管道文件(pipelines.py)中,接受爬虫文件提交过来了Item类型对象,且对其进行任意形式的持久化存储操作
python">class WangziproPipeline: fp = None #重写父类的两个方法 def open_spider(self,spider): print('我是open_spider(),我只会在爬虫开始的时候执行一次!') self.fp = open('duanzi.txt','w',encoding='utf-8') def close_spider(self,spider): print('我是close_spider(),我只会在爬虫结束的时候执行一次!') self.fp.close() #该方法是用来接受item对象。一次只能接收一个item,说明该方法会被调用多次 #参数item:就是接收到的item对象 def process_item(self, item, spider): # print(item) #item其实就是一个字典 self.fp.write(item['title']+":"+item['content']+'\n') #将item存储到文本文件 return item
-
在配置文件settings.py中开启管道机制
python">ITEM_PIPELINES = { #300表示管道类的优先级,数值越小优先级越高 'wangziPro.pipelines.WangziproPipeline': 300, }
基于管道实现数据的备份
-
将爬取到的数据分别存储到不同的载体
-
实现:将数据一份存储到mysql,一份存储到redis
-
问题:管道文件中的一个管道类表示怎样的一组操作呢?
- 一个管道类对应一种形式的持久化存储操作。如果将数据存储到不同的载体中就需要使用多个管道类
-
已经定义好了三个管道类,将数据写入到三个载体中进行存储:
- item会不会依次提交给三个管道类?
- 不会,爬虫文件中的item只会被提交给优先级最高的那一个管道类
- 优先级高的管道类需要在process_item中实现return item,就item传递给下一个即将被执行的管道类
- item会不会依次提交给三个管道类?
注意点:
python">#duanzi.py
class DuanziSpider(scrapy.Spider):
def parse(self, response):
#...
yield item
#pipelines.py,3种保存方式:文件、mysql、redis
import pymysql
from redis import Redis
class WangziproPipeline:
fp = None
#重写父类的两个方法
def open_spider(self,spider):
print('我是open_spider(),我只会在爬虫开始的时候执行一次!')
self.fp = open('duanzi.txt','w',encoding='utf-8')
def close_spider(self,spider):
print('我是close_spider(),我只会在爬虫结束的时候执行一次!')
self.fp.close()
#该方法是用来接受item对象。一次只能接收一个item,说明该方法会被调用多次
#参数item:就是接收到的item对象
def process_item(self, item, spider):
# print(item) #item其实就是一个字典
self.fp.write(item['title']+":"+item['content']+'\n')
#将item存储到文本文件
return item
#将数据存储到mysql中
class MysqlPipLine(object):
conn = None
cursor = None
def open_spider(self,spider):
self.conn = pymysql.Connect(host='127.0.0.1',port=3306,user='root',password='1234',db='spider',charset='utf8')
print(self.conn)
def process_item(self,item,spider):
self.cursor = self.conn.cursor()
sql = 'insert into duanziwang values ("%s","%s")'%(item['title'],item['content'])
#事务处理
try:
self.cursor.execute(sql)
self.conn.commit()
except Exception as e:
print(e)
self.conn.rollback()
return item
def close_spider(self, spider):
self.cursor.close()
self.conn.close()
#将数据写入redis
class RedisPipeLine(object):
conn = None
def open_spider(self,spider):
self.conn = Redis(host='127.0.0.1',port=6379)
print(self.conn)
def process_item(self,item,spider):
#item是字典,导入到duanziData的列表中
#如果报错:将redis模块的版本指定成2.10.6即可。pip install -U redis==2.10.6
self.conn.lpush('duanziData',item)
#settings.py
ITEM_PIPELINES = {
#300表示管道类的优先级,数值越小优先级越高
#优先级高:优先级高的管道类先被执行
'wangziPro.pipelines.WangziproPipeline': 300,
'wangziPro.pipelines.MysqlPipLine': 301,
'wangziPro.pipelines.RedisPipeLine': 302
}
scrapy手动请求发送实现全站数据爬取
-
yield scrapy.Request(url,callback)
:GET请求- callback指定解析函数,用于解析数据
-
yield scrapy.FormRequest(url,callback,formdata)
:POST请求- formdata:字典,请求参数
-
实现多页爬取
python">class DuanziSpider(scrapy.Spider): name = 'duanzi' start_urls = ['http://www.haoduanzi.com/category/?1-1.html'] #通用的url模板 url = 'http://www.haoduanzi.com/category/?1-%d.html' page_num = 2 def parse(self, response): #xxx if self.page_num < 5: #结束递归的条件 new_url = format(self.url%self.page_num) #其他页码对应的完整url self.page_num += 1 #对新的页码对应的url进行请求发送(手动请求GET发送) yield scrapy.Request(url=new_url,callback=self.parse)
-
为什么start_urls列表中的url会被自动进行get请求的发送?
因为列表中的url其实是被start_requests这个方法父类实现的get请求发送
python">def start_requests(self): for u in self.start_urls: yield scrapy.Request(url=u, callback=self.parse)
-
如何将start_urls中的url默认进行post请求的发送?
重写start_requests方法即可
python">def start_requests(self): for u in self.start_urls: yield scrapy.FormRequest(url=u, callback=self.parse)
剩余内容
- scrapy的五大核心组件介绍
- 请求传参实现的深度爬取
- 中间件机制
- CrawlSpider
- 分布式
- 增量式