自己动手实现爬虫scrapy框架思路汇总
这里先简要温习下爬虫实际操作:
cd ~/Desktop/spider scrapy startproject lastspider # 创建爬虫工程 cd lastspider/ # 进入工程 scrapy genspider github github.cn # 创建scrapy爬虫 scrapy genspider -t crawl gitee gitee.com # 创建crawlspider爬虫 # github========================================== # github.py # -*- coding: utf-8 -*- import scrapy class GithubSpider(scrapy.Spider): name = 'github' allowed_domains = ['github.cn'] start_urls = ['http://github.cn/'] def parse(self, response): # 实现逻辑--简单以大分类-中间分类-小分类-商品列表-商品详情-商品价格为例 # 1. 获取大分类的分组 # 2. 获取和大分类呼应的中间分类组 # 3. 遍历提取大分类组和中间分类组的数据,同时获取小分类组 # 4. 遍历小分类组,提取小分类对应的列表页url,发送列表页请求,callback指向列表页处理函数 # 5. 如果当前分类页有翻页,则提取下一页url继续发送请求,callback指向自己,以便循环读取 # 6. 构建列表页数据处理函数,获取列表页商品的分组,遍历提取各商品列表页数据,包括url # 7. 根据提取的商品url,构建请求,callback指向详情页数据处理函数 # 8. 如果当前商品列表页有翻页,则提取下一页url继续发送请求,callback指向自己,以便循环读取 # 9. 构建商品详情页函数,提取详情页信息,如果其中有数据需要单独发送请求,则再构建请求获取该数据,在最后的请求函数中,需要yield item # 10. 上述最终yield 的item 通过在settings中设置spiderpiplelines,就会进入指定的spiderpiplelines中通过process_item()进行进一步数据处理,比如清洗和保存。 pass # pipelines.py class GithubPipeline: def open_spider(self,spider): #在爬虫开启的时候执行一次 if spider.name == 'github': # 准备好mongodb数据库及集合,以便接收数据 client = MongoClient() # host='127.0.0.1',port=27017 左侧是默认值 self.collection = client["db"]["col"] # self.f = open("a.txt","a") pass def process_item(self, item, spider): if spider.name == 'github': # 数据清洗 item['content'] = self.process_content(item["content"]) # 往数据库集合中添加数据 # 如果item是通过item.py中定义的类实例对象,则不能直接存入mongodb,需要dict(item),如果是字典则可以直接存入。 self.collection.insert_one(item) # pprint(item) return item def process_content(self,content): #处理content字段的数据 # 对数据进行处理,常用方法-正则匹配,字符串切片,替换等待 return content def close_spdier(self,spider): #爬虫关闭的时候执行一次 if spider.name == 'github': # self.f.close() pass #================================================ # 字典推导式 # {keys(i):values(i) for i in list1} # eg: dict1 = {i.split('=')[0]:i.split('=')[1] for i in str_list} # gitee =========================================================== # gitee.py # -*- coding: utf-8 -*- import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class GiteeSpider(CrawlSpider): name = 'gitee' allowed_domains = ['gitee.com'] start_urls = ['http://gitee.com/'] 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 # =============================================
什么是框架,为什么需要开发框架
-
框架:为了解决一类问题而开发的程序,能够提高开发效率
-
第三方的框架不能够满足需求,在特定场景下使用,能够满足特定需求
scrapy_plus中有哪些内置对象和核心模块
-
core
-
engine
-
scheduler
-
downloader
-
pipeline
-
spider
-
-
http
-
request
-
response
-
-
middlewares
-
downloader_middlewares
-
spider_middlewares
-
-
item
scrapy_plus实现引擎的基础逻辑
-
调用爬虫的start_request方法,获取start_request请求对象
-
-
调用爬虫中间件的process_request方法,传入start-request,返回start_request
-
调用调度器的add_request,传入start_request
-
-
调用调度器的get_request方法,获取请求
-
-
调用下载器中间件的process_request,传入请求,返回请求
-
调用下载器的get_response方法,传入请求,返回response
-
-
调用下载器中间件的process_response方法,传入response,返回response
-
调用爬虫中间件的process_response方法,传入response,返回response
-
调用spider的parse方法,传入resposne,得到结果
-
-
调用爬虫中间件的process_request方法,传入request,返回request
-
判断结果的类型,如果是请求对象,调用调度器的add_request,传入请求对象
-
-
否则调用管道的process_item方法,传入结果
-
如何在项目文件中添加配置文件能够覆盖父类的默认配置
-
在框架中conf文件夹下,建立default_settings,设置默认配置
-
-
在框架的conf文件夹下,建立settings文件,导入default_settings中的配置
-
-
在项目的文件夹下,创建settings文件,设置用户配置
-
-
urllib.parse.urljoin(完整的url地址demo,不全的url地址)
返回的是补全后的url地址.
-
提取对象身上的方法,再单独使用.
class Test:
def func(self,param):
print('this is {}'.format(param))
test = Test()
ret = getattr(test,'func')
print(ret)
ret('python') # this is python
-
python中内置发送请求的方法
import requests
# 发送get请求
req = requests.get('https://www.python.org')
# 或者--上面的实现其实本质上是下面代码
req = requests.request('GET', 'http://httpbin.org/get')
# 发送post请求
payload = dict(key1='value1', key2='value2')
req = requests.post('http://httpbin.org/post', data=payload)
# 或者--上面的实现其实本质上是下面代码
req = requests.request('POST', 'http://httpbin.org/post',data=payload)
# put,options,patch,delete方法同上
框架开发分析:
-
了解框架,框架思路分析
-
框架雏形
-
http模块和item模块(传递的数据)
-
core模块(五大核心模块)
-
框架中间件
-
框架安装
-
框架运行
-
-
框架完善
-
-
import logging
# 日志的五个等级,等级依次递增
# 默认是WARNING等级
logging.DEBUG
logging.INFO
logging.WARNING
logging.ERROR
logging.CRITICAL
# 设置日志等级
logging.basicConfig(level=logging.INFO)
# 使用
logging.debug('DEBUG')
logging.info('INFO')
logging.warning('WARNING')
logging.error('ERROR')
logging.critical('CRITICAL')
# 捕获异常信息到日志
try:
raise Exception('异常')
expect Exception as e:
logging.exception(e)
# 可以对日志输出格式进行自定义
%(name)s Logger的名字
%(asctime)s 字符串形式的当前时间
%(filename)s 调用日志输出函数的模块的文件名
%(lineno)d 调用日志输出函数的语句所在的代码行
%(levelname)s 文本形式的日志级别
%(message)s 用户输出的消息
# 默认日志格式
'%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s: %(message)s'利用logger封装日志模块
# 自行封装了一个Logger类
# 思考在框架中那些地方需要输出日志信息
# 1. 引擎启动时--输出开始时间
# 2. 引擎结束时--输出结束时间,以及总耗时 -
配置文件实现
# 思路
# 1. 先设置个默认配置文件default_settings.py(里面可以包含用户看不到的配置)
# 2. 再在同级下生成settings,把default_Settings中配置全部导入
# 3. 考虑到用户使用在外部要修改配置,所以框架也会在外层生成个settings.py文件
# 4. 如何把用户和默认的同时兼顾呢,就是把用户修改了的覆盖默认的
# 5. 覆盖的方法就是在default_settings的同级目录下创建settings.py,先导入默认配置,再导入用户配置。参考项目的启动顺序,到外部的settings.py文件,直接 from settings import * 即可。 -
实现同时多请求
"""
实现思路:
1.设置start_url为一个列表
2.爬虫组件 遍历该列表,来构造请求,返回生成器对象
3.引擎组件 遍历上述请求生成器,逐一添加到调度器中
4.同时为了避免阻塞,取的过程设置为不等待取,queue.get(block=False),取不到返回none
5.可能得到none,所以引擎中要先判断,如果没有,就直接return
""" -
实现同时多解析函数
"""
多解析函数就是:实现scrapy请求中的callback
实现思路:
1.在request模块,添加callback,meta两个参数
2.在response中添加meta参数,接收request中的meta
3.在引擎中调用callback指定的函数解析响应
即使用getattr方法,获取爬虫对象上的callback方法,然后来解析响应
parse = getattr(self.spider,request.callback)
""" -
实现同时多个spider文件执行
"""
多个spider文件,先考虑传列表
首先引擎中修改代码,spiders参数变成列表,遍历该列表,进行各爬虫请求入调度器队列
然后,执行 请求-响应-item,但是考虑上上述多解析函数,解析请求的方法是爬虫中的callback,该过程中,并不知道请求对应的爬虫是谁,只知道爬虫列表。 考虑构建请求对应的爬虫,即绑定,遍历爬虫文件构建请求的构成中,给请求赋值个爬虫索引属性-
request.spider_index = self.spiders.index(spider)
如此可知请求对应的爬虫是,spider = self.spiders[request.spider_index]
上述方法用字典页可以实现。看效率,字典可能好些吧。
""" -
实现多个管道
"""
修改引擎,让pipeline由外界传入,在爬虫传item到pipelines中处理时,process_item()传入两个参数,item和spider,以此来决定用哪个爬虫
""" -
实现项目中传入多个中间件
"""
不同的中间件可以实现对请求或者是响应对象进行不同的处理,通过不同的中间件实现不同的功能,让逻辑更加清晰
修改engine让spider_middleware,download_middleware由外界传入,因为传入的都是列表
所以,使用中间件时,遍历使用。
for downloader_mid in self.download_mids:
request = downloader_mid.process_request(request)
for spider_mid in self.spider_mids:
start_resquest = spider_mid.process_request(start_resquest)
""" -
动态模块导入
"""
通过前面的代码编写,我们已经能够完成大部分的任务,但是在main.py 中的代码非常臃肿,对应的我们可以再•settings.py 配置哪些爬虫,管道,中间件需要开启,•能够让整个代码的逻辑更加清晰
利用importlib.import_modle能够传入模块的路径,即可即可实现根据模块的位置的字符串,导入该模块的功能.
1.从conf.settings配置中导入 SPIDERS,PIPELINES,SPIDER_MIDDLEWARE,DOWNLOAD_MIDDLEWARE,
上述分别都是列表
把 self.spiders=spiders 列表替换为,从配置文件中构建的列表
eg:‘spider.kcspider.BaiduSpider’
eg:'pipelines.BaiduPipeline
首先从中切割出模块和类--从右按.切割,左边为模块名,右边为类名
调用方法
self.spiders = _auto_import_instance(SPIDERS,isspider=True)
def _auto_import_instance(path,isspider)
if isspider:
instance={}
else:
instanct=[]
for p in path:
module_name = p.rsplit('.',1)[0]
cls_name = p.rsplit('.',1)[1]
# 动态导入模块
ret = importlib.import_module(module_name)
cls = getattr(module,cls_name)
if isspider:
instance[cls_name]=cls()
else:
instance.append(cls())
return instance
""" -
请求去重
"""
去重的是爬虫创建的request对象,scrapy_redis中使用的是hash.sha1创建文件指纹的方式。
根据请求的url、请求方法、请求参数、请求体进行唯一标识,进行比对,由于这四个数据加到一起,内容较长,因此使用求指纹的方式来进行去重判断。
指纹计算方法,最常用的就是md5、sha1等hash加密算法,来求指纹
考虑去重的调用
引擎中在添加爬虫组件生成的请求对象入调度器队列之前,会先去重再添加
""" -
使用线程/协程池
def _callback(self, temp):
'''执行新的请求的回调函数,实现循环'''
if self.running is True: # 如果还没满足退出条件,那么继续添加新任务,否则不继续添加,终止回调函数,达到退出循环的目的
self.pool.apply_async(self._execute_request_response_item, callback=self._callback)
def _start_engine(self):
'''依次调用其他组件对外提供的接口,实现整个框架的运作(驱动)'''
self.running = True # 启动引擎,设置状态为True
# 向调度器添加初始请求
self.pool.apply_async(self._start_requests) # 使用异步
self.pool.apply_async(self._execute_request_response_item, callback=self._callback) # 利用回调实现循环
# ===========================================
# 协程
# scrapy_plus/async/coroutine.py
'''
由于gevent的Pool的没有close方法,也没有异常回调参数
引出需要对gevent的Pool进行一些处理,实现与线程池一样接口,实现线程和协程的无缝转换
'''
from gevent.pool import Pool as BasePool
import gevent.monkey
gevent.monkey.patch_all() # 打补丁,替换内置的模块
class Pool(BasePool):
'''协程池
使得具有close方法
使得apply_async方法具有和线程池一样的接口
'''
def apply_async(self, func, args=None, kwds=None, callback=None, error_callback=None):
return super().apply_async(func, args=args, kwds=kwds, callback=callback)
def close(self):
'''什么都不需要执行'''
pass
-
-
框架升级
-
分布式爬虫,scrapy-redis
"""
利用redis实现队列
注意pickle模块的使用:如果将对象存入redis中,需要先将其序列化为二进制数据,取出后反序列化就可以再得到原始对象
接口定义一致性:利用redis使用一个Queue,使其接口同python的内置队列接口一致,可以实现无缝转换
redis -- 存储指纹和待抓取的请求对象
mysql -- 数据存储
""" -
增量爬虫
"""
增量抓取,意即针对某个站点的数据抓取,当网站的新增数据或者该站点的数据发生了变化后,自动地抓取它新增的或者变化后的数据
设计原理:
1.定时向目标站点发起请求
2.关闭对目标站点请求的去重判断
3.对抓取来的数据,入库前进行数据判断,只存储新增或改变的数据
""" -
断点续爬
"""
断点续爬的效果:爬虫程序中止后,再次启动,对已发送的请求不再发起,而是直接从之前的队列中获取请求继续执行。
意味着要实现以下两点:
1.去重标识(历史请求的指纹)持久化存储,使得新的请求可以和之前的请求进行去重对比
2.请求队列的持久化
之前的分布式实现了上述两点,但可能会出现问题:
1.如果其中部分或全部执行体被手动关闭或异常中止,那么这不未被正常执行的请求体,就会丢失,因为请求后,队列中就删除了该请求。
解决办法:创建请求备份容器,当请求成功后,再把该请求从容器中删除,而不是队列中的,pop删除取出请求。这样的话,当队列中请求全部执行完毕后,备份容器中的请求就是丢失的请求,接下来只需要把它们重新放回请求队列中重新执行就好了。
2.如果某个请求无论如何都无法执行成功,那么这里可能造成死循环。
解决办法:考虑给request请求对象设置‘重试次数’属性。
a1.每次从队列中弹出一个请求时,就把它在备份容器中对应的‘重试次数’+1
a2. 每次从队列中弹出一个请求后,先判断它的'重试次数'是否超过配置的'最大重试次数',如果超过,就不再处理该对象,把它记录到日志中,同时从备份容器中删除该请求。否则就继续执行。
"""
-
-
框架项目实战