python爬虫--scrapy框架
Scrapy
一 介绍
Scrapy简介
1.Scrapy是用纯Python实现一个为了爬取网站数据、提取结构性数据而编写的应用框架,用途非常广泛
2.框架的力量,用户只需要定制开发几个模块就可以轻松的实现一个爬虫,用来抓取网页内容以及各种图片,非常之方便
Scrapy架构图
Scrapy主要包括了以下组件:
1.引擎(Scrapy)
用来处理整个系统的数据流处理, 触发事务(框架核心)
2.调度器(Scheduler)
用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
3.下载器(Downloader)
用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
4.爬虫(Spiders)
爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
5.项目管道(Pipeline)
负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
6.下载器中间件(Downloader Middlewares)
位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应。
7.爬虫中间件(Spider Middlewares)
介于Scrapy引擎和爬虫之间的框架,主要工作是处理蜘蛛的响应输入和请求输出。
8.调度中间件(Scheduler Middewares)
介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。
Scrapy运行流程
1.引擎从调度器中取出一个链接(URL)用于接下来的抓取,包括过滤器和对列,过滤后的url交给对列
2.引擎把URL封装成一个请求(Request)传给下载器
3.下载器把资源下载下来,并封装成应答包(Response)
4.爬虫解析Response
5.解析出实体(Item),则交给引擎,在提交到管道进行进一步的处理(持久化存储处理)
6.解析出的是链接(URL),则把URL交给调度器等待抓取
都会经过引擎进行调度
二 安装
#Windows平台
1、pip3 install wheel #安装后,便支持通过wheel文件安装软件,wheel文件官网:https://www.lfd.uci.edu/~gohlke/pythonlibs
3、pip3 install lxml
4、pip3 install pyopenssl
5、下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/
6、下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
7、执行pip3 install 下载目录\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
8、pip3 install scrapy
#Linux平台
1、pip3 install scrapy
三 命令行工具
介绍
#1 查看帮助
scrapy -h
scrapy <command> -h
#2 有两种命令:其中Project-only必须切到项目文件夹下才能执行,而Global的命令则不需要
Global commands: #全局命令
startproject #创建项目
genspider #创建爬虫程序
# 示例 scrapy genspider baidu www.baidu.com
settings #如果是在项目目录下,则得到的是该项目的配置
runspider #运行一个独立的python文件,不必创建项目
shell #scrapy shell url地址 在交互式调试,如选择器规则正确与否
fetch #独立于程单纯地爬取一个页面,可以拿到请求头
view #下载完毕后直接弹出浏览器,以此可以分辨出哪些数据是ajax请求
version #scrapy version 查看scrapy的版本,scrapy version -v查看scrapy依赖库的版本
Project-only commands: #项目文件下
crawl #运行爬虫,必须创建项目才行,确保配置文件中ROBOTSTXT_OBEY = False
check #检测项目中有无语法错误
list #列出项目中所包含的爬虫名
edit #编辑器,一般不用
parse #scrapy parse url地址 --callback 回调函数 #以此可以验证我们的回调函数是否正确
bench #scrapy bentch压力测试
#3 官网链接
https://docs.scrapy.org/en/latest/topics/commands.html
示例
#1、执行全局命令:请确保不在某个项目的目录下,排除受该项目配置的影响
scrapy startproject MyProject(项目名)
cd MyProject (切换到项目下)
scrapy genspider baidu www.baidu.com #创建爬虫程序
#(baidu)爬虫名 对应相关域名(www.baidu.com可以先随便写个www.xxx.com )
#代表这个爬虫程序只能爬取www.baidu.com 这个域名或者百度的子域名
scrapy settings --get XXX #如果切换到项目目录下,看到的则是该项目的配置
scrapy runspider baidu.py #执行爬虫程序
scrapy shell https://www.baidu.com
response
response.status
response.body
view(response)
scrapy view https://www.taobao.com #如果页面显示内容不全,不全的内容则是ajax请求实现的,以此快速定位问题
scrapy fetch --nolog --headers https://www.taobao.com
scrapy version #scrapy的版本
scrapy version -v #依赖库的版本
#2、执行项目命令:切到项目目录下
scrapy crawl baidu
scrapy check
scrapy list
scrapy parse http://quotes.toscrape.com/ --callback parse
scrapy bench
四 项目结构以及爬虫应用简介
目录结构
project_name/
scrapy.cfg
project_name/
__init__.py
items.py
pipelines.py
settings.py
spiders/
__init__.py
爬虫1.py
爬虫2.py
爬虫3.py
应用说明
scrapy.cfg:爬虫项目的配置文件。
__init__.py:爬虫项目的初始化文件,用来对项目做初始化工作。
items.py:爬虫项目的数据容器文件,用来定义要获取的数据。
pipelines.py:爬虫项目的管道文件,用来对items中的数据进行进一步的加工处理。
settings.py:爬虫项目的设置文件,包含了爬虫项目的设置信息。
middlewares.py:爬虫项目的中间件文件,
pycharm中运行爬虫程序
在项目目录先创建entrypoint.py文件,文件名不能变
from scrapy.cmdline import execute
execute(['scrapy','crawl','baidu','--nolog']) #百度为爬虫名,列表的前两项不变 --nolog可写可不写,作用是不在打印其他配置项,只打印需要的内容
五 Spiders
1.介绍
1.Spider是由一系列类(定义了一个网址一组网址将别爬取)组成,具体包括了如何执行爬取任务并且如何从页面中提取结构化的数据
2.Spider是你为了一个特定的网址或一组网址自定义爬取或解析页面行为的地方
2.Spider会循环做的事情
#1、生成初始的Requests来爬取第一个URLS,并且标识一个回调函数
第一个请求定义在start_requests()方法内默认从start_urls列表中获得url地址来生成Request请求,默认的回调函数是parse方法。回调函数在下载完成返回response时自动触发
#2、在回调函数中,解析response并且返回值
返回值可以4种:
包含解析数据的字典
Item对象
新的Request对象(新的Requests也需要指定一个回调函数)
或者是可迭代对象(包含Items或Request)
#3、在回调函数中解析页面内容
通常使用Scrapy自带的Selectors,但很明显你也可以使用Beutifulsoup,lxml或其他你爱用啥用啥。
#4、最后,针对返回的Items对象将会被持久化到数据库
通过Item Pipeline组件存到数据库:https://docs.scrapy.org/en/latest/topics/item-pipeline.html#topics-item-pipeline)
或者导出到不同的文件(通过Feed exports:https://docs.scrapy.org/en/latest/topics/feed-exports.html#topics-feed-exports)
3.spiders提供的类
from scrapy.spiders import Spider,XMLFeedSpider,CSVFeedSpider,SitemapSpider
导入使用
class BaiduSpider(scrapy.Spider):
name = 'baidu' #爬虫名!!!唯一的!!标识爬虫程序
allowed_domains = ['www.baidu.com'] #会对爬取的url有限制,可以注释掉
start_urls = ['http://www.baidu.com/']
def parse(self, response):
pass
4.定制scrapy.spider属性与方法
#1、name = 'amazon'
定义爬虫名,scrapy会根据该值定位爬虫程序
所以它必须要有且必须唯一(In Python 2 this must be ASCII only.)
#2、allowed_domains = ['www.amazon.cn']
定义允许爬取的域名,如果OffsiteMiddleware启动(默认就启动),
那么不属于该列表的域名及其子域名都不允许爬取
如果爬取的网址为:https://www.example.com/1.html,那就添加'example.com'到列表.
#3、start_urls = ['http://www.amazon.cn/']
如果没有指定url,就从该列表中读取url来生成第一个请求
#4、custom_settings
值为一个字典,定义一些配置信息,在运行爬虫程序时,这些配置会覆盖项目级别的配置
所以custom_settings必须被定义成一个类属性,由于settings会在类实例化前被加载
例如
custom_settings = {
'BOT_NAME' : 'Spider_baidu',
'REQUEST_HEADERS' : {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
}
}
LOG_LEVEL='ERROR' #错误信息,报错信息,就不用写--nolog
#5、settings
通过self.settings['配置项的名字']可以访问settings.py中的配置,如果自己定义了custom_settings还是以自己的为准
#6、logger
日志名默认为spider的名字
self.logger.debug('=============>%s' %self.settings['BOT_NAME'])
#5、crawler:了解
该属性必须被定义到类方法from_crawler中
#7、start_requests()
该方法用来发起第一个Requests请求,且必须返回一个可迭代的对象。它在爬虫程序打开时就被Scrapy调用,Scrapy只调用它一次。
默认从start_urls里取出每个url来生成Request(url, dont_filter=True)
def start_urls(self):
scrapy.Request("http://www.example.com/login")
这样写会先执行这个url,之前定义的start_urls=['xxx.xx.xx']就没用了
return [多个scrapy.Request]
#针对参数dont_filter,请看自定义去重规则
如果你想要改变起始爬取的Requests,你就需要覆盖这个方法,例如你想要起始发送一个POST请求,如下
class MySpider(scrapy.Spider):
name = 'myspider'
def start_requests(self):
return [scrapy.FormRequest("http://www.example.com/login",
formdata={'user': 'john', 'pass': 'secret'},
callback=self.logged_in)]
def logged_in(self, response):
pass
#8、parse(response)
这是默认的回调函数,所有的回调函数必须返回an iterable of Request and/or dicts or Item objects.
#10、closed(reason)
爬虫程序结束时自动触发
5.爬取格式
entrypoint.py
from scrapy.cmdline import execute
execute(['scrapy','crawl','amazon1','-a','keywords=iphone8手机','--nolog'])
加参数的格式
# -*- coding: utf-8 -*-
import scrapy
from urllib.parse import urlencode
class Amazon1Spider(scrapy.Spider):
name = 'amazon1'
allowed_domains = ['www.amazon.cn'] #也可以不用,写的话就会对请求的url进行限制,下边的start_urls就只能请求allowed_domains中url
start_urls = ['https://www.amazon.cn/'] #可以放多个请求的url
#不写allowed_domains,就可以在start_urls请求列表中写多个url
# 自定义的配置,可以加请求头系列的配置,先从这里找,没有去settings中找
custom_settings = {
'REQUEST_HEADERS':{
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36',
}
}
# 规定外部传进来的参数
def __init__(self,keywords,*args,**kwargs):
super(Amazon1Spider,self).__init__(*args,**kwargs)
self.keywords=keywords
# 发送请求
def start_requests(self):
url = 'https://www.amazon.cn/s?%s&ref=nb_sb_noss_1' %(urlencode({'k':self.keywords}))
yield scrapy.Request(url=url,
callback=self.parse,
dont_filter=True
) #请求方式GET
#post请求 : scrapy.FormRequest(url,formdata=data,callback)
# 解析
def parse(self, response):
detail_url = response.xpath('//*[@id="search"]/div[1]/div[2]/div/span[4]/div[1]/div[1]/div/span/div/div/div[2]/div[3]/div/div/h2/a/@href') #如果这里得到多个url,商品详情url求,通过for循环再次对详情的url返送请求,每次返送请求必须有对应的回调函数
for url in detail_url:
yiled yield scrapy.Request(url=url,
callback=self.parse_detail
dont_filter=True
)
print(detail_url)
def parse_detail(self,response):
print(response) #详情页的响应结果
def close(spider, reason): #解析之后执行close
print('结束')
6.示例
爬取三国演义的文章标题以及每篇文章的内容
from scrapy.cmdline import execute
execute(['scrapy','crawl','sang','--nolog'])
# -*- coding: utf-8 -*-
import scrapy
class SangSpider(scrapy.Spider):
name = 'sang'
allowed_domains = ['www.shicimingju.com/book/sanguoyanyi.html']
start_urls = ['http://www.shicimingju.com/book/sanguoyanyi.html/']
custom_settings = { #加一些请求头信息
'REQUEST_HEADERS':{
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36',
}
}
def start_requests(self):
url = 'http://www.shicimingju.com/book/sanguoyanyi.html/'
yield scrapy.Request(url=url,
callback=self.parse,#解析主页面的回调函数
) #向主页面发起请求
def parse(self, response): #解析主页面的响应信息
all_urls = response.xpath('/html/body/div[4]/div[2]/div[1]/div/div[4]/ul/li/a/@href').extract()#所有的文章的url
all_title = response.xpath('/html/body/div[4]/div[2]/div[1]/div/div[4]/ul/li/a/text()').extract()#所有标题
for url in all_urls:
# print(response.urljoin(url)) #拼接url
detail_urls = 'http://www.shicimingju.com'+url
print(detail_urls)
yield scrapy.Request(url=detail_urls,
callback=self.parse_detail,#解析详情页面的回调函数
dont_filter=True
) #向详细文章发送请求
def parse_detail(self,response):
content = response.xpath('/html/body/div[4]/div[2]/div[1]/div[1]/div/p/text()') #解析详情文章的响应
print(content)
7.数据解析
- scrapy中封装的xpath的方式进行数据解析。
- scrapy中的xpath和etree中的xpath的区别是什么?
etree中的xpath返回的是字符串或列表
scrapy的xpath进行数据解析后返回的列表元素为Selector对象,然后必须通过extract或者extract_first这两个方法将Selector对象中的对应数据取出
extract_first:将列表元素中第0个selector对象提取
extract:取出列表中的每一个selector对象提取
8.数据持久化存储
基于终端的指令存储
特性:只能将parse方法的返回值存储到本地的磁盘文件中
指令:scrapy crawl 爬虫程序名 -o 文件名
局限性:只能存储到磁盘中,存储的文件名有限制,只能用提供的文件名
示例
# 爬取糗事百科笑话的标题和内容
import scrapy
class QiubSpider(scrapy.Spider):
name = 'qiub'
start_urls = ['http://www.lovehhy.net/Joke/Detail/QSBK/']
def parse(self, response):
all_data = []
#response.xpath拿到的是个列表里面是Selector对象
title = response.xpath('//*[@id="footzoon"]/h3/a/text()').extract()
content = response.xpath('//*[@id="endtext"]//text()').extract() #因为在内容中有对个br标签进行分割,所以用//text()
title=''.join(title)
content=''.join(content)
dic = {
'title':title,
'content':content
}
all_data.append(dic)
return all_data #必须有返回值才能用基于终端指令的存储
终端存储指令:scrapy crawl qiub -o qiubai.csv(csv是提供的文件类型)
基于管道存储
实现流程
- 基于管道:实现流程
1.数据解析
2.在item类中定义相关的属性
3.将解析的数据存储或者封装到一个item类型的对象(items文件中对应类的对象)
4.向管道提交item
5.在管道文件的process_item方法中接收item进行持久化存储
6.在配置文件中开启管道
管道只能处理item类型的对象
文件形式
示例
qiub.py
# -*- coding: utf-8 -*-
import scrapy
from qiubai.items import QiubaiItem
class QiubSpider(scrapy.Spider):
name = 'qiub'
start_urls = ['http://www.lovehhy.net/Joke/Detail/QSBK/']
def parse(self, response):
all_data = []
title = response.xpath('//*[@id="footzoon"]/h3/a/text()').extract()
content = response.xpath('//*[@id="endtext"]//text()').extract()
title=''.join(title)
content=''.join(content)
dic = {
'title':title,
'content':content
}
all_data.append(dic)
item = QiubaiItem() #实例化一个item对象
item['title'] = title #封装好的数据结构
item['content'] = content
yield item #向管道提交item,提交给优先级最高的管道类
items.py
import scrapy
class QiubaiItem(scrapy.Item):
# define the fields for your item here like:
title = scrapy.Field() #Fieid可以接受任意类型的数据格式/类型
content = scrapy.Field()
pipelines.py
class QiubaiPipeline(object):
f= None
print('开始爬虫.....')
def open_spider(self,spider): # 重写父类方法,开启文件
self.f = open('qiubai.txt','w',encoding='utf-8')
def close_spider(self,spider):# 重写父类方法,关闭文件
print('结束爬虫')
self.f.close()
def process_item(self, item, spider):
#item是管道提交过来的item对象
title = item['title'] #取值
content = item['content']
self.f.write(title+':'+content+'\n') #写入文件
return item
settings.py
# 开启管道,可以开启多个管道
ITEM_PIPELINES = {
'qiubai.pipelines.QiubaiPipeline': 300, #300表示的优先级
}
将同一份数据持久化到不同的平台
1.管道文件中的一个管道类负责数据的一种形式的持久化存储
2.爬虫文件向管道提交的item只会提交给优先级最高的那一个管道类
3.在管道类的process_item中的return item表示的是将当前管道接收的item返回/提交给下一个即将被执行的管道类
数据库(mysql)
示例
import pymysql
# 负责将数据存储到mysql
class MysqlPL(object):
conn = None
cursor = None
def open_spider(self, spider):
self.conn = pymysql.Connect(host='127.0.0.1', port=3306, user='root', password='123', db='spider',charset='utf8')
print(self.conn)
def process_item(self, item, spider):
author = item['author']
content = item['content']
sql = 'insert into qiubai values ("%s","%s")' % (author, content)
self.cursor = self.conn.cursor()
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()
数据库(mongodb)
示例
#一:可以写多个Pipeline类
#1、如果优先级高的Pipeline的process_item返回一个值或者None,会自动传给下一个pipline的process_item,
#2、如果只想让第一个Pipeline执行,那得让第一个pipline的process_item抛出异常raise DropItem()
#3、可以用spider.name == '爬虫名' 来控制哪些爬虫用哪些pipeline
二:示范
from scrapy.exceptions import DropItem
class CustomPipeline(object):
def __init__(self,v):
self.value = v
@classmethod
def from_crawler(cls, crawler):
"""
Scrapy会先通过getattr判断我们是否自定义了from_crawler,有则调它来完,加载配置项
成实例化
"""
val = crawler.settings.getint('MMMM')
return cls(val)
def open_spider(self,spider):
"""
爬虫刚启动时执行一次
"""
print('000000')
def close_spider(self,spider):
"""
爬虫关闭时执行一次
"""
print('111111')
def process_item(self, item, spider):
# 操作并进行持久化
# return表示会被后续的pipeline继续处理
return item
# 表示将item丢弃,不会被后续pipeline处理
# raise DropItem()
#1、settings.py
HOST="127.0.0.1"
PORT=27017
USER="root"
PWD="123"
DB="amazon"
TABLE="goods"
ITEM_PIPELINES = {
'Amazon.pipelines.CustomPipeline': 200,
}
#2、pipelines.py
class CustomPipeline(object):
def __init__(self,host,port,user,pwd,db,table):
self.host=host
self.port=port
self.user=user
self.pwd=pwd
self.db=db
self.table=table
@classmethod
def from_crawler(cls, crawler):
"""
Scrapy会先通过getattr判断我们是否自定义了from_crawler,有则调它来完
成实例化
"""
HOST = crawler.settings.get('HOST')
PORT = crawler.settings.get('PORT')
USER = crawler.settings.get('USER')
PWD = crawler.settings.get('PWD')
DB = crawler.settings.get('DB')
TABLE = crawler.settings.get('TABLE')
return cls(HOST,PORT,USER,PWD,DB,TABLE)
def open_spider(self,spider):
"""
爬虫刚启动时执行一次
"""
self.client = MongoClient('mongodb://%s:%s@%s:%s' %(self.user,self.pwd,self.host,self.port))
def close_spider(self,spider):
"""
爬虫关闭时执行一次
"""
self.client.close()
def process_item(self, item, spider):
# 操作并进行持久化
self.client[self.db][self.table].save(dict(item))
9.爬取校花网图片
ImagesPipeline的简介
专门爬取图片的管道
1.爬取一个Item,将图片的URLs放入image_urls字段
2.从Spider返回的Item,传递到Item Pipeline
3.当Item传递到ImagePipeline,将调用Scrapy 调度器和下载器完成4.image_urls中的url的调度和下载。
5.图片下载成功结束后,图片下载路径、url和校验和等信息会被填充到images字段中。
ImagesPipeline的使用:
from scrapy.pipelines.images import ImagesPipeline
import scrapy
# 通过重写父类方法
class SpiderImgPipeline(ImagesPipeline):
# 对某一个媒体资源进行请求发送
# item是提交过来的item(src)
def get_media_requests(self, item, info):
yield scrapy.Request(item['src'])
# 制定媒体数据存储的名称
def file_path(self, request, response=None, info=None):
img_name = request.url.split('/')[-1]
print(img_name+'正在爬取')
return img_name
# 将item传递个下一个即将被执行的管道类
def item_completed(self, results, item, info):
return item
# 在配置中添加图片的存储路径
IMAGES_STORE = './imgslib'
爬取示例:
settings.py
IMAGES_STORE = './imgslib' #存储爬取到的图片
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36'
ROBOTSTXT_OBEY = False
LOG_LEVEL = 'ERROR'
ITEM_PIPELINES = {
'spider_img.pipelines.SpiderImgPipeline': 300,
}
img.py
# -*- coding: utf-8 -*-
import scrapy
from spider_img.items import SpiderImgItem
class ImgSpider(scrapy.Spider):
name = 'img'
start_urls = ['http://www.521609.com/daxuexiaohua/']
url= 'http://www.521609.com/daxuexiaohua/list3%d.html' #通用的url模板,需要加page页数,每个页数的模板
page_num = 1
def parse(self, response):
li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li')
for li in li_list:
img_src = 'http://www.521609.com/' + li.xpath('./a[1]/img/@src').extract_first() #获取的所有的图片的url,extract_first获取的字符串类型,extract获取的是list,里面存了selector对象
item = SpiderImgItem() #实例化item对象
item['src'] = img_src #把数据添加到item中
yield item #提交到item
if self.page_num < 3: #爬取的前两页
self.page_num += 1
new_url = format(self.url%self.page_num) #其他页数的url
yield scrapy.Request(new_url,callback=self.parse) #对页数发请求
items.py
import scrapy
class SpiderImgItem(scrapy.Item):
src = scrapy.Field()
pass
piplines.py
from scrapy.pipelines.images import ImagesPipeline
import scrapy
class SpiderImgPipeline(ImagesPipeline):
# 对某一个媒体资源进行请求发送
# item是提交过来的item(src)
def get_media_requests(self, item, info):
yield scrapy.Request(item['src'])
# 制定媒体数据存储的名称
def file_path(self, request, response=None, info=None):
img_name = request.url.split('/')[-1]
print(img_name+'正在爬取')
return img_name
# 将item传递个下一个即将被执行的管道类
def item_completed(self, results, item, info):
return item
10.请求传参(深度爬取)
示例:爬取4567tv电影网的电影信息
movie.py
# 需求:爬取电影网的电影名称和电影的简介
# 分析:去电影网的首页请求会看到所有的电影,但不能获取简介,需要现获取所有的电影的url,在对每个电影的url反请求,对电影详情发请求获取简介
import scrapy
from movie_spider.items import MovieSpiderItem
class MovieSpider(scrapy.Spider):
name = 'movie'
start_urls = ['https://www.4567tv.tv/index.php/vod/show/class/%E5%8A%A8%E4%BD%9C/id/1.html']
page_num = 1
def parse(self, response):
movie_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li')
for movie_li in movie_list:
movie_title = movie_li.xpath('./div[1]/a/@title').extract_first()
movie_url = 'https://www.4567tv.tv'+movie_li.xpath('./div[1]/a/@href').extract_first()
item = MovieSpiderItem()
item['movie_title'] = movie_title
yield scrapy.Request(movie_url,callback=self.parse_movie_detail,meta={'item':item})#请求传参
if self.page_num < 5:
self.page_num += 1
new_url = f'https://www.4567tv.tv/index.php/vod/show/class/%E5%8A%A8%E4%BD%9C/id/1/page/{self.page_num}.html'
yield scrapy.Request(url=new_url,callback=self.parse,meta={'item':item})
def parse_movie_detail(self,response):
item = response.meta['item']#取出item,接收
movie_about = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/span[2]/text()').extract_first()
item['movie_about'] = movie_about #电影的简介
yield item
# 深度爬取(传参):在电影首页获取不到电影的简介,在最后提交item时,电影的标题和电影的简介在不同的解析回调函数中,所以先把标题放在item中(但是不提交),传递给下一个回调函数,继续用item,向item中存入要提交的数据,!!!
# 为什么不能设置为全局?
# 因为在最后提交yield item时会产生数据覆盖现象
item['movie_title'] = movie_title #存
meta={'item':item} # meta参数传递
item = response.meta['item'] #取
items.py
import scrapy
class MovieSpiderItem(scrapy.Item):
# define the fields for your item here like:
movie_title = scrapy.Field()
movie_about = scrapy.Field()
pass
pipelines.py
import pymysql
class MovieSpiderPipeline(object):
conn = None
cursor = None
def open_spider(self, spider):
self.conn = pymysql.Connect(host='127.0.0.1', port=3306, user='root', password='123', db='movie_spider',charset='utf8')
def process_item(self, item, spider):
movie_title = item['movie_title']
movie_about = item['movie_about']
sql = 'insert into movie_info values ("%s","%s")' % (movie_title, movie_about)
self.cursor = self.conn.cursor()
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()
11.scrapy中间件的应用
下载中间件
-
作用:批量拦截请求和响应
-
拦截请求:
1.UA伪装
2.代理操作
-
拦截响应:
1.篡改响应数据,篡改不满足需求的响应对象,例如有动态加载的数据,直接请求是请求不到的
2.直接更换响应对象
- process_request request通过下载中间件时,该方法被调用
- process_response 下载结果经过中间件时被此方法处理
- process_exception 下载过程中出现异常时被调用
# process_request(request, spider) :
当每个request通过下载中间件的时候, 该方法被调用, 该方法必须返回以下三种中的任意一种:
1. None: Scrapy将继续处理该request, 执行其他的中间件的响应方法, 知道何时的下载器处理函数(download handler)被调用, 该request被执行(其resposne被下载)
2. Response对象: Scrapy将不会调用人其他的process_request()或者process_exception()方法, 或者响应的下载函数; 其将返回response. 已安装的中间件的process_response()方法胡子爱每个res[onse返回时被调用
3. Request对象或raise异常:
返回Request对象时: Scrapy停止调用process_request方法并重新调度返回的request. 当新的request被执行之后, 相应的中间件将会根据下载的response被调用.
当raises异常时: 安装的下载中间件的process_exception()方法会被调用. 如果没有任何一个方法处理该异常, 则request的errback(Request.errback)方法被调用. 如果没有代码处理抛出的异常, 则该异常被忽略且不被记录.
# process_response(request, response, spider) :
process_response的返回值也是有三种:
1. response对象: 如果返回的是一个Resopnse(可以与传入的response相同, 也可以是全新的对象), 该response会被链中的其他中间件的process_response()方法处理.
2. Request对象: 如果其返回一个Request对象, 则中间件链停止, 返回的request会被重新调度下载. 处理类似于process_request()返回request所做的那样.
3. raiseu异常: 如果其抛出一个lgnoreRequest异常, 则调用request的errback(Request.errback). 如果没有代码处理抛出的异常, 则该异常被忽略且不记录.
process_exception(request, exception, spider) :
当下载处理器(downloader handler)或者process_request()(下载中间件)抛出异常(包括lgnoreRequest异常)时, Scrapy调用process_exception().
# process_exception()也是返回三者中的一个:
1. 返回None: Scrapy将会继续处理该异常, 接着调用已安装的其他中间件的process_exception()方法,知道所有的中间件都被调用完毕, 则调用默认的异常处理.
2. 返回Response: 已安装的中间件链的process_response()方法被调用. Scrapy将不会调用任何其他中间件的process_exception()方法.
3. 返回一个Request对象: 返回的额request将会被重新调用下载. 浙江停止中间件的process_exception()方法的执行, 就如返回一个response那样. 相当于如果失败了可以在这里进行一次失败的重试, 例如当访问一个网站出现因为频繁爬取被封ip就可以在这里设置增加代理继续访问.
爬虫中间件
案例
爬虫程序
# -*- coding: utf-8 -*-
import scrapy
import requests
from wangyi.items import WangyiItem
from selenium import webdriver
class WySpider(scrapy.Spider):
name = 'wy'
start_urls = ['https://news.163.com/']
un_url = []
bro = webdriver.Chrome(executable_path=r'C:\pycahrm文件\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')
model_indexs = [3, 4, 6, 7, 8]
for index in model_indexs:
li_tag = li_list[index]
# 解析出了每一个板块对应的url
model_url = li_tag.xpath('./a/@href').extract_first()
self.un_url.append(model_url) # 里面的内容是动态生成的,所以是不满足条件的url,需要在中间件中进行进一步处理
yield scrapy.Request(model_url,callback=self.news_parse)
def news_parse(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:
title = div.xpath('./div/div[1]/h3/a/text()').extract_first()
# title = div.xpath('./div/div/div[1]/h3/a/text()').extract_first()
news_url = div.xpath('./div/div[1]/h3/a/@href').extract_first()
item = WangyiItem()
#对得到结果进行去除空格换行
item['title'] = title.replace('\n','')
item['title'] = title.replace('\t','')
item['title'] = title.replace(' ','')
item['title'] = title.replace('\t\n','')
yield scrapy.Request(news_url,callback=self.detail_parse,meta={'item':item})
def detail_parse(self,response):
item = response.meta['item']
content = response.xpath('//*[@id="endText"]//text()').extract()
content = ''.join(content)
#对得到结果进行去除空格换行
item['content'] = content.replace('\n','')
item['content'] = content.replace('\t','')
item['content'] = content.replace(' ','')
item['content'] = content.replace('\t\n','')
item['content'] = content.replace(' \n','')
yield item
下载中间件
from scrapy import signals
from scrapy.http import HtmlResponse
import time
class WangyiDownloaderMiddleware(object):
def process_request(self, request, spider):
return None
def process_response(self, request, response, spider):
if request.url in spider.un_url:
spider.bro.get(request.url)
time.sleep(3)
spider.bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
time.sleep(2)
spider.bro.execute_script('window.scrollTo(0,document.body.scrollHeight)')
time.sleep(2)
page_text = spider.bro.page_source
# 新的响应的构造方法
new_response = HtmlResponse(url=request.url,body=page_text,encoding='utf-8',request=request)
return new_response #拦截响应后并篡改后,并重新发送新的响应对象,响应的构造方法
else:
return response
def process_exception(self, request, exception, spider):
pass
12.CrawlSpider(全站数据爬取)
创建一个基于CrawlSpider的爬虫文件:scrapy genspider -t crawl sun www.xxx.com
CrawlSpider使用
爬取阳光热线问政平台所有页码中的内容标题以及状态http://wz.sun0769.com/html/top/report.shtml
阳光问政平台共151711条记录,好几百页,实现这几百页的数据爬取
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor #链接提取器
from scrapy.spiders import CrawlSpider, Rule# rule规则解析器
# 链接提取器:提取链接,可以根据指定的规则进行指定的连链接的提取
# 提取规则:allow='正则表达式'
# 规则解析器:获取链接提取到的链接,然后对其进行请求发送,根据指定的规则(callback)对请求的页面源码数据进行数据解析
class SunSpider(CrawlSpider):
name = 'sun'
start_urls = ['http://wz.sun0769.com/index.php/question/report?page=']
link = LinkExtractor(allow=r'page=\d+')
rules = (
# 实例化一个rule对象,基于linkextractor
Rule(link, callback='parse_item', follow=True),
) # fllow=true 将链接提取器继续作用到链接提取器提取的页码链接所对应 的页面中.如果fllow=Flase,得到的页面数据只是当前页面的显示的页码数据
def parse_item(self, response):
tr_list = response.xpath('/html//div[8]/table[2]//tr')
for tr in tr_list:
title = tr.xpath('./td[3]/a[1]/@title').extract_first()
status = tr.xpath('./td[4]/span/text()').extract_first()
print(title,status)
基于CrawlSpider的深度爬取:爬取所有页码中的详情数据
sun.py
import scrapy
from scrapy.linkextractors import LinkExtractor #链接提取器
from scrapy.spiders import CrawlSpider, Rule# rule规则解析器
from sun_spider.items import SunSpiderItem,SunSpiderItem_second
class SunSpider(CrawlSpider):
name = 'sun'
start_urls = ['http://wz.sun0769.com/index.php/question/report?page=']
# href="http://wz.sun0769.com/html/question/201912/437515.shtml"
link = LinkExtractor(allow=r'page=\d+')
link_detail = LinkExtractor(allow=r'question/\d+/\d+/.shtml')
rules = (
# 实例化一个rule对象,基于linkextractor
Rule(link, callback='parse_item', follow=True),
Rule(link_detail, callback='parse_detail'),
) # fllow=true 将链接提取器继续作用到链接提取器提取的页码链接所对应 的页面中
def parse_item(self, response):
tr_list = response.xpath('/html//div[8]/table[2]//tr')
for tr in tr_list:
title = tr.xpath('./td[3]/a[1]/@title').extract_first()
status = tr.xpath('./td[4]/span/text()').extract_first()
num = tr.xpath('./td[1]/text()').extract_first()
item = SunSpiderItem_second()
item['title'] = title
item['status'] = status
item['num'] = num #用于mysql数据库的条件存储,num就是存储的条件
if num:
yield item
def parse_detail(self,response):
content = response.xpath('/html/body/div[9]/table[2]/tbody/tr[1]/td//text()').extract()
num = response.xpath('/html/body/div[9]/table[1]/tbody/tr/td[2]/span[2]/text()').extract_first()
num = num.split(':')[-1]
if num:
content = ''.join(content)
item = SunSpiderItem()
item['content'] = content
item['num'] = num
yield item
items.py
import scrapy
class SunSpiderItem(scrapy.Item):
# define the fields for your item here like:
content = scrapy.Field()
num = scrapy.Field()
class SunSpiderItem_second(scrapy.Item):
# define the fields for your item here like:
title = scrapy.Field()
status = scrapy.Field()
num = scrapy.Field()
pipelines.py
# -*- 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
import pymysql
class SunSpiderPipeline(object):
conn = None
cursor = None
def open_spider(self, spider):
self.conn = pymysql.Connect(host='127.0.0.1', port=3306, user='root', password='123', db='spider',
charset='utf8')
def process_item(self, item, spider):
if item.__class__.__name__ == 'SunSpiderItem':
content = item['content']
num = item['num']
sql = f'insert into spider_crawl_sun values {(content)} where num={num}'
print(num)
# sql = 'insert into spider_crawl_sun values ("%s") where num=%s'%(content,num)
self.cursor = self.conn.cursor()
try:
self.cursor.execute(sql) # 执行正确的话就提交事务
self.conn.commit()
except Exception as e:
self.conn.rollback()
return item
elif item.__class__.__name__=='SunSpiderItem_second':
title = item['title']
status = item['status']
num = item['num']
sql = 'insert into spider_crawl_sun values ("%s","%s","%s")'%(title,status,num)
print(sql)
self.cursor = self.conn.cursor()
try:
self.cursor.execute(sql) # 执行正确的话就提交事务
self.conn.commit()
except Exception as e:
self.conn.rollback()
return item
def close_spider(self, spider):
self.cursor.close()
self.conn.close()
13.提升scrapy爬取数据的效率
需要将如下五个步骤配置在配置文件中即可
增加并发:
默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改CONCURRENT_REQUESTS = 100值为100,并发设置成了为100。
降低日志级别:
在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:LOG_LEVEL = ‘INFO’
禁止cookie:
如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:COOKIES_ENABLED = False
禁止重试:
对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:RETRY_ENABLED = False
减少下载超时:
如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:DOWNLOAD_TIMEOUT = 10 超时时间为10s