接下来会写一个按照Scrapy框架的原理流程实现自定义的Scrapy框架,而后再看源码的时候更便于阅读。
前戏
Scrapy内部实现并发操作采用的是twisted模块,简单实现一个小DEMO
from twisted.internet import reactor # 事件循环(终止条件,所有的socket都已经移除) from twisted.web.client import getPage # socket对象(如果下载完成,自动从事件循环中移除...) from twisted.internet import defer # defer.Deferred 特殊的socket对象 (不会发请求,手动移除) def response(body): print(body) @defer.inlineCallbacks def task(): url = 'http://www.baidu.com' d = getPage(url.encode('utf-8')) d.addCallback(response) yield d task() reactor.run() # 开启事件循环
在 Twisted 中,有一种特殊的对象用于实现事件循环。这个对象叫做 reactor。可以把反应器(reactor)想象为 Twisted 程序的中枢神经。除了分发事件循环之外,反应器还做很多重要的工作:定时任务、线程、建立网络连接、监听连接。为了让反应器可以正常工作,需要启动事件循环。
from twisted.internet import reactor # 事件循环(终止条件,所有的socket都已经移除) from twisted.web.client import getPage # socket对象(如果下载完成,自动从事件循环中移除...) from twisted.internet import defer # defer.Deferred 特殊的socket对象 (不会发请求,手动移除) ######################### # 1.利用getPage创建socket # 2.将socket添加到事件循环中 # 3.开始事件循环(自动结束) ######################### def response(content): print(content) # 该装饰器装饰的内容,只要yield是一个阻塞的对象都会转交给reactor接手 @defer.inlineCallbacks def task(): url = "http://www.baidu.com" d = getPage(url.encode('utf-8')) d.addCallback(response) yield d url = "http://www.baidu.com" d = getPage(url.encode('utf-8')) d.addCallback(response) yield d def done(*args,**kwargs): reactor.stop() li = [] for i in range(10): d = task() li.append(d) # DeferredList也属于defer的对象,也会转交给reactor接手 dd = defer.DeferredList(li) # 给它增加了一个回调函数 dd.addBoth(done) reactor.run()
自定制爬虫CrazyScrapy
from twisted.internet import reactor # 事件循环(终止条件,所有的socket都已经移除) from twisted.web.client import getPage # socket对象(如果下载完成,自动从时间循环中移除...) from twisted.internet import defer # defer.Deferred 特殊的socket对象 (不会发请求,手动移除) # 自定义一个Request 类 class Request(object): def __init__(self, url, callback): """ 初始化接受url和callback回调函数 :param url: 请求的url :param callback: 获取内容后的callback """ self.url = url self.callback = callback # 响应对象 class HttpResponse(object): def __init__(self, content, request): """ 初始化相应内容 :param content: 下载 下来的响应的content :param request: response对应的request """ # 响应的内容 self.content = content # 响应的请求 self.request = request # response对应的request self.url = request.url # 将内容转换为文本 self.text = str(content, encoding='utf-8') class ChoutiSpider(object): """ 初始化顶一个小蜘蛛 """ name = 'chouti' # 蜘蛛一开始的执行方法 def start_requests(self): start_url = ['http://www.baidu.com', 'http://www.bing.com', ] for url in start_url: yield Request(url, self.parse) # 收到response后的解析函数 def parse(self, response): print(response) # response是下载的页面 yield Request('http://www.cnblogs.com', callback=self.parse) import queue # 这里是调度器 Q = queue.Queue() # 定义了一个引擎类 class Engine(object): def __init__(self): # 引擎关闭 self._close = None # 最大的请求数 self.max = 5 # 正在爬的请求 self.crawlling = [] # 拿着相应的回调函数 def get_response_callback(self, content, request): """ :param content: 响应的content :param request: 响应对应的request :return: """ # 一旦执行回调函数,就可以从调度中拿走这个请求 self.crawlling.remove(request) # 将内容封装成 一个 HttpResponse对象 rep = HttpResponse(content, request) # 调用请求时的回调函数,将封装的HttpResponse传递进去 result = request.callback(rep) import types # 查看回调函数是否继续返回迭代器对象 if isinstance(result, types.GeneratorType): # 将回调函数 新的请求放到调度器 for req in result: Q.put(req) # 从调度器取请求,执行,下载,并控制最大并发数 def _next_request(self): """ 去取request对象,并发送请求 最大并发数限制 :return: """ print(self.crawlling, Q.qsize()) # 如果调度器的长度为0,而且处于正在爬取的数目也为 0 ,那么就说明该关闭了 if Q.qsize() == 0 and len(self.crawlling) == 0: # 直接调用 defer.Deferred().callback(None)就会关闭defer self._close.callback(None) return # 如果正在爬取的数目超过了最大的并发限制,直接返回 if len(self.crawlling) >= self.max: return # 如果没有达到并发限制,就执行以下内容 while len(self.crawlling) < self.max: try: # 从 调度器 取一个请求 任务 req = Q.get(block=False) # 把拿到的请求放到 正在爬取的列表中 self.crawlling.append(req) # 获取相应的页面 d = getPage(req.url.encode('utf-8')) # 页面下载完成,get_response_callback,调用用户spider中定义的parse方法,并且将新请求添加到调度器 d.addCallback(self.get_response_callback, req) # 未达到最大并发数,可以再去调度器中获取Request # 继续给d添加回调函数,这个回调函数可以是匿名函数 d.addCallback(lambda _: reactor.callLater(0, self._next_request)) except Exception as e: print(e) return @defer.inlineCallbacks def crawl(self, spider): # 将start_requests包含的生成器,初始Request对象添加到调度器 start_requests = iter(spider.start_requests()) while True: try: # 拿到每个request,放到调度器中 request = next(start_requests) Q.put( request) except StopIteration as e: break # 去调度器中取request,并发送请求 # self._next_request() reactor.callLater(0, self._next_request) # 初始化self._close self._close = defer.Deferred() yield self._close # 初始化一个抽屉爬虫 spider = ChoutiSpider() _active = set() # 实例化一个引擎对象 engine = Engine() # 引擎对象 调用 crawl方法,运行指定的spider d = engine.crawl(spider) # 将crawl方法放到set中 _active.add(d) # 实例化一个DeferredList,将_active 内容放进去,返回一个defer.Deferred()对象,若defer.Deferred()被关闭,dd就为空 dd = defer.DeferredList(_active) # 一旦dd里面为空,就调用reactor.stop()方法 dd.addBoth(lambda a: reactor.stop()) # 让它run起来 reactor.run()
定制版CrazyScrapy:点击下载
更多文档参见:http://scrapy-chs.readthedocs.io/zh_CN/latest/index.html