1. 程式人生 > >通過twisted來自己寫scrapy框架來了解scrapy原始碼

通過twisted來自己寫scrapy框架來了解scrapy原始碼

from twisted.internet import reactor #事件迴圈 相當於selecet作用 監聽是否有連線成功(終止條件,所有的socket物件都被移除。)
from twisted.web.client import getPage #socket物件(如果下載完成,自動從事件迴圈中移除)
from twisted.internet import defer    #defer.Deferred 特殊的socket物件(不會發請求,目的是不讓事件迴圈結束,可以手動移除。)
from queue import Queue

class Request:#請求類
    def __init__(self, url, callback):
        self.url = url
        self.callback = callback


class HttpResponse:#響應類
    def __init__(self, content, request):
        self.content = content
        self.request = request
        
             
class Command:
    def run(self):#開始執行
        crawl_process = CrawlerProcess()#建立crawl_process物件
        spider_cls_path_list = ["chouti.ChoutiSpider", "baidu.BaiduSpider"]
        for spider_cls_path in spider_cls_path_list:
            crawl_process.crawl(spider_cls_path)#建立爬蟲 開啟引擎
        crawl_process.start()#開啟事件迴圈


class CrawlerProcess:#用於開啟事件迴圈
    def __init__(self):
        self._active = set()

    def crawl(self, spider_cls_path):
        crawler = Crawler()
        d = crawler.crawl(spider_cls_path)#通過spider_cls_path建立socket物件
        self._active.add(d)

    def start(self):#開啟事件迴圈
        dd = defer.DeferredList(self._active)#新增到集合中方便defer.DeferredList(用來監聽的事件迴圈列表)監聽
        dd.addBoth(lambda _: reactor.stop())#如果事件迴圈中的socket物件都被移除則迴圈停止
        reactor.run()#開啟事件迴圈


class Crawler:#用於封裝引擎和排程器
    @defer.inlineCallbacks#非同步處理 此裝飾器下的函式必須返回yield 不想返回東西可以返回None
    def crawl(self, spider_cls_path):
        engine = self.create_engine()#建立引擎
        spider = self.create_spider(spider_cls_path)#建立爬蟲
        start_requests = iter(spider.start_requests())#通過爬蟲的start_requests()方法獲取請求,
        # 因為start_requests()返回的是yield,所以要從生成器轉成迭代器然後用next獲取請求
        yield engine.open_spider(start_requests)#引擎開始
        yield engine.start()#引擎開啟

    def create_engine(self):
        return ExecutionEngine()#返回ExecutionEngine物件 -- 引擎

    def create_spider(self, spider_cls_path):#通過路徑建立爬蟲的物件
        module_path, cls_name = spider_cls_path.rsplit(".", maxsplit=1)
        import importlib
        m = importlib.import_module(module_path)
        cls = getattr(m, cls_name)#獲取爬蟲的類名
        return cls()#返回爬蟲的物件


class ExecutionEngine:#引擎:所有的排程
    def __init__(self):#引擎初始化
        self._close = None#特殊的socket類 不發請求
        self.scheduler = None
        self.max = 5#正在執行的請求的限制數量
        self.crawlling = []#正在執行的請求

    def get_response_callback(self, content, request):#獲得響應
        self.crawlling.remove(request)#先把獲得響應的請求從正在執行的請求列表中刪除
        response = HttpResponse(content, request)#獲得響應的物件 裡面封裝了響應內容和之前傳送的請求
        result = request.callback(response)#其實就是呼叫Request的parse 來判斷是否還有其他請求
        import types
        if isinstance(result, types.GeneratorType):#如果result是迭代器 說明又返回了其他請求
            for req in result:
                self.scheduler.enqueue_request(req)#把新的請求加入到排程佇列中

    def _next_request(self):
        if self.scheduler.size() == 0 and len(self.crawlling) == 0:#事件迴圈的終止條件既排程佇列為空且正在執行的請求列表也為空
            self._close.callback(None)#從事件迴圈中移除,關閉事件迴圈,進而關閉爬蟲
            return
        while len(self.crawlling) < self.max:#當正在執行的請求小於5時
            req = self.scheduler.next_request()#獲取新的請求
            if not req:#如果請求不為None
                return
            self.crawlling.append(req)#把不為None的請求放入到正在執行的請求列表中
            d = getPage(req.url.encode("utf-8"))
            d.addCallback(self.get_response_callback, req)#用get_response_callback獲得響應的內容
            d.addCallback(lambda _:reactor.callLater(0, self._next_request))

    @defer.inlineCallbacks
    def open_spider(self, start_requests):#開啟爬蟲
        self.scheduler = Scheduler()#建立排程器
        yield self.scheduler.open()#self.scheduler.open()為返回值為None 這裡一定要yield 因為@defer.inlineCallbacks規定的
        while True:
            try:
                req = next(start_requests)#通過迭代器獲取請求
            except StopIteration:#如果迭代器為空 跳出迴圈
                break
            self.scheduler.enqueue_request(req)#不斷地把獲得的請求加入排程佇列中,直到迭代器取空
        reactor.callLater(0, self._next_request)#呼叫回撥函式 self._next_request來把排程佇列中的請求傳送出去

    @defer.inlineCallbacks
    def start(self):
        self._close = defer.Deferred()#建立特殊的socket物件,此物件不發請求,加入到事件迴圈中,
                                        # 等socket請求都得到響應,再從事件迴圈中刪除
        yield self._close


class Scheduler:#任務排程器
    def __init__(self):
        self.q = Queue()#建立排程的佇列

    def enqueue_request(self, req):#從佇列中放入請求
        self.q.put(req)

    def next_request(self):#從佇列中取請求
        try:
            req = self.q.get(block=False)#非阻塞
        except Exception as e:#如果佇列為空則會發生異常 抓住這個異常
            req = None
        return req#返回請求 可能為None

    def size(self):#佇列裡的數量
        return self.q.qsize()

    def open(self):
        pass


if __name__ == '__main__':
    cmd = Command()
    cmd.run()




ChoutiSpider

from engine import Request

class ChoutiSpider:
    def start_requests(self):
        start_urls = ["https://www.chouti.com","https://www.taobao.com"]
        for url in start_urls:
            yield Request(url=url, callback=self.parse)

    def parse(self, response):
        print(response.request.url)
        # yield Request("https://www.zhihu.com",callback=self.parse)

BaiduSpider

from engine import Request

class BaiduSpider:
    def start_requests(self):
        start_urls = ["https://www.baidu.com","https://www.bilibili.com"]
        for url in start_urls:
            yield Request(url=url, callback=self.parse)

    def parse(self, response):
        print(response.request.url)

結果:

獲得網頁程式碼太大,不方便放在csdn上,所以我就列印下獲得的響應裡封裝的url,parse裡可以繼續發請求。

D:\untitled\spider\venv\Scripts\python.exe D:/spider/engine.py
https://www.baidu.com
https://www.bilibili.com
https://www.taobao.com
https://www.chouti.com

Process finished with exit code 0