Python之Scrapy框架源码解析

接下来会写一个按照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()
DEMO

定制版CrazyScrapy:点击下载

更多文档参见:http://scrapy-chs.readthedocs.io/zh_CN/latest/index.html

posted @ 2019-03-17 16:56  初遇ぃ  阅读(1171)  评论(0编辑  收藏  举报
//一下两个链接最好自己保存下来,再上传到自己的博客园的“文件”选项中 //一下两个链接最好自己保存下来,再上传到自己的博客园的“文件”选项中