scrapy初识
-
什么是框架?
- 所谓的框架简单通用解释就是就是一个具有很强通用性并且集成了很多功能的项目模板,该模板可被应用在不同的项目需求中。也可被视为是一个项目的半成品。
-
如何学习框架?
- 对于刚接触编程或者初级程序员来讲,对于一个新的框架,只需要掌握该框架的作用及其各个功能的使用和应用即可,对于框架的底层实现和原理,在逐步进阶的过程中在慢慢深入即可。
-
什么是scrapy?
- Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架,非常出名,非常强悍。其内部已经被集成了各种功能(高性能异步下载,队列,分布式,解析,持久化等)。对于框架的学习,重点是要学习其框架的特性、各个功能的用法即可。
scrapy基本使用
-
环境安装:
- linux和mac操作系统:
- 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指令,没有报错即表示安装成功!
- linux和mac操作系统:
-
scrapy使用流程:
- 创建工程:
- scrapy startproject ProName
- 进入工程目录:
- cd ProName
- 创建爬虫文件:
- scrapy genspider spiderName www.xxx.com
- 编写相关操作代码
- 执行工程:
- scrapy crawl spiderName
- 创建工程:
爬虫文件剖析:
import scrapy
class QiubaiSpider(scrapy.Spider):
name = 'qiubai' #应用名称
#允许爬取的域名(如果遇到非该域名的url则爬取不到数据,一般使用的时候把它注释)
allowed_domains = ['https://www.qiushibaike.com/']
#起始爬取的url
start_urls = ['https://www.qiushibaike.com/']
#访问起始URL并获取结果后的回调函数,该函数的response参数就是向起始的url发送请求后,获取的响应对象.该函数返回值必须为可迭代对象或者NUll
def parse(self, response):
print(response.text) #获取字符串类型的响应内容
print(response.body) #获取字节类型的相应内容
配置文件settings.py修改:
#添加如下内容:
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36' #伪装请求载体身份
ROBOTSTXT_OBEY = False #可以忽略或者不遵守robots协议
LOG_LEVEL = 'ERROR' #在终端只显示错误等级的日志
scrapy基于xpath数据解析操作
爬取糗事百科的段子数据
import scrapy
class SecondSpider(scrapy.Spider):
name = 'second'
# allowed_domains = ['www.second.com']
start_urls = ['https://www.qiushibaike.com/text/']
def parse(self, response):
# xpath为response中的方法,可以将xpath表达式直接作用于该函数中
# 解析:作者的名称+段子内容
div_list = response.xpath('//*[@id="content"]/div/div[2]')
'''
[<Selector xpath='//*[@id="content"]/div/div[2]' data='<div class="col1 old-style-col1">\n\n\n\n...'>]
'''
for div in div_list:
# xpath返回的是列表,但是列表元素一定是Selector类型的对象
# extract可以将Selector对象中data参数存储的字符串提取出来
author = div.xpath('.//div[1]/a[2]/h2/text()')[0].extract()
author = author.strip('\n') # 过滤空行
# 列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
content = div.xpath('.//a/div/span//text()').extract()
content = ''.join(content)
content = content.strip('\n') #过滤空行
print(author,content)
scrapy的数据持久化存储
基于终端指令的持久化存储
保证爬虫文件的parse方法中有可迭代类型对象(通常为列表or字典)的返回,该返回值可以通过终端指令的形式写入指定格式的文件中进行持久化操作。
import scrapy
class SecondSpider(scrapy.Spider):
name = 'second'
# allowed_domains = ['www.second.com']
start_urls = ['https://www.qiushibaike.com/text/']
def parse(self, response):
# 解析:作者的名称+段子内容
div_list = response.xpath('//*[@id="content"]/div/div[2]')
'''
[<Selector xpath='//*[@id="content"]/div/div[2]' data='<div class="col1 old-style-col1">\n\n\n\n...'>]
'''
all_data = [] # 存储所有解析到的数据
for div in div_list:
# xpath返回的是列表,但是列表元素一定是Selector类型的对象
# extract可以将Selector对象中data参数存储的字符串提取出来
author = div.xpath('.//div[1]/a[2]/h2/text()')[0].extract()
author = author.strip('\n') # 过滤空行
# 列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
content = div.xpath('.//a/div/span//text()').extract()
content = ''.join(content)
content = content.strip('\n') #过滤空行
# 将解析到的内容封装到字典中
dic = {
'author':author,
'content':content
}
# 将数据存储到all_data这个列表中
all_data.append(dic)
return all_data
输入指令:scrapy crawl second -o ./second.csv即可存储
执行指令:
执行输出指定格式进行存储:将爬取到的数据写入不同格式的文件中进行存储
- scrapy crawl 爬虫名称 -o xxx.json
- scrapy crawl 爬虫名称 -o xxx.xml
- scrapy crawl 爬虫名称 -o xxx.csv
基于终端指令:
----- 要求:只可以将parse方法的返回值存储到本地的文本文件中
----- 注意:持久化存储对应的文本文件的类型只可以为:‘json’, ‘jsonlines’, ‘jl’, ‘csv’, ‘xml’, ‘marshal’, 'pickle
----- 指令:scrapy crawl xxx -o filePath
----- 好处:简介高效便捷
----- 缺点:局限性比较强(数据只可以存储到指定后缀的文本文件中)
基于管道的持久化存储操作
推荐使用这种方式。
scrapy框架中已经为我们专门集成好了高效、便捷的持久化操作功能,我们直接使用即可。要想使用scrapy的持久化操作功能,我们首先来认识如下两个文件:
items.py:数据结构模板文件。定义数据属性。
pipelines.py:管道文件。接收数据(items),进行持久化操作。
持久化流程:
1.爬虫文件爬取到数据后,需要将数据封装到items对象中。
2.使用yield关键字将items对象提交给pipelines管道进行持久化操作。
3.在管道文件中的process_item方法中接收爬虫文件提交过来的item对象,然后编写持久化存储的代码将item对象中存储的数据进行持久化存储
4.settings.py配置文件中开启管道
爬虫文件:second.py
import scrapy
# 导入items.py中的QiutuItem类
from qiutu.items import QiutuItem
class SecondSpider(scrapy.Spider):
name = 'second'
# allowed_domains = ['www.second.com']
start_urls = ['https://www.qiushibaike.com/text/']
def parse(self, response):
# 解析:作者的名称+段子内容
div_list = response.xpath('//*[@id="content"]/div/div[2]')
'''
[<Selector xpath='//*[@id="content"]/div/div[2]' data='<div class="col1 old-style-col1">\n\n\n\n...'>]
'''
for div in div_list:
# xpath返回的是列表,但是列表元素一定是Selector类型的对象
# extract可以将Selector对象中data参数存储的字符串提取出来
author = div.xpath('.//div[1]/a[2]/h2/text()')[0].extract()
# 列表调用了extract之后,则表示将列表中每一个Selector对象中data对应的字符串提取了出来
author = author.strip('\n') #过滤空行
content = div.xpath('.//a/div/span//text()').extract()
content = ''.join(content) # 将列表变为字符串
content = content.strip('\n') #过滤空行
# 将解析到的数据封装至items对象中
item = QiutuItem() # 实例化 QiutuItem类
item['author'] = author # 将author中的内容添加到item对象的author属性中
item['content'] = content
# 提交item到管道文件(pipelines.py)
yield item
items文件:items.py
import scrapy
class QiutuItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
author = scrapy.Field() #存储作者
content = scrapy.Field() #存储段子内容
管道文件:pipelines.py
class QiutuPipeline(object):
# 定义一个文件描述符属性
fp = None
# 重写父类的一个方法:
# 开始爬虫时,执行一次
def open_spider(self,spider):
print('开始爬虫.......')
self.fp = open('./spider.txt','w',encoding='utf-8')
# 因为该方法会被执行调用多次,所以文件的开启和关闭操作写在了另外两个只会各自执行一次的方法中。
# 专门用来处理item类型对象
# 该方法可以接收爬虫文件提交过来的item对象
# 该方法每接收到一个item就会被调用一次
def process_item(self, item, spider):
author = item['author']
content = item['content']
# 将爬虫程序提交的item进行持久化存储
self.fp.write(author+': \n'+content+'\n')
return item
# 重写父类的一个方法:结束爬虫时,执行一次
def close_spider(self,spider):
print('爬虫结束!')
self.fp.close()
配置文件settings.py:
ITEM_PIPELINES = {
# 300表示为优先级,值越小优先级越高
'qiutu.pipelines.QiutuPipeline': 300,
}
基于管道:
编码流程:
-------- 数据解析
-------- 在item类中定义相关的属性
-------- 将解析的数据封装存储到item类型的对象
-------- 将item类型的对象提交给管道进行持久化存储的操作
-------- 在管道类的process_item中要将其接受到的item对象中存储的数据进行持久化存储操作
-------- 在配置文件中开启管道
----- 好处:
---- 通用性强。
面试题
如果最终需要将爬取到的数据值一份存储到磁盘文件,一份存储到数据库中,则应该如何操作scrapy?
分析:
管道文件中一个管道类对应的是将数据存储到一种平台
爬虫文件提交的item只会给管道文件中第一个被执行的管道类接受
管道文件中process_item中的return item
表示将item传递给下一个即将被执行的管道类。
爬虫文件secend.py和数据结构模板文件items.py还是使用上面的两个文件
修改管道文件:pipelines.py
import pymysql
class QiutuPipeline(object):
# 定义一个文件描述符属性
fp = None
# 重写父类的一个方法:
# 开始爬虫时,执行一次
def open_spider(self,spider):
print('开始爬虫.......')
self.fp = open('./spider.txt','w',encoding='utf-8')
# 因为该方法会被执行调用多次,所以文件的开启和关闭操作写在了另外两个只会各自执行一次的方法中。
# 专门用来处理item类型对象
# 该方法可以接收爬虫文件提交过来的item对象
# 该方法每接收到一个item就会被调用一次
def process_item(self, item, spider):
author = item['author']
content = item['content']
# 将爬虫程序提交的item进行持久化存储
self.fp.write(author+': '+content+'\n')
return item
# 重写父类的一个方法: 结束爬虫时,执行一次
def close_spider(self,spider):
print('爬虫结束!')
self.fp.close()
# 管道文件中一个管道类对应将一组数据存储到一个平台或者载体中
# 模仿上面的写
class MysqlPipeline(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',database='scrapy',charset='utf8')
def process_item(self,item,spider):
# 使用 cursor() 方法创建一个游标对象 cursor
self.cursor = self.conn.cursor()
# 使用 execute() 方法执行 SQL
try:
self.cursor.execute('insert into qiubai values ("%s","%s")' % (item["author"],item["content"]))
# 提交到数据库执行,若没有此操作的话插入数据只是在内存中执行,并不会再数据库中插入
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() # 关闭数据库连接
# 爬虫文件提交的item类型的对象最终会提交给哪一个管道类?
# : 先执行的管道类
修改配置文件settings.py:
ITEM_PIPELINES = {
# 300表示为优先级,值越小优先级越高
'qiutu.pipelines.QiutuPipeline': 300,
'qiutu.pipelines.MysqlPipeline': 301,
}
终端执行:scrapy crawl second
效果:spider.txt文件
数据库:
scrapy基于Spider类的全站数据爬取
- 就是将网站中某板块下的全部页码对应的页面数据进行爬取
- 需求:爬取校花网中的1-6页照片的名称并使用管道进行持久化存储
- 实现方式:
- 将所有页面的url添加到start_urls列表(不推荐)
- 自行手动进行请求发送(推荐)
- 手动请求发送:
- yield scrapy.Request(url,callback):callback专门用做于数据解析
爬虫文件xiaohua.py
# -*- coding: utf-8 -*-
import scrapy
from xiaohuaPro.items import XiaohuaproItem
class XiaohuaSpider(scrapy.Spider):
name = 'xiaohua'
# allowed_domains = ['www.xxxx.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标签
li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li')
for li in li_list:
# 合并两个xpath表达式,有的名称一个xpath表达式拿不到,规律不一样
# 使用extract_first()的前提是:该列表只有一个元素,相当于下面注释的方式
# name = li.xpath('./a[2]/text() | ./a[2]//text() ')[0].extract()
name = li.xpath('./a[2]/text() | ./a[2]//text() ').extract_first()
# 持久化存储操作,实例化items.py中的XiaohuaproItem类得到一个item对象
# 给item对象的name属性添加name中解析到的内容
item = XiaohuaproItem()
item['name'] = name
# 提交item到管道文件(pipelines.py)
yield item
# 递归函数需要截止
if self.page_num <= 6: #一共爬取6页
# 每页的url
new_url = format(self.url%self.page_num)
# 递归爬取数据:callback参数的值为回调函数(将url请求后,得到的相应数据继续进行parse解析),递归调用parse函数
yield scrapy.Request(url=new_url,callback=self.parse)
self.page_num += 1
数据结构模板文件items.py
import scrapy
class XiaohuaproItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
# 存储名字,定义属性name,需要与xiaohua爬虫文件中的item['name']保持一样的名称,即都叫'name'
name = scrapy.Field()
管道文件pipelines.py
class XiaohuaproPipeline(object):
# 构造方法
def __init__(self):
self.fp = None # 定义一个文件描述符属性
# 重写父类的方法:开始爬虫时,执行一次
def open_spider(self,spider):
print('begin crawl.......')
self.fp = open('./xiaohua.txt','w',encoding='utf-8')
# 因为该方法会被执行调用多次,所以文件的开启和关闭操作写在了另外两个只会各自执行一次的方法中。
def process_item(self, item, spider):
name = item['name']
# 将爬虫程序提交的item进行持久化存储
self.fp.write(name+'\n')
return item
# 重写父类的方法: 结束爬虫时,执行一次
def close_spider(self,spider):
print('end crawl!!!')
self.fp.close()
配置文件settings.py
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
# 开启管道
ITEM_PIPELINES = {
# 300表示为优先级,值越小优先级越高
'xiaohuaPro.pipelines.XiaohuaproPipeline': 300,
}
效果:
scrapy五大核心组件简介
scrapy的基本使用我们已经掌握,但是各位心中一定会有些许的疑问,我们在编写scrapy工程的时候,我们只是在定义相关类中的属性或者方法,但是我们并没有手动的对类进行实例化或者手动调用过相关的方法,那么这些操作都是谁做的呢?接下来我们就来看看scrapy的五大核心组件的工作流程,然后大家就会上述的疑问有基本了解了。
下图概述了Scrapy体系结构及其组件,并概述了系统内部发生的数据流(由红色箭头显示)。
- spiders发起请求,经过引擎engine,抵达调度器scheduler。Scrapy中的数据流由执行引擎ENGINE控制.
- 引擎在调度程序中调度请求,并请求下一个要爬网的请求。
- 调度程序将下一个请求返回给引擎。
- 引擎通过下载器中间件将请求发送到下载器downloader
- 页面下载完成后,Downloader会生成一个带有该页面的响应,并通过Downloader中间件将其发送到Engine
- 引擎从下载器接收响应,并将其发送到Spider进行处理,并通过Spider中间件传递
- Spider处理响应,并通过Spider中间件将抓取的项目和新的请求(要遵循)返回给引擎
- 引擎将处理后的项目发送到项目管道,然后将处理后的请求发送到调度程序,并请求爬网的下一个请求。
- 重复该过程(从步骤1开始),直到调度程序不再发出请求为止
- 引擎(Scrapy)
- 用来处理整个系统的数据流处理, 触发事务(框架核心)
- 调度器(Scheduler)
- 用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
- 下载器(Downloader)
- 用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
- 爬虫(Spiders)
- 爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
- 项目管道(Pipeline)
- 负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
请求传参
- 在某些情况下,我们爬取的数据不在同一个页面中,例如,我们爬取一个电影网站,电影的名称,评分在一级页面,而要爬取的其他电影详情在其二级子页面中。这时我们就需要用到请求传参。这里和分页有的类似,分页的url是有规律的,而这里没有,所有要使用请求传参
- 请求传参的使用场景
- 当我们使用爬虫爬取的数据没有存在于同一张页面的时候,则必须使用请求传参
- 代码示例
import scrapy
from bossPro.items import BossproItem
# 回调函数接受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
# print(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})
items.py文件:
import scrapy
class BossproItem(scrapy.Item):
# define the fields for your item here like:
job_name = scrapy.Field()
job_desc = scrapy.Field()
图片数据爬取之ImagesPipeline
- 在scrapy中我们之前爬取的都是基于字符串类型的数据,那么要是基于图片数据的爬取,那又该如何呢?
- 其实在scrapy中已经为我们封装好了一个专门基于图片请求和持久化存储的管道类ImagesPipeline,那也就是说如果想要基于scrapy实现图片数据的爬取,则可以直接使用该管道类即可。
- ImagesPipeline使用流程
- 在配置文件中进行如下配置:
- IMAGES_STORE = ‘./imgs’:表示最终图片存储的目录
- 指定开启的管道
- 在配置文件中进行如下配置:
- 基于scrapy爬取字符串类型的数据和爬取图片类型的数据区别?
- 字符串:只需要基于xpath进行解析且提交管道进行持久化存储
- 图片:xpath解析出图片src的属性值。单独的对图片地址发起请求获取图片二进制类型的数据
- ImagesPipeline:
- 只需要将img的src的属性值进行解析,提交到管道,管道就会对图片的src进行请求发送获取图片的二进制类型的数据,且还会帮我们进行持久化存储。
- 需求:爬取站长素材中的高清图片
- 使用流程:
- 数据解析(图片的地址)
- 将存储图片地址的item提交到制定的管道类
- 在管道文件中自定制一个基于ImagesPipeLine的一个管道类
- get_media_request
- file_path
- item_completed
- 在配置文件中:
- 指定图片存储的目录:IMAGES_STORE = './imgs_bobo'
- 指定开启的管道:自定制的管道类
爬虫文件:img.py
import scrapy
from imgsPro.items import ImgsproItem
class ImgSpider(scrapy.Spider):
name = 'img'
# allowed_domains = ['www.img.com']
start_urls = ['https://sc.chinaz.com/tupian/']
def parse(self, response):
div_list = response.xpath('//div[@id="container"]/div')
for div in div_list:
# 这里拿到的是缩略图,去掉后面的_s及可得到原图,http://scpic1.chinaz.net/Files/pic/pic9/202012/hpic3261_s.jpg
# 注意:使用伪属性@src2,这里网站使用了懒加载反爬虫机制
img_url = 'http:' + div.xpath('./div/a/img/@src2').extract_first()
img_url = img_url.split('_')[0] + '.jpg'
# http://scpic1.chinaz.net/Files/pic/pic9/202012/hpic3261.jpg
# 持久化
item = ImgsproItem()
item['img_url'] = img_url
yield item
数据属性定义文件:items.py
import scrapy
class ImgsproItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
img_url = scrapy.Field()
管道文件:pipelines.py
# class ImgsproPipeline(object):
# def process_item(self, item, spider):
# return item
from scrapy.pipelines.images import ImagesPipeline
import scrapy
# ImagesPipeline专门用于文件下载的管道类,下载过程支持异步和多线程
class ImgsPipeline(ImagesPipeline):
# #对item中的图片进行请求操作
def get_media_requests(self, item, info):
yield scrapy.Request(item['img_url'])
# 指定图片存储的路径
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
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
ITEM_PIPELINES = {
'imgsPro.pipelines.ImgsPipeline': 300,
}
#指定图片存储的目录
IMAGES_STORE = './imgs_zz'
scrapy中间件
中间件
- 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件。
- 作用:主要使用下载中间件处理请求,一般会对请求设置随机的User-Agent ,设置随机的代理。目的在于防止爬取网站的反爬虫策略。
- (1)引擎将请求传递给下载器过程中, 下载中间件可以对请求进行一系列处理。比如设置请求的 User-Agent,设置代理等
- (2)在下载器完成将Response传递给引擎中,下载中间件可以对响应进行一系列处理。比如进行gzip解压等。
UA池:User-Agent池
-
作用:尽可能多的将scrapy工程中的请求伪装成不同类型的浏览器身份。
-
操作流程:
- 1.在下载中间件中拦截请求
- 2.将拦截到的请求的请求头信息中的UA进行篡改伪装
- 3.在配置文件中开启下载中间件
-
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" ]
代理池
-
作用:尽可能多的将scrapy工程中的请求的IP设置成不同的。
-
操作流程:
- 1.在下载中间件中拦截请求
- 2.将拦截到的请求的IP修改成某一代理IP
- 3.在配置文件中开启下载中间件
-
代码展示:
class Proxy(object): #可被选用的代理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): #对拦截到请求的url进行判断(协议头到底是http还是https) #request.url返回值:http://www.xxx.com def process_exception(self, request, exception, spider): 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
拦截请求:
UA伪装:process_request
代理IP:process_exception:return request
需求:爬取百度显示不同ip,使用不同的UA
爬虫文件:middle.py
import scrapy
class MiddleSpider(scrapy.Spider):
name = 'middle'
# allowed_domains = ['www.middle.com']
start_urls = ['https://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
import random
class MiddleproDownloaderMiddleware(object):
# 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"
]
# ip代理池
PROXY_http = [
'58.220.95.42:10174',
'175.43.33.53:9999',
]
PROXY_https = [
'118.212.104.138:9999',
'183.166.71.177:9999',
]
# 拦截请求
def process_request(self, request, spider):
# UA伪装
request.headers['User-Agent'] = random.choice(self.user_agent_list)
# 为了验证代理的操作是否生效
request.meta['proxy'] = 'https://183.166.71.177:66'
return None
# 拦截所有的响应
def process_response(self, request, response, spider):
return response
# 拦截 process_request发生异常的请求
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
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
DOWNLOADER_MIDDLEWARES = {
'middlePro.middlewares.MiddleproDownloaderMiddleware': 543,
}
scrapy中selenium的应用
拦截响应
篡改响应数据,响应对象
需求:爬取网易新闻中的新闻数据(标题和内容)
1.通过网易新闻的首页解析出五大板块对应的详情页的url(没有动态加载)
2.每一个板块对应的新闻标题都是动态加载出来的(动态加载)
3.通过解析出每一条新闻详情页的url获取详情页的页面源码,解析出新闻内容
引入
- 在通过scrapy框架进行某些网站数据爬取的时候,往往会碰到页面动态数据加载的情况发生,如果直接使用scrapy对其url发请求,是绝对获取不到那部分动态加载出来的数据值。但是通过观察我们会发现,通过浏览器进行url请求发送则会加载出对应的动态加载出的数据。那么如果我们想要在scrapy也获取动态加载出的数据,则必须使用selenium创建浏览器对象,然后通过该浏览器对象进行请求发送,获取动态加载的数据值。
案例分析:
- 需求:爬取网易新闻的国内,国际,军事,航空,无人机板块下的新闻数据
- 需求分析:当点击国内超链进入板块下对应的页面时,会发现当前页面展示的新闻数据是被动态加载出来的,如果直接通过程序对url进行请求,是获取不到动态加载出的新闻数据的。则就需要我们使用selenium实例化一个浏览器对象,在该对象中进行url的请求,获取动态加载的新闻数据。
selenium在scrapy中使用的原理分析:
- 当引擎将国内板块url对应的请求提交给下载器后,下载器进行网页数据的下载,然后将下载到的页面数据,封装到response中,提交给引擎,引擎将response在转交给Spiders。Spiders接受到的response对象中存储的页面数据里是没有动态加载的新闻数据的。要想获取动态加载的新闻数据,则需要在下载中间件中对下载器提交给引擎的response响应对象进行拦截,切对其内部存储的页面数据进行篡改,修改成携带了动态加载出的新闻数据,然后将被篡改的response对象最终交给Spiders进行解析操作。
selenium在scrapy中的使用流程:
- 重写爬虫文件的构造方法,在该方法中使用selenium实例化一个浏览器对象(因为浏览器对象只需要被实例化一次)
- 重写爬虫文件的closed(self,spider)方法,在其内部关闭浏览器对象。该方法是在爬虫结束时被调用
- 重写下载中间件的process_response方法,让该方法对响应对象进行拦截,并篡改response中存储的页面数据
- 在配置文件中开启下载中间件
爬虫文件:wangyi.py
# -*- coding: utf-8 -*-
import scrapy
from selenium import webdriver
from wangyiPro.items import WangyiproItem
class WangyiSpider(scrapy.Spider):
name = 'wangyi'
start_urls = ['https://news.163.com/']
models_urls = [] #存储五个板块对应详情页的url
# 解析五大板块对应详情页的url
# 实例化一个浏览器对象
def __init__(self):
self.bro = webdriver.Chrome(executable_path='./chromedriver.exe')
def parse(self, response):
li_list = response.xpath('//*[@id="index2016_wrap"]/div[1]/div[2]/div[2]/div[2]/div[2]/div/ul/li')
list_index = [3, 4, 6, 7, 8]
for li in list_index:
home_title = li_list[li]
son_url = home_title.xpath('./a/@href').extract_first()
self.models_urls.append(son_url)
# 依次对每一个板块对应的页面进行请求
for url in self.models_urls: #对每一个板块的url进行请求发送
yield scrapy.Request(url=url, callback=self.son_page)
# 每一个板块对应的新闻标题相关的内容都是动态加载
def son_page(self, response):
div_list = response.xpath('/html/body/div/div[3]/div[4]/div[1]/div/div/ul/li/div/div')
for div in div_list:
try:
title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
content_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
item = WangyiproItem()
item['title'] = title
# 对新闻详情页的url发起请求
yield scrapy.Request(url=content_url, callback=self.detail_page, meta={'item': item})
raise TypeError('unsupported operand type(s) for +: "NoneType" and "str" OR equest url must be str or unicode_')
except Exception as e:
print(e)
# 解析新闻内容
def detail_page(self, response):
content = response.xpath('//*[@id="content"]/div[2]//text() | //div[@id="endText"]//text()').extract()
content = ''.join(content)
item = response.meta['item']
item['content'] = content
yield item
def closed(self, spider):
self.bro.quit()
中间件文件middlewares.py
from time import sleep
from scrapy.http import HtmlResponse
class WangyiproDownloaderMiddleware(object):
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(2)
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
def process_exception(self, request, exception, spider):
pass
管道文件:
class WangyiproPipeline(object):
def __init__(self):
self.fp = None
def open_spider(self, spider):
self.fp = open('./wangyi.txt', 'w', encoding='utf-8')
def process_item(self, item, spider):
title = item['title']
content = item['content']
self.fp.write(title + ':\r\n' + content + '\r\n')
return item
def close_spider(self, spider):
self.fp.close()
配置文件:
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
DOWNLOADER_MIDDLEWARES = {
'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
}
ITEM_PIPELINES = {
'wangyiPro.pipelines.WangyiproPipeline': 300,
}
数据属性定义文件items.py:
import scrapy
class WangyiproItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
content = scrapy.Field()
全站数据爬取CrawlSpider
- CrawlSpider:类,Spider的一个子类
- 全站数据爬取的方式
- 基于Spider:手动请求
- 基于CrawlSpider
- CrawlSpider的使用:
- 创建一个工程
- cd XXX
- 创建爬虫文件(CrawlSpider):
- scrapy genspider -t crawl xxx www.xxxx.com
- 链接提取器:
- 作用:根据指定的规则(allow)进行指定链接的提取
- 规则解析器:
- 作用:将链接提取器提取到的链接进行指定规则(callback)的解析
#需求:爬取sun网站中的编号,新闻标题,新闻内容,编号
- 分析:爬取的数据没有在同一张页面中。
- 1.可以使用链接提取器提取所有的页码链接
- 2.让链接提取器提取所有的新闻详情页的链接
爬虫文件:sun.py
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from sunPro.items import SunproItem, DetailItem
class SunSpider(CrawlSpider):
name = 'sun'
# allowed_domains = ['www.sun.com']
start_urls = ['http://wz.sun0769.com/political/index/supervise?page=']
# 链接提取器LinkExtractor:根据指定规则(allow="正则")进行指定链接的提取,对上面的链接进行正则解析
link = LinkExtractor(allow=r'page=\d+')
# 对解析到的页面的标题对应的详细页链接进行提取
link_detail = LinkExtractor(allow=r'politics/\w+\?id=\d+')
rules = (
# 规则解析器:将链接提取器提取到的链接进行指定规则(callback)的解析操作
# follow=True:可以将链接提取器 继续作用到 链接提取器提取到的链接 所对应的页面中
Rule(link, callback='parse_item', follow=True),
Rule(link_detail, callback='parse_detail'),
)
#如下两个解析方法中是不可以实现请求传参!
#无法将两个解析方法解析的数据存储到同一个item中,可以以此存储到两个item
def parse_item(self, response):
li_list = response.xpath('/html/body/div[2]/div[3]/ul/li')
for li in li_list:
# 解析新闻编号和新闻的标题,xpath表达式中不可以出现tbody标签
home_num = li.xpath('./span[1]/text()').extract_first()
home_title = li.xpath('./span[3]/a/text()').extract_first()
item = SunproItem()
item['home_num'] = home_num
item['home_title'] = home_title
yield item
# 解析新闻内容和新闻编号
def parse_detail(self, response):
detail_num = response.xpath('/html/body/div[3]/div[2]/div[2]/div[1]/span[4]/text()').extract_first()
# detail_num = detail_num.split(':')[1]
detail_content = response.xpath('/html/body/div[3]/div[2]/div[2]/div[2]//text()').extract()
detail_content = ''.join(detail_content)
item = DetailItem()
item['detail_num'] = detail_num
item['detail_content'] = detail_content
yield item
数据属性定义文件:items.py
class SunproItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
home_num = scrapy.Field()
home_title = scrapy.Field()
# 模仿上面写一个类
class DetailItem(scrapy.Item):
detail_num = scrapy.Field()
detail_content = scrapy.Field()
管道文件pipelines.py
class SunproPipeline(object):
def __init__(self):
self.fp = None
def open_spider(self, spider):
self.fp = open('./sun.txt', 'w', encoding='utf-8')
def process_item(self, item, spider):
if item.__class__.__name__ == 'SunproItem':
self.fp.write(item['home_num'] + '\t' + item['home_title'] + '\n')
else:
self.fp.write(item['detail_num'] + '\t' + item['detail_content'] + '\n')
return item
def close_spider(self, spider):
self.fp.close()
配置文件:settings.py
ROBOTSTXT_OBEY = False
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
LOG_LEVEL = 'ERROR'
ITEM_PIPELINES = {
'sunPro.pipelines.SunproPipeline': 300,
}