利用scrapy框架进行数据的爬取
-
首先介绍一下什么是scrapy框架:
爬虫中封装好的一个明星框架。代表性功能:高性能的持久化存储,异步的数据下载,高性能的数据解析,分布式。Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架,非常出名,非常强悍。其内部已经被集成了各种功能(高性能异步下载,队列,分布式,解析,持久化等)。对于框架的学习,重点是要学习其框架的特性、各个功能的用法即可。 -
框架安装
-
环境安装:
- mac或者linux直接pip install scrapy
- windows:
- pip install wheel()读取whl文件
下载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指令,没有报错即表示安装成功!注:我使用的是pycharm,可以直接在settings中安装twisted、pywin32、scrapy
-
基本使用
- 创建一个工程:scrapy startproject xxxpro
- cd xxxpro
- 在spiders子目录中创建一个爬虫文件
- scrapy genspider spidersName www.xxx.com
- 执行工程:
- scrapy crawl spidersName
注: LOG_LEVEL ='ERROR’即可指定日志信息只包含错误信息
Robots协议爬取是换成False
USER-AGENT可自行修改 -
scrapy 数据解析和持久化存储
数据解析见代码
持久化存储:-
基于终端指令:
- 要求:只可以将parse方法的返回值存储到本地的文本文件中
- 注意: 持久化存储对应的文本文件类型只能为:
‘json’, ‘jsonlines’, ‘jl’, ‘csv’, ‘xml’, ‘marshal’, ‘pickle’ - 指令:scrapy crawl FileName -o Filepath
- 好处:简洁高效便捷
- 缺点:局限性比较强(只可以存储到指定文件后缀的本地文件中)
-
基于管道的持久化存储
- 编码流程:
- 数据解析
- 在item类中定义相关属性
- 将解析的数据封装存储到item类型对象中
- 将item类型的对象提交给管道进行持久化存储 yield items
- 在管道类process_item中将其接收到的item对象中存储的数据进行持久化存储操作
- 在配置文件中开启管道
- 好处:
- 通用性强
注:推荐大家使用管道持久化存储。
下面看一个代码,是爬取糗图百科中的段子 - 编码流程:
-
#qiutu.py
import scrapy
from qiutuPro.items import QiutuproItem
class QiutuSpider(scrapy.Spider):
name = 'qiutu'
#allowed_domains = ['www.xxx.com']
start_urls = ['https://www.qiushibaike.com/text/']
#基于终端指令
# def parse(self, response):
# #解析:作者名字和内容
# all_data = []
# div_list = response.xpath('//div[@class="col1 old-style-col1"]/div')
#
# for div in div_list:
# #xpath返回的时列表,但是列表元素一定是Selector类型的对象
# #extract可以将selector对象中data参数存储的字符串提取出来
# author = div.xpath('./div[1]/a[2]/h2/text()')[0].extract()
# #name_1 = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
# #print(author)
# #也可以用上述语句和【0】是一样的意思,适用于列表中只有一个列表元素
# #列表调用了extract之后,则表示将列表中每一个seclector对象中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
# #print(content)
# #break
#基于管道
def parse(self, response):
# 解析:作者名字和内容
div_list = response.xpath('//div[@class="col1 old-style-col1"]/div')
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...')[0].extract()
#author = div.xpath('./div[1]/a[2]/h2/text()').extract_first()
# print(author)
# 也可以用上述语句和【0】是一样的意思,适用于列表中只有一个列表元素
# 列表调用了extract之后,则表示将列表中每一个seclector对象中data参数中存储的字符串提取出来
content = div.xpath('./a[1]/div/span//text()').extract()
# 列表换为字符串
content = ''.join(content)
items = QiutuproItem()
items['author'] = author
items['content']= content
yield items#将items类型对象提交给管道
#items
import scrapy
class QiutuproItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
#pass
author = scrapy.Field()
content= scrapy.Field()
#pipelines.py
import pymysql
class QiutuproPipeline(object):
fp = None
#重写父类的一个方法:该方法只会在开始爬虫是被调用一次
def open_spider(self,spider):
print('开始爬虫。。。')
self.fp=open('./qiutu.txt','w',encoding='utf-8')
#专门用来处理items对象
#该方法可以接受爬虫文件提交过来的item类型对象
#该方法每接受一个item对象就会被调用一次
def process_item(self, item, spider):
author=item['author']
content=item['content']
self.fp.write(author+':'+content+'\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')#db表示数据仓库
def process_item(self,item,spider):
self.cursor = self.conn.cursor()
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()
settings 设置将ROBOTSTXT_OBEY = False,开启管道ITEM_PIPELINES,输入日志信息LOG_LEVEL = ‘ERROR’,只显示错误信息,USER_AGENT修改。
数据库自行创建:
代码行:
create database qiutu;#创建数据库
use qiutu;#切换数据库
create table qiutu(author char(50),content varchar(500));
- 基于spider得全站数据爬取
- 就是将网站中某板块下的全部页码对应的页面数据进行爬取
- 需求:爬取校花网中的照片的名字
- 实现方式:
- 将所有页面的url添加到start_urls列表(不推荐)
- 自行手动进行请求发送(推荐)
- 手动请求发送:
- yield scrapy.Request(url,callback):callback专门用于数据解析
具体见代码:
- yield scrapy.Request(url,callback):callback专门用于数据解析
- 手动请求发送:
#xiaohua.py
import scrapy
from xiaohuaPro.items import XiaohuaproItem
#任务:爬取校花网得名字
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
# 法一:将每一页得链接都放入start_urls,逐一爬取
# 法二:自行手动进行请求发送
def parse(self, response):
li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li')
for li in li_list:
name = li.xpath('./a[2]/text() | ./a[2]/b/text()').extract_first()
items = XiaohuaproItem()#实例化
items['name'] =name
yield items
if self.page_num<=11:
new_url = format(self.url%self.page_num)
self.page_num +=1
#手动发送请求:callback回调函数专门用于数据解析
yield scrapy.Request(url=new_url,callback=self.parse)
#items.py
import scrapy
class XiaohuaproItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
name = scrapy.Field()
import pymysql
class XiaohuaproPipeline(object):
fp =None
def open_spider(self,spider):
print('开始爬虫。。。')
self.fp = open('./xiaohuaname.txt','w',encoding ='utf-8')
def process_item(self, item, spider):
name = item['name']
self.fp.write(name+'\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',db='xiaohua',charset = 'utf8')
def process_item(self,item,spider):
self.cursor = self.conn.cursor()
try:
self.cursor.execute('insert into xiaohuaname values("%s")'%(item['name']))
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()
此例子的核心就是进行手动请求分页爬取。重点掌握分页操作和手动请求发送。
- 五大核心组件
引擎(Scrapy) 用来处理整个系统的数据流处理, 触发事务(框架核心)
调度器(Scheduler) 用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL (抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
下载器(Downloader) 用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
爬虫(Spiders) 爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中 提取出链接,让Scrapy继续抓取下一个页面
项目管道(Pipeline) 负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。 当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
核心组件之间的工作流程如下图:
- 请求传参
- 使用场景:如果爬取解析的数据不在同一张页面中。(深度爬取)
9.图片数据爬取
- 图片数据爬取有专门的管道类ImagesPipeline
- 基于scrapy爬取字符串类型的数据和爬取图片类型的数据有何区别?字符串:只需基于xpath进行解析且提交给管道类进行持久化存储 图片:xpath解析出图片src属性值,单独的对图片地址发起请求获取图片二进制类型的数据
- 需求:爬取站长素材中的高清图片
- ImagesPipeline:
- 只需要img的src属性值进行解析,提交到管道,管道就会对图片的src进行请求发送获取图片的二进制类型的数据,
且帮我们进行持久化存储- 注意使用伪属性
- 使用流程:
- 数据解析(图片的地址)
- 将存储图片地址的item提交到制定的管道类
- 在管道文件中自制定一个基于ImagesPipeLine的一个管道类
from scrapy.pipelines.images import ImagesPipeline- 三个重新定义的父类
- get_media_request
- file_path
- item_completed
- 在配置文件中:
- 指定图片的存储目录:IMAGES_STORE = ‘./xxx’(settings中设置)
- 指定开启的管道:开启基于自制定的管道类
- 三个重新定义的父类
#tupian.py
import scrapy
from tupianpro.items import TupianproItem
class TupianSpider(scrapy.Spider):
name = 'tupian'
#allowed_domains = ['www.xxx.com']
start_urls = ['http://sc.chinaz.com/tupian/']
def parse(self, response):
div_list = response.xpath('//*[@id="container"]/div')
for div in div_list:
#注意:src2为伪属性
src = div.xpath('./div/a/img/@src2').extract_first()
item = TupianproItem()
item['src'] = src
yield item
#print(src)
#items.py
import scrapy
class TupianproItem(scrapy.Item):
# define the fields for your item here like:
src = scrapy.Field()
#pipelines.py
from scrapy.pipelines.images import ImagesPipeline
import scrapy
class imgsPipeLine(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和前面的代码是相似的
10.中间件
- 爬虫中间件:
- 不重要,一般用不到
- 下载中间件
- 作用:我们主要使用下载中间件处理请求,一般会对请求设置随机的User-Agent ,
设置随机的代理。目的在于防止爬取网站的反爬虫策略。
(1)引擎将请求传递给下载器过程中, 下载中间件可以对请求进行一系列处理。
比如设置请求的 User-Agent,设置代理等
(2)在下载器完成将Response传递给引擎中,下载中间件可以对响应进行一系列处理。
比如进行gzip解压等。
- 位置:位于引擎和下载器之间
- 作用:批量拦截到整个工程中所有的请求和响应
- 拦截请求:
- UA伪装:process_request
- UA池:User-Agent池
作用:尽可能多的将scrapy工程中的请求伪装成不同类型的浏览器身份。
操作流程:
1.在下载中间件中拦截请求
2.将拦截到的请求的请求头信息中的UA进行篡改伪装
3.在配置文件中开启下载中间件
- 代理IP:process_exception: return request (必须返回request)
- 拦截响应:
- 篡改响应数据和响应对象
- 需求:爬取网易新闻中的新闻数据(标题和内容)
- 1.通过网易新闻的首页解析出五大板块对应的详情页的url(没有动态加载)
2.每一个板块对应的新闻标题都是动态加载出来的(动态加载)可以自己通过
抓包工具验证是否为动态加载
3.通过解析出的每一条新闻详情页url获取详情页的页面源码,解析出新闻内容
1>. 拦截请求代码示例
简单讲解一个代理IP的代码
#主文件
import scrapy
class MiddleSpider(scrapy.Spider):
name = 'middle'
#allowed_domains = ['www.xxxcom']
start_urls = ['http://www.baidu.com/s?wd=IP']
def parse(self, response):
page_text = response.text #此时获取的是属性而不是方法值,方法值带括号,二进制数据为body
with open('ip.html','w',encoding='utf-8') as fp:
fp.write(page_text)
#下载中间件
import random
#下载中间件
class MiddleproDownloaderMiddleware:
#定义UA池和proxy
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://211.137.52.159:8080'
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):
#代理IP设定
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 #将修正之后的请求对象进行重新的请求发送
主要为process_request和process_exception俩部分关于拦截请求
2>拦截响应(结合selenium)
10中已经介绍了网易新闻的操作流程 这里直接上代码
#主文件
import scrapy
from wangyipro.items import WangyiproItem
from selenium import webdriver
class WangyiSpider(scrapy.Spider):
name = 'wangyi'
#allowed_domains = ['www.wangyi.com']
start_urls = ['https://news.163.com/']
moudles_urls = [] #用来存储五大板块的url
#实例化一个浏览器对象
def __init__(self):
#启动驱动
self.bro = webdriver.Chrome(executable_path='E:\python爬虫\爬虫学习\第七章:selenium动态加载数据处理\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')
alist = [3,4,6,7,8]
for index in alist:
modle_url = li_list[index].xpath('./a/@href').extract_first()
self.moudles_urls.append(modle_url)
#依次对每一个板块对应的页面进行请求
for url in self.moudles_urls:#对每一个详情页进行请求发送
yield scrapy.Request(url,callback=self.parse_modle)
#每一个板块对应的新闻标题和相关内容都是动态加载的
def parse_modle(self,response):#解析每一个板块中对应的新闻标题和新闻详情页的url
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()
item = WangyiproItem()
item['title'] = title
new_detail_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
#print(new_detail_url)
#对详情页的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
import scrapy
class WangyiproItem(scrapy.Item):
# define the fields for your item here like:
title = scrapy.Field()
content = scrapy.Field()
#下载中间件中的相应拦截
def process_response(self, request, response, spider):#spider指爬虫对象,即wangyi.py中
bro = spider.bro#获取了在爬虫类中定义的浏览器对象
#挑选出指定的响应对象进行篡改
#用url指定request,request指定response
if request.url in spider.moudles_urls:
bro.get(request.url) #五个板块对应的url进行请求
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:
return response #其他请求对应的响应对象
#管道持久化存储
import pymysql
class WangyiproPipeline(object):
fp = None
def open_spider(self,spider):
print('开始爬虫。。')
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+':'+content+'\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='wangyi',charset = 'utf8')#db表示数据仓库
def process_item(self,item,spider):
self.cursor = self.conn.cursor()
try:
self.cursor.execute('insert into wangyii values("%s","%s")'%(item["title"],item["content"]))
self.conn.commit()
except Exception as e:
print(e)
self.conn.rollback()
return item
def close_spider(self,spider):
self.conn.close()
这里有一个小问题 我运行后没有关闭游标就可以正常运行,关闭游标后出现一个错误如下:
File "E:\python爬虫\爬虫学习\第八章:scrapy框架\wangyipro\wangyipro\pipelines.py", line 39, in close_spider
self.cursor.close()
AttributeError: 'NoneType' object has no attribute 'close'
希望有大佬可以帮忙看看!
上一篇: 纯javascript,不依赖服务器脚本的SlickSpeed发布
下一篇: web前端学习笔记
推荐阅读
-
使用Python的Scrapy框架十分钟爬取美女图
-
Python利用Scrapy框架爬取豆瓣电影示例
-
Python使用Scrapy爬虫框架全站爬取图片并保存本地的实现代码
-
爬虫(十七):Scrapy框架(四) 对接selenium爬取京东商品数据
-
利用 scrapy-splash 对京东进行模拟点击并进行数据爬取
-
Python scrapy框架爬取瓜子二手车信息数据
-
基于python的scrapy框架爬取豆瓣电影及其可视化
-
scrapy 中如何爬取json数据,并解决加载慢的问题
-
Python每天定时爬取中国天气网,并对数据进行简单的可视化处理,并部署在服务器上
-
【Python Scrapy 爬虫框架】 5、利用 pipelines 和 settings 将爬取数据存储到 MongoDB