目录
一:scrapy框架
什么是框架?
- 就是一个集成了很多功能并且具有很强通用性的一个项目模板
什么是scrapy?
- 爬虫中封装好的一个框架。功能:高性能的持久化存储,异步的数据下载,高性能的数据解析,分布式
大体过程
创建一个工程
scrapy startproject 工程名
创建爬虫文件
**进入工程目录(cd 工程目录)**后
scrapy genspider 爬虫文件名 www.xxx.com
在spiders子目录中创建一个爬虫文件,其中url为起始url
例如:
scrapy startproject firstBlood # 工程名
scrapy genspider first www.xxx.com # 在创建的项目文件夹中 爬虫文件名
scrapy crawl 爬虫文件名 # 执行
结果:
执行爬虫工程
scrapy crawl 爬虫文件名
-
scrapy crawl 文件名 --nolog
去掉日志输出,避免干扰自己代码中定义的输出[但会使错误类型的输出不显示]- 可以在settings文件中加==
LOG_LEVEL='ERROR'
==显示错误相关的输出
- 可以在settings文件中加==
-
爬虫文件名称是爬虫源文件的唯一标识
自动发送请求 与 手动发送请求(scrapy.Request)
在spiders
子目录下的爬虫文件中
# -*- coding: utf-8 -*-
import scrapy
class FirstSpider(scrapy.Spider):
#爬虫文件的名称:就是爬虫源文件的一个唯一标识
name = 'first'
#允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
allowed_domains = ['www.baidu.com'] # start_urls中只有这个域名下的url才会访问
#起始的url列表:该列表中存放的url会被scrapy自动进行请求的发送
start_urls = ['https://www.baidu.com/','https://www.sogou.com']
#用作于数据解析:response参数表示的就是请求成功后对应的响应对象,对该相应对象进行解析
def parse(self, response): # start_urls有几个url就会调用几次parse
print(response)
-
start_urls
起始的url列表:该列表中存放的url会被scrapy自动进行请求的发送 -
重点: 如果要手动请求发送:scrapy.Request(url=new_url, callback) 要包含scrapy包
适用于全站数据爬取,爬取不同页面 (其url有一定的规律,与页面号有关) 的数据;当然也可以将所要的url放到start_urls
中
# 在parse函数中
# 手动请求发送:callback回调函数是专门用作于数据解析 类似递归调用
yield scrapy.Request(url=new_url, callback=self.parse)
- callback参数代表对对应url的相应对象,专门用于数据解析的函数,可以自己写一个函数进行数据解析
例子:
# 在spiders文件夹下的爬虫文件中
class XiaohuaSpider(scrapy.Spider):
name = 'xiaohua'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://www.521609.com/meinvxiaohua/']
#生成一个通用的url模板(不可变)
url = 'http://www.521609.com/meinvxiaohua/list12%d.html'
page_num = 2
def parse(self, response):
li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li')
for li in li_list:
img_name = li.xpath('./a[2]/b/text() | ./a[2]/text()').extract_first()
print(img_name)
if self.page_num <= 3: # page_num从2开始,因为上面已经对第一页解析过了
new_url = format(self.url%self.page_num) # 得到新的页面url
self.page_num += 1
#手动请求发送:callback回调函数是专门用作于数据解析
yield scrapy.Request(url=new_url,callback=self.parse) # 对新的url手动发送请求
伪装策略 — 不遵循robots与UA伪装
- 默认爬取是遵循robots协议的,所以没有几个网址可以正确的爬取,所以要在settings.py中修改
ROBOTSTXT_OBEY=False
- 使用UA伪装
在settings
中USER_AGENT
中写入浏览器相关的参数
进行数据解析 — response.xpath()与.extract()
使用xpath进行解析,平时的一般使用(先实例化一个etree对象,再将页面的相应对象response.text作为参数传入进行解析)
# 导入模块
from lxml import etree
# 网站爬取的
page_text = response.text
tree = etree.HTML('page_text') # 将页面上的数据加载到该对象
r = tree.xpath('/html//div')
而在scrapy中的进行解析的函数parse中直接使用
response.xpath('...')
response.xpath 结果:
-
返回的是一个列表,列表中元素为Selector对象
-
.extract()
-
对Selector对象直接调用
.extract()
方法会得到data参数存储的字符串 -
列表调用了
.extract()
方法会使得每一个Selector对象中的data对应字符串都提取出来,仍然为字符串- 使用
content = ''.join(content)
,将列表中的字符串拼接成一个字符串 .extract_first()
将列表中第一个Selector对象进行得到其data参数中的字符串,适用于:该列表中仅有一个Selector对象
- 使用
-
scrapy持久化存储
基于终端指令对parse返回值持久化存储
只可以将parse方法返回值存储到本地的文件中
终端命令:scrapy crawl 文件名 -o ./xxx.csv
-
保存文件的类型只有:
json, jsonlines, jl, csv, xml, marshal, pickle
-
优缺点:
- 好处:简洁
- 缺点:局限性强,仅可以将parse返回值存到指定后缀的文件中
例子
# 在spider文件夹中的项目文件中
class 项目名Spider(scrapy.Spider):
name = 'qiubai'
# allowed_domains = ['www.xxx.com']
start_urls = ['https://www.qiushibaike.com/text/']
def parse(self, response):
#解析:作者的名称+段子内容
div_list = response.xpath('//div[@id="content-left"]/div')
all_data = [] #存储所有解析到的数据
for div in div_list:
#xpath返回的是列表,但是列表元素一定是Selector类型的对象
#extract可以将Selector对象中data参数存储的字符串提取出来
# author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()
author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
#列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
content = div.xpath('./a[1]/div/span//text()').extract()
content = ''.join(content)
dic = {
'author':author,
'content':content
}
all_data.append(dic)
return all_data
- 之后
scrapy crawl 文件名 -o ./xxx.csv
对返回值进行存储
重:基于管道 — pipelines中的process_item
- 优缺点:
- 好处:通用性强,无后缀限制
- 涉及到的文件:
parse
函数:对数据进行解析,并把数据放到item对象
中items.py
:定义item类
,便于在parse函数
中使用pipelines.py
中process_item
函数,对传入的item对象
进行自定义存储,重写父类相关方法open_spider(self,spider)
与close_spider(self,spider)
settings.py
中设置ITEM_PIPELINES
编码流程
- 数据解析 — 使用xpath
- 在item类重定义相关的属性**(该item类在文件items.py中)**使用
scrapy.Field()
定义属性
-
解析的数据封装到item类型对象
-
将item类型对象提交给管道进行持久化存储**(管道类的定义在pipelines.py中)**
- 在管道类的process_item中要将其接受到的item对象中存储的数据进行持久化存储操作
- 在配置文件中开启管道
在==settings.py
==中取消注释ITEM_PIPELINES
实例 — 创建Item类,数据封装到item与存储
Item
类中定义相关的属性
class 工程名Item(scrapy.Item):
# define the fields for your item here like:
author = scrapy.Field()
content = scrapy.Field()
- items中属性用Field(),而其继承了字典,所以可以使用item[‘key’]进行赋值
- 将解析数据封装到item对象的属性中:要注意包含相应包
# 项目在 爬虫文件中
from 工程名.pipelines import pipe类名
item = items.py中item类名()
# items中属性用Field(),而其继承了字典,所以可以使用item['key']进行赋值
item['author'] = author
- 将item提交给管道
yield item # 当有多个结果要提交时,使用for循环,在for循环内部使用yield
parse函数
每次想管道提交一个item,pipelines文件中的类就会被调用一次process_item
parse函数的例子
import scrapy
from qiubaiPro.items import QiubaiproItem # 对相应项目的items包进行包含,便于将解析数据传入item对象中
class QiubaiSpider(scrapy.Spider):
name = 'qiubai'
# allowed_domains = ['www.xxx.com']
start_urls = ['https://www.qiushibaike.com/text/']
def parse(self, response):
#解析:作者的名称+段子内容
div_list = response.xpath('//div[@id="content-left"]/div')
all_data = [] #存储所有解析到的数据
for div in div_list:
# xpath返回的是列表,但是列表元素一定是Selector类型的对象
# extract可以将Selector对象中data参数存储的字符串提取出来
# author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()
author = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span/h2/text()').extract_first()
# 列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
content = div.xpath('./a[1]/div/span//text()').extract()
content = ''.join(content)
# 实例化一个Item对象
item = QiubaiproItem()
item['author'] = author
item['content'] = content
yield item # 将item提交给了管道
- 在管道中持久存储
问题:
在pipelines文件中,打开文件并写入的代码 最好不要在函数
process_item
中,因为每提交一个item,就调用一次process_item,这样会经常打开文件
==解决办法:==在文件pipelines
中
- 定义一个属性为 文件对象
fp
- 重写父类相关方法
open_spider(self,spider)
与close_spider(self,spider)
,会在爬虫之前和结束后调用
# pipelines.py文件中
class 项目名Pipeline(object):
# 定义一个属性,在开始爬虫,写入数据,结束爬虫时调用
fp = None
# 重点 --- 重写父类的一个方法:该方法只在开始爬虫的时候被调用一次
def open_spider(self,spider):
print('开始爬虫......')
self.fp = open('./qiubai.txt','w',encoding='utf-8')
# 重点 --- 重写父类一个方法:该方法在结束爬虫时调用
def close_spider(self,spider):
print('结束爬虫!')
self.fp.close()
# 专门用来处理item类型对象
# 该方法可以接收爬虫文件提交过来的item对象
# 该方法没接收到一个item就会被调用一次
def process_item(self, item, spider):
author = item['author']
content= item['content']
self.fp.write(author+':'+content+'\n')
return item # 就会传递给下一个即将被执行的管道类
爬虫文件向管道提交的item对象,仅仅会提交给优先级最高的管道类(仅给一个管道类)
- 但若该管道类中的
process_iter
函数中有return item
,item会传递给下一个即将被执行的管道类
- 配置文件中打开管道
在==settings.py
==中取消注释ITEM_PIPELINES
-
其中的key表示一个类,value表示优先级,数越低优先级越高,优先级高的先执行
-
多个管道类时,写入该字典
ITEM_PIPELINES
中-
爬虫文件向管道提交的item对象
仅仅会提交给优先级最高的管道类(仅给一个管道类)
- 但若该管道类 中的
process_iter
函数 中有return item
,item会传递给下一个即将被执行的管道类
- 但若该管道类 中的
-
二:scrapy五大核心部件
五大核心组件
-
引擎(Scrapy)
用来处理整个系统的数据流处理
触发事务(框架核心)
-
调度器(Scheduler)
用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
- 组成部分:
- 过滤器:将请求对象去重
- 队列:将请求对象放到队列
- 组成部分:
-
下载器(Downloader)
用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
-
爬虫(Spiders)
爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
- 作用:
- 产生url,将url封装成请求对象,并手动发送到引擎
- 进行数据解析
- 作用:
-
项目管道(Pipeline)
负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
相互作用:
-
Spider产生url,将url封装成请求对象,并手动发送到引擎
-
引擎将收到的url给调度器,调度器进行对其进行去除并放到队列中
-
调度器从队列中调度请求对象给引擎,引擎再将其给下载器,下载器去互联网下载数据得到
response
,并交给引擎 -
引擎将
response
交给Spider,之后Spider将==response
交给爬虫文件中的parse
方法==,进行数据解析,将解析的数据放到item
中 -
Spider将==
item
==给引擎后再给管道进行持久化存储
scrapy中间件
下载中间件 — UA伪装,代理,爬取动态加载数据
位置:在引擎和下载器之间的为下载中间件(Downloader Middlewares)
作用:批量拦截到整个工作中所以 请求 和 响应
-
拦截请求
- 拦截请求对象后对其进行UA伪装:可以实现对每个请求对象实现不同的UA伪装
- 代理IP设定
在
middlewares.py
下的MiddleproDownloaderMiddleware类
- UA伪装:
process_request函数
- 代理IP:
process_exception函数
,注意修改完该函数后要return request
进行重新发送请求
-
拦截相应
- 篡改相应数据,相应对象:
process_response
— 进行爬取动态数据
- 篡改相应数据,相应对象:
==下载中间件代码:==在middlewares.py
下的MiddleproDownloaderMiddleware类
UA伪装
-
process_request(self, request, spider)
:拦截请求,修改请求对应请求载体请求标识- 可以进行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 " "(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" ] # 很多UA #拦截请求 def process_request(self, request, spider): #UA伪装 request.headers['User-Agent'] = random.choice(self.user_agent_list) # 拦截所有请求,进行随机UA伪装 #为了验证代理的操作是否生效 request.meta['proxy'] = 'http://183.146.213.198:80' return None
爬取动态加载数据
-
process_response(self, request, response, spider)
:拦截所有响应对象-
spider参数
是爬虫类实例化的一个对象,因此可以调用spiders文件夹
下自己创建的爬虫项目中的 各个属性 -
一个url对应一个request(请求对象)
一个request对应一个response(响应对象)
-
思路: 使用
selenium
获取动态响应数据,用其 对需要请求的url的响应进行修改。
-
动态加载的数据不能对页面直接请求得到,但可以使用selenium
进行爬取,将爬取的数据修改到获取的响应中
步骤:
-
在
spiders文件夹下的爬虫文件中
记录需要动态请求的url -
实例化一个
selenium对象
在spiders文件夹下
爬虫文件中(不写在
process_response方法
中,因为该方法每得到一个响应对象就调用一次)在爬虫文件的构造方法中实例化一个浏览器对象
from selenium import webdriver # 实例化一个浏览器对象 def __init__(self): self.bro = webdriver.Chrome(executable_path='...')
在
middlewares.py中的process_response
函数中bro.get(request.url) # 对 对应的url进行请求 page_text = bro.page_source # 包含了动态加载的新闻数据
- 在
middlewares.py中的process_response函数中
,使用selenium对象
对需要动态请求的数据爬取
(通过
spider.selenium对象名
进行调用)- 将对应的url得到的新的响应数据进行封装,得到一个新的响应对象
使用函数
new_response = HtmlResponse(url=request.url,body=page_text,encoding='utf-8',request=request)
- url为对应的需要动态访问的url
request.url
- body为得到的动态加载数据,
selenium对象bro.page_source得到
- encoding为编码
- request为对应的请求对象
- 在
from scrapy.http import HtmlResponse # 包含对应头文件
#该方法拦截五大板块对应的 响应对象,进行篡改
def process_response(self, request, response, spider):
# spider参数 是 爬虫类实例化对象, 可以调用`spiders文件夹`下自己创建的爬虫项目中的 各个属性
bro = spider.bro # 获取了在爬虫类中定义的浏览器对象
# 挑选出指定的响应对象进行篡改
# 通过url指定request
# 通过request指定response
if request.url in spider.models_urls: # spider.models_urls 记录需要动态请求的url
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: # 对于不需要动态请求的url,直接返回得到的响应
# response #其他请求对应的响应对象
return response
- 关闭浏览器对象
-
在
spiders文件夹中的爬虫文件中
重写父类方法closed(self,spider)
- 该方法在爬虫结束时进行调
def closed(self,spider): self.bro.quit()
代理
-
process_exception
:拦截发送异常的请求- 代理 — 对不能访问的请求使用代理,使得可以访问
# 请求的url是http类型则使用http的代理 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_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
中取消DOWNLOADER_MIDDLEWARES
三:手动发送请求 — 并实现 请求传参(meta参数)
**适用:**如果爬取解析的数据不在同一张页面中,即要进行保存到item的解析的数据不在一个url中
- 要使用多个数据解析方法进行解析数据 保存到一个item中
例子:
# -*- coding: utf-8 -*-
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=']
# 回调函数接受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() # 实例化一个Item对象
job_name = li.xpath('.//div[@class="info-primary"]/h3/a/div[1]/text()').extract_first()
item['job_name'] = job_name
# print(job_name)
detail_url = 'https://www.zhipin.com' + li.xpath('.//div[@class="info-primary"]/h3/a/@href').extract_first()
# 对详情页发请求获取详情页的页面源码数据
# 手动请求的发送
# 请求传参:meta={},可以将meta字典传递给请求对应的回调函数parse_detail
yield scrapy.Request(detail_url, callback=self.parse_detail, meta={'item': item})
-
对主页面项目进行解析得到项目名并报存到item中,每个项目对应的detail_rul,然后请求detail_url,得到详细说明保存到item中
但由于要获得两个项目所用到的xpath不同,并存在手动请求发送的关系,所以:写了两个函数
parse
与parse_detail
问题:item需要得到这两个函数中分别解析的数据以便持久化存储,所以需要在
parse
函数中进行手动请求发送调用回调函数parse_datail
时,以item作为参数 -
scrapy.Request(detail_url, callback=self.parse_detail, meta={‘item’: item})
callback
参数:表示回调函数,对主动请求的url对应的相应对象进行解析meta
参数:是一个字典,meta={'item': item}
,将meta字典传递给请求对应的回调函数- 回调函数接受item:
item = response.meta['item']
,就可以在回调函数将解析的数据保存到item中
四:基于scrapy爬取图片 — ImagesPipeline类
字符串:只需要基于xpath进行解析且提交管道进行持久化存储
图片:xpath得到图片src属性值,单独对图片地址发起请求获取图片二进制数据
ImagesPipeline类:
- 只需要将img的src的属性值进行解析,提交到管道
- 管道就会对图片的src进行请求发送获取图片的二进制类型的数据,且还会帮我们进行持久化存储。
图片软加载
只有图片移动到可视化区域时,该图片才会被显示出来**(即该图片的软属性会变成真正的属性)**
- 该图片未移动到可视化区域,看到伪属性src2
- 该图片移动到可视化区域,看到伪属性src2变成了src
解决办法:基于scrapy爬取时,没有可视化界面,也就是说所以图片都是基于伪属性src2的,可以爬取该属性得到图片的结果
重:使用的流程
使用流程:
-
数据解析(图片的地址)
-
将存储图片地址的item提交到制定的管道类
-
在管道文件中自定制一个基于ImagesPipeLine的一个管道类
- get_media_request
- file_path
- item_completed
-
在配置文件中:
- 指定图片存储的目录:IMAGES_STORE = ‘./imgs_bobo’
- 指定开启的管道:自定制的管道类
将src属性传到管道中
将得到的src封装到item中
-
要在爬虫文件中包含
items.py
对应的类 -
使用
yield item
将其提交到管道中创建父类是
ImagesPipeline类
的管道类,并重写三个方法get_media_requests
,file_path
,item_completed
get_media_requests
:根据图片地址进行图片数据的请求file_path
:指定图片存储的路径item_completed
:返回item,将item返回下一个执行的管道类
import scrapy
from scrapy.pipelines.images import ImagesPipeline
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 # 返回给下一个即将被执行的管道类
-
items
中属性用Field(),而其继承了字典,所以可以使用item[‘key’]进行赋值 -
对图片url手动请求不需要callback参数,因为得到url对应的相应后不需要调用回调函数进行解析
-
file_path(self, request, response=None, info=None)
request
为请求对象- 该函数要将图片路径进行返回,包括图片名称返回
- 而图片存储的目录
- 在==
settings.py
中加入参数IMAGES_STORE
==
- 在==
写好管道类后一定要在settings.py中修改ITEM_PIPELINES
五:CrawlSpider类 — 全站数据爬取
为Spider的一个子类,用于全站数据爬取 — 将所有页码数据爬取
实现全站数据爬取
-
基于
Spider
,使用手动请求发送 -
基于CrawlSpider进行
基本使用
创建一个工程
scrapy startproject 工程名
执行爬虫工程
scrapy crawl 爬虫文件名
-
可以在settings文件中加==
LOG_LEVEL='ERROR'
==仅错误相关的输出忽略其他错误输出 -
爬虫文件名称是爬虫源文件的唯一标识
创建爬虫文件
**进入工程目录(cd 工程目录)**后
scrapy genspider -t crawl 爬虫文件名 www.xxx.com
在spiders子目录中创建一个爬虫文件,其中url为起始url,文件中:
-
parse_item
:对数据进行解析 -
rules
属性中Rule( LinkExtractor(allow=r'正则'), callback='parse_item', follow=True)
rules
包含一个或多个Rule
对象的集合。每个Rule
对爬取网站的动作定义了特定规则。如果多个Rule
匹配了相同的链接,则根据他们在本属性中被定义的顺序,第一个会被使用。所以:规则的顺序可以决定获取的数据,应该把 精确匹配的规则放在前面,越模糊的规则放在后面。
-
LinkExtractor
链接提取器:根据指定规则(allow=“正则”)进行指定链接的提取 -
Rule
规则解析器:对链接提取器提取到的链接进行指定规则(callback)的解析操作-
对链接提取器提取到的链接会自动的请求,只需将链接提取器结果放入Rule中
-
follow = True
:可以将 链接提取器 继续作用到 连接提取器提取到的链接 所对应的页面中这样可以得到所有的页码对应链接
-
-
作用
- 得到网址中全部的页码对应的链接
- 得到一个页面中所有项目的详情页
import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class TestSpider(CrawlSpider): name = 'test' allowed_domains = ['www.baidu.com'] start_urls = ['http://www.baidu.com/'] # LinkExtractor 链接提取器:根据指定规则(allow="正则")进行指定链接的提取 # Rule 规则解析器:将链接提取器提取到的链接进行指定规则(callback)的解析操作 # 其中参数 follow=True:可以将链接提取器 继续作用到 连接提取器提取到的链接 所对应的页面中 rules = ( Rule( LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True), ) def parse_item(self, response): item = {} #item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get() #item['name'] = response.xpath('//div[@id="name"]').get() #item['description'] = response.xpath('//div[@id="description"]').get() return item
-
爬虫文件的例子
属性rules
中可以==写多个规则解析器 对 对应的链接解析器解析的链接 进行调用 定义的解析函数==
-
如果多个
Rule
匹配了相同的链接,则根据他们在本属性中被定义的顺序,第一个会被使用。所以:规则的顺序可以决定获取的数据,应该把 精确匹配的规则放在前面,越模糊的规则放在后面。
-
即rules仅定义了规则,而对得到的响应就使用这些规则看看是否匹配,匹配则进行调用回调函数进行 解析
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from sunPro.items import SunproItem, DetailItem # 导入两个Item类的包
# 需求:爬取sun网站中的编号,新闻标题,新闻内容,标号
class SunSpider(CrawlSpider):
name = 'sun'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page=']
# LinkExtractor 链接提取器:根据指定规则(allow="正则")进行指定链接的提取
link = LinkExtractor(allow=r'type=4&page=\d+') # 对起始url下 所有页码的url进行提取
link_detail = LinkExtractor(allow=r'question/\d+/\d+\.shtml') # 对某一页面上的所有项目url提取
rules = (
# Rule 规则解析器:将链接提取器提取到的链接进行指定规则(callback)的 解析操作
# follow=True:可以将链接提取器 继续作用到 连接提取器提取到的链接 所对应的页面中
Rule(link, callback='parse_item', follow=True), # 对响应的url发送请求并进行解析
Rule(link_detail, callback='parse_detail')
)
# 如下两个解析方法中是不可以实现请求传参!
# 如法将两个解析方法解析的数据存储到同一个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
问题
如果爬取解析的数据不在同一张页面中,即要进行保存到item的解析的数据不在一个url中
例如:对一个网址上所有页面上的数据解析得到A,并得到对应详情页的数据内容B,将A,B进行存储,这是要使用两个解析函数
法1:如果使用是Spider
,可以进行手动请求发送,这样可以进行传参(使用meta
参数传递Item对象
),让两个解析函数进行传参,让两个解析函数解析的内容保存到一个Item类的对象中
法2:但是使用是CrawlSpider
时,请求是自动发送的,没有办法传递参数
-
所以要使用两个Item类,一个保存解析数据A,一个保存解析数据B
# 一个保存解析数据A ,一个保存数据B class SunproItem(scrapy.Item): # define the fields for your item here like: title = scrapy.Field() new_id = scrapy.Field() class DetailItem(scrapy.Item): new_id = scrapy.Field() content = scrapy.Field()
-
在管道
pipelines.py的函数process_item
中使用item.__class__.__name__
得到定义的 Item类 的类名,根据类名将得到的数据进行保存class SunproPipeline(object): def process_item(self, item, spider): # 如何判定item的类型 --- new_id # 将数据写入数据库时,如何保证数据的一致性 if item.__class__.__name__ == 'DetailItem': print(item['new_id'], item['content']) else: print(item['new_id'], item['title']) return item
-
因为是两个Item类,为了使得两个类的数据进行保存时不破坏一致性,要额外保存一个通用属性,来标记是来自同一页面的信息
例如上面的
new_id
就是为了标记同一页面 -
注意要开启管道:
settings.py
中设置ITEM_PIPELINES
六:分布式爬虫 — scrapy-redis
概念:我们需要搭建一个分布式的机群,让其对一组资源进行分布联合爬取。
作用:提升爬取数据的效率
如何实现分布式?
-
安装一个scrapy-redis的组件
-
原生的scarapy是不可以实现分布式爬虫,必须要让scrapy结合着scrapy-redis组件一起实现分布式爬虫。
-
如果直接使用
scrapy
各个机器的scrapy五大核心部件
没有关联 -
如果调度器可以被共享,则可以进行多个机器分布式爬取
-
调度器(Scheduler)
用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
-
- 如果管道可以被共享,则可以汇总保存数据
-
scrapy-redis组件作用:
- 可以给原生的scrapy框架提供可以被共享的管道和调度器
实现流程
-
创建一个工程
-
创建一个基于CrawlSpider的爬虫文件
-
修改当前的爬虫文件:
-
导包:
from scrapy_redis.spiders import RedisCrawlSpider
-
将
start_urls
和allowed_domains
进行注释(起始的url在客户端进行给出) -
添加一个新属性:
redis_key = '调度器队列名称'
表示可以被共享的调度器队列的名称
-
编写数据解析相关的操作(使用
rules
,解析数据,创建Item对象
,保存到item
) -
将当前爬虫类的父类修改成
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_HOST = '127.0.0.1' # redis远程服务器的ip(修改) REDIS_PORT = 6379
-
-
redis相关操作配置:
-
配置redis的配置文件:
-
linux或者mac:redis.conf
-
windows:redis.windows.conf
-
代开配置文件修改:
-
将
bind 127.0.0.1
进行删除,不去掉的话代表仅运行本机访问服务器 -
关闭保护模式:
protected-mode
yes改为no
-
-
-
结合着配置文件开启redis服务
redis-server 配置文件路径
,配置相关环境变量后可以直接redis-server
-
启动客户端:
redis-cli
-
关闭服务器:
redis-cli shutdown
-
-
执行工程:
scrapy runspider 爬虫文件名称.py
-
向调度器的队列中放入一个起始的url
- 调度器的队列 在redis的客户端中
lpush 调度器队列名称 起始url
- 调度器的队列 在redis的客户端中
-
爬取到的数据存储在了redis的
proName:items
这个数据结构中
七:增量式爬虫
监测网站数据更新的情况,只会爬取更新出来的数据
-
可以使用简单的记录已经爬取过的url来实现
而接下来是为了实现基于服务器redis进行记录,实现增量式爬取
分析
制定起始url
基于CrawSpider获取其他页码链接
基于Rule将其他页码链接请求
对每个页码对应页面的源码解析出详情页url
核心:检测详情页url是否请求过
- 将爬取过的电影详情页url存储
- 可以存储到redis的set集合中(在数据解析时实现)
- 把所有url先放入集合中,如果返回1,则之前没有在集合中,需要进行爬取
- 把所有url先放入集合中,如果返回0,则之前在集合中,不需要进行爬取
- 可以存储到redis的set集合中(在数据解析时实现)
对详情页url进行发起请求,解析
进行持久化存储
代码
在spiders文件夹下
from redis import Redis
# 创建一个redis链接对象
conn = Redis(host='...', port=6379)
# 在解析函数中判断是否之前爬取过详情页的url
def parse_item(self, response):
... # 进行解析得到详情页detail_url
ex = self.conn.sadd('urls', detail_url) # 将detail_url保存到服务器中的集合urls中
# 返回值1 不在集合中,需要进行爬取
# 返回值0 在集合中,不需要进行爬取
if ex == 1:
yield scrapy.Request(url=detail_url, callback=self.parst_detail) # 手动进行请求发送
else:
print('已爬取')
对爬取数据封装的Item对象item进行保存
— 在pipelines.py文件中对应的类中
conn = None
def open_spider(self, spider):
self.comm = spider.conn
def process_item(self, item, spider):
dic = {
'xxx':item['xxx'],
...
}
self.conn.lpush('定义在服务器一个列表名', dic)
return item
- 最后服务器上有一个集合urls,一个自定义的列表用于存储