44.用Tornado實現web界面爬蟲

用Tornado實現web界面爬蟲

準備

Tornado

無論你們認不認識Tornado,仍是簡要地介紹一下吧。
Tornado是Facebook的一個開源的高性能Web服務器,最大的優點在於它基於事件的設計,使得它在IO密集型(好比代理或爬蟲)應用上有着基於線程web框架沒法比擬的巨大優點。 html

除此以外Tornado有一個很是讚的設計(固然Twisted等別的事件框架也有),基於Generator的協程模型。具體可參見這裏,官方的example淺顯易懂,一眼就能夠看出協程比回調(包括閉包)的優點。 python

數據源

幾乎全部的CryptoCoin相關網站都會提供很方便的Web API,另外也有一些第三方的API接口供使用。
有趣的是,這些API幾乎所有都是Json格式的(究其緣由主要是Bitcoin客戶端就使用的Json表示,因而programmer更傾向於這種方式),因此使用起來很方便。 web

演進

下面咱們以btc-e的ticker API爲例講述一下代碼演進的過程,對比一下幾種實現方式的區別。 json

同步

1
2
3
4
5
6
defget_btce():
    globalcurrent
    res=tornado.httpclient.HTTPClient().fetch(
            "https://btc-e.com/api/2/ltc_btc/ticker"
        )
    current['btce']=tornado.escape.json_decode(res.body)['ticker']['last']

最簡單的同步寫法。注意這種寫法是不能夠實際使用的,由於同步請求會阻塞當前線程,而通常來講全部的事件處理在同一個線程,因此會致使整個服務器的全部其他動做(包括客戶端請求的處理)都要在這個請求返回以後才能夠被處理。 api

回調

1
2
3
4
5
6
7
8
9
defget_btce():
    defcallback(res):
        globalcurrent
        current['btce']=tornado.escape.json_decode(res.body)['ticker']['last']
 
    tornado.httpclient.AsyncHTTPClient().fetch(
            "https://btc-e.com/api/2/ltc_btc/ticker",
            callback=callback,
        )

樸素的callback就是這樣使用的,因爲Python沒有閉包因此寫起來異常噁心(好吧),接下來咱們繼續演進它。 服務器

協程

上面提到的Tornado有利用Python Generator實現的協程,相應的模塊叫作tornado.gen,從3.0開始gen能夠吃Future了因此致使寫起來比以前更簡潔。 閉包

1
2
3
4
5
6
7
@tornado.gen.coroutine
defget_btce():
    globalcurrent
    res=yieldtornado.httpclient.AsyncHTTPClient().fetch(
            "https://btc-e.com/api/2/ltc_btc/ticker"
        )
    current['btce']=tornado.escape.json_decode(res.body)['ticker']['last']

會發現這個已經和同步幾乎同樣了,除了添加了一個修飾器gen.coroutine表示協程,以及在AsyncHTTPClient.fetch()的返回值上用了yield而已,是否是很方便w
講得更細節一點:
首先使用gen.coroutine來表示協程。coroutine會生成一個wrapper,在外面層吃yield point,在原函數yield的時候會傳給外層的wrapper處理,執行完以後再send()回來;
而後ASyncHTTPClient()返回一個Future(也向後兼容以前的callback方式)供回調,須要的童鞋能夠本身看Future的寫法;
當Future執行完成後,coroutine的wrapper會把結果(res)用generator的send()方法傳遞迴用戶代碼,賦值給res變量,用戶代碼繼續執行。 app

重複執行

咱們須要在執行完一次請求以後等待1分鐘再次請求,在線程寫法中time.sleep()能夠很好地解決這個問題,可是事件寫法的話就不能夠sleep了(和上面同步請求同樣會阻塞)。
因而Tornado提供了一個方法叫IOLoop.instance().add_timeout(),這個方法吃一個回調函數,而後能夠指定在某時間或通過多長時間後執行這個函數。因而咱們讓它吃本身就能夠了。 框架

1
2
3
4
5
6
7
8
9
10
11
@tornado.gen.coroutine
defget_btce():
    globalcurrent
    res=yieldtornado.httpclient.AsyncHTTPClient().fetch(
            "https://btc-e.com/api/2/ltc_btc/ticker"
        )
    current['btce']=tornado.escape.json_decode(res.body)['ticker']['last']
    tornado.ioloop.IOLoop.instance().add_timeout(
        datetime.timedelta(milliseconds=delta),
        get_btce()
    )

修飾器

可是呢,每次寫起來會很麻煩,本着「以代碼重用爲榮,以複製粘貼爲恥」的精神,咱們把這個功能抽出來,用有着「Python兩大黑科技」的修飾器(另外一個是metaclass)來實現。 函數

1
2
3
4
5
6
7
8
9
10
11
defloop_call(delta=60*1000):
    defdecorator(func):
        @functools.wraps(func)
        defwrapper(*args,**kwargs):
            func(*args,**kwargs)
            tornado.ioloop.IOLoop.instance().add_timeout(
                datetime.timedelta(milliseconds=delta),
                wrapper,
            )
        returnwrapper
    returndecorator

這是一個典型的三層(帶參)修飾器寫法,使用以下(注意@coroutine要放在@loop_call下面)

1
2
3
4
5
6
7
8
@loop_call()
@tornado.gen.coroutine
defget_btce():
    globalcurrent
    res=yieldtornado.httpclient.AsyncHTTPClient().fetch(
            "https://btc-e.com/api/2/ltc_btc/ticker"
        )
    current['btce']=tornado.escape.json_decode(res.body)['ticker']['last']

修飾器分爲帶參和無參兩種,前者使用@deco()調用,後者使用@deco ;前者在被應用的時候會調用deco()函數,這個函數必須返回一個無參修飾器(也就是decorator()那一層)
最外層的函數的函數名用作(帶參)修飾器的名字,用於產生一個修飾器;
中間層的函數是一個無參修飾器,用於被帶參修飾器返回;
最裏層的函數是被修飾以後的新函數,總使用@functools.wraps(原函數)修飾,這個函數用於把原來函數的一些屬性(__name__、__doc__一類)應用到新建的函數(wrapper)上。

修飾器應用流程:
以缺省參數delta=60*1000執行loop_call(),返回一個新函數decorator();
以參數func=get_btce執行decorator(),返回被wrap(get_btce)修飾的新函數wrapper做爲新的get_btce();
在get_btce()被調用時,會先執行func(也就是原來的get_btce()),而後用最外層的delta參數調用add_timeout()

代碼

整體來講效果就是這樣的,其他部分都是sample級的代碼因此很少說。

import tornado.web
import tornado.gen
import tornado.httputil
import tornado.escape
import tornado.httpclient
import datetime
import functools
 
current = {}
 
def loop_call(delta=60*1000):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            func(*args, **kwargs)
            tornado.ioloop.IOLoop.instance().add_timeout(
                datetime.timedelta(milliseconds=delta),
                wrapper,
            )
        return wrapper
 
   return decorator
 
# These two decorators must be applied in order
@loop_call()
@tornado.gen.coroutine
def get_btce():
    global current
    res = yield tornado.httpclient.AsyncHTTPClient().fetch(
            "https://btc-e.com/api/2/ltc_btc/ticker"
        )
    current['btce'] = tornado.escape.json_decode(res.body)['ticker']['last']
 
@loop_call()
@tornado.gen.coroutine
def get_btcc():
    global current
    res = yield tornado.httpclient.AsyncHTTPClient().fetch(
            "https://data.btcchina.com/data/ticker"
        )
    current['btcc'] = tornado.escape.json_decode(res.body)['ticker']['last']
 
@loop_call()
@tornado.gen.coroutine
def get_diff():
    global current
    res = yield tornado.httpclient.AsyncHTTPClient().fetch(
        "http://api.ltcd.info/difficulty"
    )
    current['diff'] = tornado.escape.json_decode(res.body)['current-difficulty']
 
class TickerHandler(tornado.web.RequestHandler):
    def get(self,key):
        global current
        self.write(str(current.get(key,"N/A")))
 
class RootHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Ticker")
 
tornado.web.Application([
    (r'/',RootHandler),
    (r'/(.*)',TickerHandler),
],debug=True).listen(8733)
 
get_btcc()
get_btce()
get_diff()
 
tornado.ioloop.IOLoop.instance().start()
相關文章
相關標籤/搜索