Python自動化開發學習-TinyScrapy

這裏通過代碼一步一步的演變,最後完成的是一個精簡的Scrapy。在Scrapy內部,基本的流程就是這麼實現的。主要是爲了能通過學習瞭解Scrapy大致的流程,對之後再要去看Scrapy的源碼也是有幫助的。

Twisted使用

因爲Scrapy是基於Twisted實現的,所以先看Twisted怎麼用

基本使用

基本使用的示例:

from twisted.web.client import getPage, defer
from twisted.internet import reactor

# 所有任務完成後的回調函數
def all_done(arg):
    """所有爬蟲執行完後執行,循環終止"""
    print("All Done")
    reactor.stop()

# 單個任務的回調函數
def callback(contents):
    """每個爬蟲獲取到結果後執行"""
    print(contents)

deferred_list = []

url_list = [
    'http://www.bing.com',
    'http://www.baidu.com',
    'http://edu.51cto.com',
]

for url in url_list:
    deferred = getPage(bytes(url, encoding='utf-8'))
    deferred.addCallback(callback)
    deferred_list.append(deferred)

dlist = defer.DeferredList(deferred_list)
dlist.addBoth(all_done)

if __name__ == '__main__':
    reactor.run()

在for循環裏,創建了對象,還給對象加了回調函數,這是單個任務完成後執行的。此時還沒有進行下載,而是把所有的對象加到一個列表裏。
之後的defer.DeferredList的調用,纔是執行所有的任務。並且又加了一個回調函數all_done,這個是所有任務都完成後才執行的。

基於裝飾器1

基於裝飾器也可以實現,下面的代碼是基於上面的示例做了簡單的轉換:

from twisted.web.client import getPage, defer
from twisted.internet import reactor

def all_done(arg):
    print("All Done")
    reactor.stop()

def one_done(response):
    print(response)

@defer.inlineCallbacks
def task(url):
    deferred = getPage(bytes(url, encoding='utf-8'))
    deferred.addCallback(one_done)
    yield deferred

deferred_list = []

url_list = [
    'http://www.bing.com',
    'http://www.baidu.com',
    'http://edu.51cto.com',
]

for url in url_list:
    deferred = task(url)
    deferred_list.append(deferred)

dlist = defer.DeferredList(deferred_list)
dlist.addBoth(all_done)

if __name__ == '__main__':
    reactor.run()

把原來for循環裏的2行代碼封裝的了一個task函數裏,並且加了裝飾器。
這個task函數有3個要素:裝飾器、deferred對象、通過yield返回返回對象。這個是Twisted裏標準的寫法。

基於裝飾器2

在上面的示例的基礎上,把整個for循環都移到task函數裏了:

from twisted.web.client import getPage, defer
from twisted.internet import reactor

def all_done(arg):
    print("All Done")
    reactor.stop()

def one_done(response):
    print(response)

@defer.inlineCallbacks
def task():
    for url in url_list:
        deferred = getPage(bytes(url, encoding='utf-8'))
        deferred.addCallback(one_done)
        yield deferred

url_list = [
    'http://www.bing.com',
    'http://www.baidu.com',
    'http://edu.51cto.com',
]

ret = task()
ret.addBoth(all_done)

if __name__ == '__main__':
    reactor.run()

上面說個的3要素:裝飾器、deferred對象、yield都有。

基於裝飾器永不退出

在前面的示例中,每完成一個任務,就會返回並執行一個回調函數one_done。所有任務如果都返回了,程序就會退出(退出前會執行回調函數all_done)。
這裏所做的,就是添加一個不會返回的任務,這樣程序的一直不會退出了:

from twisted.web.client import getPage, defer
from twisted.internet import reactor

def all_done(arg):
    print("All Done")
    reactor.stop()

def one_done(response):
    print(response)

@defer.inlineCallbacks
def task():
    for url in url_list:
        deferred = getPage(bytes(url, encoding='utf-8'))
        deferred.addCallback(one_done)
        yield deferred
    # 下面的這個任務永遠不會完成
    stop_deferred = defer.Deferred()  # 這是一個空任務,不會去下載,所以永遠不會返回
    # stop_deferred.callback(None)  # 執行這句可以讓這個任務返回
    stop_deferred.addCallback(lambda s: print(s))
    stop_deferred.callback("stop_deferred")
    yield stop_deferred

url_list = [
    'http://www.bing.com',
    'http://www.baidu.com',
    'http://edu.51cto.com',
]

ret = task()
ret.addBoth(all_done)

if __name__ == '__main__':
    reactor.run()

這裏的做法,就是加了一個額外的任務。要求返回的是Deferred對象,這裏就創建了一個空的Deferred對象,並把這個對象返回。
在這裏,我們並沒有讓這個空的Deferred對象去下載,所以也就永遠不會有返回。
永不退出的意義
這裏目的就是不讓程序退出,讓這個事件循環一直在那裏執行。之後還可以繼續往裏面添加任務,然後執行新的任務。
程序退出的方法
還是可以讓程序退出的。就是調用stop_deferred的callback方法,在上面的代碼裏註釋掉了。執行這個方法,就是強制執行該任務的回調函數。
之前都是等任務執行完返回後,會自動調用callback方法,這裏就是強制調用了。
並且由於代碼裏沒有爲stop_deferred指定回調函數,所有調用方法後不會執行任何函數。不過調用callback方法必須有一個參數,這裏隨便寫個就好了。
也可以給stop_deferred加一個回調函數,然後再調用callback方法:

stop_deferred.addCallback(lambda s: print(s))
stop_deferred.callback("stop_deferred")

Scrapy裏的做法
這就是Scrapy裏運行完終止的邏輯。第一次只有一個url,執行完就返回了,並且此時應該是所有任務都返回了,那麼就會退出程序。
在Scrapy裏,也是這樣加了一個永遠不會返回的任務,不讓程序退出。然後之前的結果返回後,又會生成新的任務到調度器,這樣就會動態的添加任務繼續執行。
要讓程序可以退出,這裏還需要做一個檢測。在下載完成之後的回調函數裏,會生成新的任務繼續給執行。這裏可以執行2個回調函數。
第一個回調函數就是生成新的任務放入調度器,第二個回調函數就是檢測等待執行的任務的數量,以及正在執行的任務數量。如果都是0,表示程序可以結束了。
程序結束的方法就是上面的用的調用執行callback方法。

執行完畢後停止事件循環

基於上面的說的,這裏的代碼實現了全部任務執行完畢後可以調用stop_deferred的callback方法來退出:

from twisted.web.client import getPage, defer
from twisted.internet import reactor

task_list = []
stop_deferred = None

def all_done(arg):
    print("All Done")
    reactor.stop()

def one_done(response):
    print(response)

def check_empty(response, *args, **kw):
    url = kw.get('url')
    if url in running_list:
        running_list.remove(url)
    if not running_list:
        stop_deferred.callback()

@defer.inlineCallbacks
def task():
    global running_list, stop_deferred  # 全局變量
    running_list = url_list.copy()
    for url in url_list:
        deferred = getPage(bytes(url, encoding='utf-8'))
        deferred.addCallback(one_done)
        deferred.addCallback(check_empty, url=url)
        yield deferred
    stop_deferred = defer.Deferred()
    yield stop_deferred

url_list = [
    'http://www.bing.com',
    'http://www.baidu.com',
    'http://edu.51cto.com',
]

ret = task()
ret.addBoth(all_done)

if __name__ == '__main__':
    reactor.run()

代碼優化

上面的代碼功能上都實現了,但是實現方法有點不太好。
首先,task函數裏分成了兩部分,一部分是我們自己調度的任務,一部分是爲了不讓程序退出,而加的一個空任務。可以把這兩部分拆開放在兩個函數裏。分拆之後,只有第一部分的函數是需要留給用戶使用的。下面是把原來的task函數分拆後的代碼,並且每個函數也都需要加上裝飾器:

from twisted.web.client import getPage, defer
from twisted.internet import reactor

task_list = []
stop_deferred = None

def all_done(arg):
    print("All Done")
    reactor.stop()

def one_done(response):
    print(response)

def check_empty(response, url):
    if url in running_list:
        running_list.remove(url)
    if not running_list:
        stop_deferred.callback()

@defer.inlineCallbacks
def open_spider():
    global running_list
    running_list = url_list.copy()
    for url in url_list:
        deferred = getPage(bytes(url, encoding='utf-8'))
        deferred.addCallback(one_done)
        deferred.addCallback(check_empty, url)
        yield deferred

@defer.inlineCallbacks
def stop():
    global stop_deferred
    stop_deferred = defer.Deferred()
    yield stop_deferred

@defer.inlineCallbacks
def task():
    yield open_spider()
    yield stop()

url_list = [
    'http://www.bing.com',
    'http://www.baidu.com',
    'http://edu.51cto.com',
]

ret = task()
ret.addBoth(all_done)

if __name__ == '__main__':
    reactor.run()

另外還有全局變量的問題,這裏的代碼使用了全部變量,這不是一個好的做法。再改下去需要引入class了。

模擬Scrapy

從這裏開始,就要使用面向對象的方法,進一步進行封裝了。

封裝部分

先把之前主要的代碼封裝到類裏:

from twisted.web.client import getPage, defer
from twisted.internet import reactor
import queue

class Request(object):
    """封裝請求的url和回調函數"""
    def __init__(self, url, callback):
        self.url = url
        self.callback = callback

class Scheduler(object):
    """調度器"""
    def __init__(self, engine):
        self.engine = engine
        self.q = queue.Queue()

    def enqueue_request(self, request):
        """添加任務"""
        self.q.put(request)

    def next_request(self):
        """獲取下一個任務"""
        try:
            req = self.q.get(block=False)
        except queue.Empty:
            req = None
        return req

    def size(self):
        return self.q.qsize()

class ExecutionEngine(object):
    """引擎"""
    def __init__(self):
        self._close_wait = None  # stop_deferred
        self.start_requests = None
        self.scheduler = Scheduler(self)
        self.in_progress = set()  # 正在執行中的任務

    def _next_request(self):
        while self.start_requests:
            request = next(self.start_requests, None)
            if request:
                self.scheduler.enqueue_request(request)
            else:
                self.start_requests = None
        while len(self.in_progress) < 5 and self.scheduler.size() > 0:  # 最大編髮爲5
            request = self.scheduler.next_request()
            if not request:
                break
            self.in_progress.add(request)
            d = getPage(bytes(request.url, encoding='utf-8'))
            # addCallback是正確返回的時候執行,還有addErrback是返回有錯誤的時候執行
            # addBoth就是上面兩種情況返回都會執行
            d.addBoth(self._handle_downloader_output, request)
            d.addBoth(lambda x, req: self.in_progress.remove(req), request)
            d.addBoth(lambda x: self._next_request())
        if len(self.in_progress) == 0 and self.scheduler.size() == 0:
            self._close_wait.callback(None)

    def _handle_downloader_output(self, response, request):
        import types
        gen = request.callback(response)
        if isinstance(gen, types.GeneratorType):  # 是否爲生成器類型
            for req in gen:
                # 這裏還可以再加判斷,如果是request對象則繼續爬取
                # 如果是item對象,則可以交給pipline
                self.scheduler.enqueue_request(req)

    @defer.inlineCallbacks
    def open_spider(self, start_requests):
        self.start_requests = start_requests
        yield None
        reactor.callLater(0, self._next_request)  # 過多少秒之後,執行後面的函數

    @defer.inlineCallbacks
    def start(self):
        """原來的stop函數"""
        self._close_wait = defer.Deferred()
        yield self._close_wait

@defer.inlineCallbacks
def crawl(start_requests):
    """原來的task函數"""
    engine = ExecutionEngine()
    start_requests = iter(start_requests)
    yield engine.open_spider(start_requests)
    yield engine.start()

def all_done(arg):
    print("All Done")
    reactor.stop()

def one_done(response):
    print(response)

count = 0
def chouti(response):
    """任務返回後生成新的Request繼續交給調度器執行"""
    global count
    count += 1
    print(response)
    if count > 3:
        return None
    for i in range(10):
        yield Request("http://dig.chouti.com/all/hot/recent/%s" % i, lambda x: print(len(x)))

if __name__ == '__main__':
    url_list = [
        'http://www.bing.com',
        'https://www.baidu.com',
        'http://edu.51cto.com',
    ]
    requests = [Request(url, callback=one_done) for url in url_list]
    # requests = [Request(url, callback=chouti) for url in url_list]
    ret = crawl(requests)
    ret.addBoth(all_done)
    reactor.run()

這裏還寫了一個回調函數chouti,可以在爬蟲返回後,生成新的Request繼續爬取。爲了控制這個回調函數的調用,又加了一個全局變量。
接下來會對這部分函數繼續封裝,把所有的代碼都封裝到類裏。
閉包解決全局變量
這裏的部分是我自己嘗試的思考。
其實還可以通過閉包的方法。通過閉包來保存函數的狀態,而不使用全局變量:

def chouti2():
    n = 0

    def func(response):
        print(response)
        nonlocal n
        n += 1
        if n > 3:
            return None
        for i in range(10):
            yield Request("http://dig.chouti.com/all/hot/recent/%s" % i, lambda x: print(len(x)))
    return func

if __name__ == '__main__':
    url_list = [
        'http://www.bing.com',
        'https://www.baidu.com',
        'http://edu.51cto.com',
    ]
    # requests = [Request(url, callback=one_done) for url in url_list]
    # requests = [Request(url, callback=chouti) for url in url_list]
    callback = chouti2()
    requests = [Request(url, callback=callback) for url in url_list]
    ret = crawl(requests)
    ret.addBoth(all_done)
    reactor.run()

完全封裝

上面的示例還有幾個函數,繼續把剩下的函數也封裝到類裏。下面的這個就是TinyScrapy

from twisted.web.client import getPage, defer
from twisted.internet import reactor
import queue

class Request(object):
    """封裝請求的url和回調函數"""
    def __init__(self, url, callback=None):
        self.url = url
        self.callback = callback  # 默認是None,則會去調用Spider對象的parse方法

class Scheduler(object):
    """調度器"""
    def __init__(self, engine):
        self.engine = engine
        self.q = queue.Queue()

    def enqueue_request(self, request):
        """添加任務"""
        self.q.put(request)

    def next_request(self):
        """獲取下一個任務"""
        try:
            req = self.q.get(block=False)
        except queue.Empty:
            req = None
        return req

    def size(self):
        return self.q.qsize()

class ExecutionEngine(object):
    """引擎"""
    def __init__(self):
        self._close_wait = None  # stop_deferred
        self.start_requests = None
        self.scheduler = Scheduler(self)
        self.in_progress = set()  # 正在執行中的任務
        self.spider = None  # 在open_spider方法裏添加

    def _next_request(self):
        while self.start_requests:
            request = next(self.start_requests, None)
            if request:
                self.scheduler.enqueue_request(request)
            else:
                self.start_requests = None
        while len(self.in_progress) < 5 and self.scheduler.size() > 0:  # 最大編髮爲5
            request = self.scheduler.next_request()
            if not request:
                break
            self.in_progress.add(request)
            d = getPage(bytes(request.url, encoding='utf-8'))
            # addCallback是正確返回的時候執行,還有addErrback是返回有錯誤的時候執行
            # addBoth就是上面兩種情況返回都會執行
            d.addBoth(self._handle_downloader_output, request)
            d.addBoth(lambda x, req: self.in_progress.remove(req), request)
            d.addBoth(lambda x: self._next_request())
        if len(self.in_progress) == 0 and self.scheduler.size() == 0:
            self._close_wait.callback(None)

    # 這個方法和之前的有一點小的變化,主要是用到了新定義的Response對象
    def _handle_downloader_output(self, body, request):
        import types
        response = Response(body, request)
        # 如果沒有指定callback就調用Spider類的parse方法
        func = request.callback or self.spider.parse
        gen = func(response)
        if isinstance(gen, types.GeneratorType):  # 是否爲生成器類型
            for req in gen:
                # 這裏還可以再加判斷,如果是request對象則繼續爬取
                # 如果是item對象,則可以交給pipline
                self.scheduler.enqueue_request(req)

    @defer.inlineCallbacks
    def open_spider(self, spider, start_requests):
        self.start_requests = start_requests
        self.spider = spider  # 加了這句
        yield None
        reactor.callLater(0, self._next_request)  # 過多少秒之後,執行後面的函數

    @defer.inlineCallbacks
    def start(self):
        """原來的stop函數"""
        self._close_wait = defer.Deferred()
        yield self._close_wait

class Response(object):
    def __init__(self, body, request):
        self.body = body
        self.request = request
        self.url = request.url

    @property
    def text(self):
        return self.body.decode('utf-8')

class Crawler(object):
    def __init__(self, spider_cls):
        self.spider_cls = spider_cls
        self.spider = None
        self.engine = None

    @defer.inlineCallbacks
    def crawl(self):
        self.engine = ExecutionEngine()
        self.spider = self.spider_cls()
        start_requests = iter(self.spider.start_requests())
        yield self.engine.open_spider(self.spider, start_requests)
        yield self.engine.start()

class CrawlerProcess(object):
    def __init__(self):
        self._active = set()
        self.crawlers = set()

    def crawl(self, spider_cls, *args, **kwargs):
        crawler = Crawler(spider_cls)
        self.crawlers.add(crawler)
        d = crawler.crawl(*args, **kwargs)
        self._active.add(d)
        return d

    def start(self):
        dl = defer.DeferredList(self._active)
        dl.addBoth(self._stop_reactor)
        reactor.run()

    @classmethod
    def _stop_reactor(cls, _=None):
        """原來的all_done函數
        之前的示例中,這個函數都是要接收一個參數的。
        雖然不用,但是調用的模塊一定會傳過來,所以一定要接收一下。
        這裏就用了佔位符來接收這個參數,並且設置了默認值None。
        """
        print("All Done")
        reactor.stop()

class Spider(object):
    def __init__(self):
        if not hasattr(self, 'start_urls'):
            self.start_urls = []

    def start_requests(self):
        for url in self.start_urls:
            yield Request(url)

    def parse(self, response):
        print(response.body)

class ChoutiSpider(Spider):
    name = "chouti"
    start_urls = ["http://dig.chouti.com"]

    def parse(self, response):
        print(next((s for s in response.text.split('\n') if "<title>" in s)))

class BingSpider(Spider):
    name = "bing"
    start_urls = ["http://www.bing.com"]

class BaiduSpider(Spider):
    name = "baidu"
    start_urls = ["http://www.baidu.com"]

if __name__ == '__main__':
    spider_cls_list = [ChoutiSpider, BingSpider, BaiduSpider]
    crawler_process = CrawlerProcess()
    for spider_cls in spider_cls_list:
        crawler_process.crawl(spider_cls)
    crawler_process.start()

這裏用的類名、方法名、部分代碼都是和Scrapy的源碼裏一樣的。相當於把Scrapy精簡了,把其中的核心都提取出來了。如果能看明白這部分代碼,再去Scrapy裏看源碼應該能相對容易一些了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章