Tornado 实现web网页爬虫
最近在研究BitCoin,有一个Excel想要自动更新一些数据,想了一下拿VBA写还不如搭个服务器用Python写主要逻辑来得简单方便,于是先用Python写个爬虫整理好数据再拿给Excel吃好了。
准备
Tornado
不管大家认不认识Tornado,还是简要地介绍一下吧。
Tornado是Facebook的一个开源的高性能Web服务器,最大的优势在于它基于事件的设计,使得它在IO密集型(比如代理或爬虫)应用上有着基于线程web框架无法比拟的巨大优势。
除此之外Tornado有一个非常赞的设计(当然Twisted等别的事件框架也有),基于Generator的协程模型。具体可参见这里,官方的example浅显易懂,一眼就可以看出协程比回调(包括闭包)的优势。
数据源
几乎所有的CryptoCoin相关网站都会提供很方便的Web API,另外也有一些第三方的API接口供使用。
有趣的是,这些API几乎全部都是Json格式的(究其原因主要是Bitcoin客户端就使用的Json表示,于是programmer更倾向于这种方式),所以使用起来很方便。
演进
下面我们以btc-e的ticker API为例讲述一下代码演进的过程,对比一下几种实现方式的区别。
同步
def get_btce(): global current res = tornado.httpclient.HTTPClient().fetch( "https://btc-e.com/api/2/ltc_btc/ticker" ) current['btce'] = tornado.escape.json_decode(res.body)['ticker']['last']
最简单的同步写法。注意这种写法是不可以实际使用的,因为同步请求会阻塞当前线程,而一般来说所有的事件处理在同一个线程,所以会导致整个服务器的所有其余动作(包括客户端请求的处理)都要在这个请求返回之后才可以被处理。
回调
def get_btce(): def callback(res): global current 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,从Python 3.0开始gen可以吃Future所以导致写起来比之前更简洁。
@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']
会发现这个已经和同步几乎一样了,除了添加了一个修饰器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变量,用户代码继续执行。
重复执行
我们需要在执行完一次请求之后等待1分钟再次请求,在线程写法中time.sleep()可以很好地解决这个问题,但是事件写法的话就不可以sleep了(和上面同步请求一样会阻塞)。
于是Tornado提供了一个方法叫IOLoop.instance().add_timeout(),这个方法吃一个回调函数,然后可以指定在某时间或经过多长时间后执行这个函数。于是我们让它吃自己就可以了。
@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'] tornado.ioloop.IOLoop.instance().add_timeout( datetime.timedelta(milliseconds=delta), get_btce() )
修饰器
但是呢,每次写起来会很麻烦,本着“以代码重用为荣,以复制粘贴为耻”的精神,我们把这个功能抽出来,用有着“Python两大黑科技”的修饰器(另一个是metaclass)来实现。
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
这是一个典型的三层(带参)修饰器写法,使用如下(注意@coroutine要放在@loop_call下面)
@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']
修饰器分为带参和无参两种,前者使用@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级的代码所以不多说。
#!/usr/bin/env python # -*- coding: utf-8 -*- 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://blog.mimvp.com" ) 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()
版权所有: 本文系米扑博客原创、转载、摘录,或修订后发表,最后更新于 2014-04-28 13:46:37
侵权处理: 本个人博客,不盈利,若侵犯了您的作品权,请联系博主删除,莫恶意,索钱财,感谢!
转载注明: Tornado 实现web网页爬虫 (米扑博客)