Tornadoを使ってみる

非同期HTTPClient


Tornado独特の機能にHTTPClientが有ります。
TornadoはWEBサーバーですが、HTTPClient機能を使って、こちらの サーバーにデータを要求し、その結果を表示することができます。
全国の天気予報がJSONフォーマットとして公開されています。


pythonスクリプトは以下の通りです。
URLパラメータが無いときは、city=400040(福岡県久留米)で天気予報を取得します。
#
# Basic Synchronous Call
#
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.httpclient
import os.path

import urllib
import json
import datetime
import time

from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            (r"/", IndexHandler),
        ]
        settings = dict(
            template_path=os.path.join(os.path.dirname(__file__), "templates"),
            debug=True,
            )
        tornado.web.Application.__init__(self, handlers, **settings)

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        city = self.get_argument('city', '400040')
        print("city=[{}]".format(city))
        print("len(city)={}".format(len(city)))
        url = "http://weather.livedoor.com/forecast/webservice/json/v1"
        if (len(city) != 0): url = url + "?city=" + city
        print("url={}".format(url))

        client = tornado.httpclient.HTTPClient()
        response = client.fetch(url)
        print("response={}".format(response))
        data = json.loads(response.body)
        print("data={}".format(data))
        print(json.dumps(data, indent=2))

        print()
        print()
        print(data['location'])
        print(data['location']['city'])
        city=data['location']['city']
        print(data['location']['prefecture'])
        prefecture=data['location']['prefecture']
        print(data['location']['area'])
        area=data['location']['area']
        print(data['publicTime'])
        publicTime=data['publicTime']
        dateTime=publicTime.split('T')
        print("dateTime={}".format(dateTime))
        date=dateTime[0]
        time=dateTime[1]
        time=time.split('+')
        print("time={}".format(time))
        print(data['description']['text'])
        text=data['description']['text']

        self.render(
            "index.html",
            area = area,
            prefecture = prefecture,
            city = city,
            date = date,
            time = time[0],
            text = text,
        )

if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = Application()
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

テンプレート(index.html)は以下の様に簡単なものです。
<html>
   <head>
      <title>今日のお天気</title>
   </head>

   <body>
      <h1>{{ area }} {{ prefecture }} {{ city }}</h1>
      <h2>{{ date }} {{ time }}</h2>
      {{ text }}
   </body>
</html>

URLパラメータが指定されたときは、指定された都市の天気予報を取得します。




TornadoのVersion5.0以降ではHTTPClient()が正しく動きません。
HTTPClient()を実行すると以下のエラーとなります。

RuntimeError: Cannot run the event loop while another loop is running


Tornadoのバージョンは以下で確認することができます。
if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = Application()
    http_server = tornado.httpserver.HTTPServer(app)
    print("version={}".format(tornado.version))
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

もし、Version5.0以上がインストールされているときは、以下のコマンドでバージョンダウンすることができます。
$ pip uninstall tornado
$ pip install tornado==4.5.3



Tornadoの最大の特徴はAsyncronous(非同期)通信をサポートしていることです。
上の例はclient.fetch()で応答が有るまで待ちますが、以下の例はコールバックで応答を処理します。
client.fetch()でハンドラー(on_response)を指定します。
応答が来るまでの間、他の処理を行うことができます。
結果は上と同じになります。
#
# Basic Asynchronous Call
#
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.httpclient
import os.path

import urllib
import json
import datetime
import time

from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            (r"/", IndexHandler),
        ]
        settings = dict(
            template_path=os.path.join(os.path.dirname(__file__), "templates"),
            #static_path=os.path.join(os.path.dirname(__file__), "static"),
            debug=True,
            )
        tornado.web.Application.__init__(self, handlers, **settings)

class IndexHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        city = self.get_argument('city', '400040')
        print("city=[{}]".format(city))
        print("len(city)={}".format(len(city)))
        url = "http://weather.livedoor.com/forecast/webservice/json/v1"
        if (len(city) != 0): url = url + "?city=" + city
        print("url={}".format(url))

        client = tornado.httpclient.AsyncHTTPClient()
        client.fetch(url, callback=self.on_response)

    def on_response(self, response):
        print("response={}".format(response))
        data = json.loads(response.body)
        print("data={}".format(data))
        print(json.dumps(data, indent=2))

        print()
        print()
        print(data['location'])
        print(data['location']['city'])
        city=data['location']['city']
        print(data['location']['prefecture'])
        prefecture=data['location']['prefecture']
        print(data['location']['area'])
        area=data['location']['area']
        print(data['publicTime'])
        publicTime=data['publicTime']
        dateTime=publicTime.split('T')
        print("dateTime={}".format(dateTime))
        date=dateTime[0]
        time=dateTime[1]
        time=time.split('+')
        print("time={}".format(time))
        print(data['description']['text'])
        text=data['description']['text']

        self.render(
            "index.html",
            area = area,
            prefecture = prefecture,
            city = city,
            date = date,
            time = time[0],
            text = text,
        )

if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = Application()
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()



Asyncronous(非同期)通信は大変便利な機能ですが、問い合わせを2回以上行うときは
以下の様にコールバックの連続となります。
{前略}
class IndexHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        client = tornado.httpclient.AsyncHTTPClient()
        client.fetch(url1, callback=self.on_response1)

    def on_response1(self, response):
        data = json.loads(response.body)
        client = tornado.httpclient.AsyncHTTPClient()
        client.fetch(url2, callback=self.on_response2)

    def on_response2(self, response):
        data = json.loads(response.body)
        client = tornado.httpclient.AsyncHTTPClient()
        client.fetch(url3, callback=self.on_response3)

    def on_response3(self, response):
        data = json.loads(response.body)

if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = Application()
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

TornadoにはGenerator-based coroutines(gen.coroutine)という機能が有ります。
以前のバージョンではgen.engineという実装でしたが、こ ちらの理由でV3.0.0からgen.coroutineに変わりました。
互換性を取るためにgen.engineもまだ使えますが、Tornadoの公式ドキュメントからはgen.engineの記載は無くなっていま す。

この機能を使うと簡単に非同期通信を行うことができます。
yieldを使用すると、プログラムの制御がTornadoに返され、HTTPリクエストの進行中に他のタスクを実行できるようになります。
HTTP要求が終了すると、RequestHandlerメソッドは中断したところから再開します。
Generator-based coroutines(gen.coroutine)を使えば、コールバックの連続を避けることができます。
#
# Basic Asynchronous Generators
#
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.httpclient
import os.path

import urllib
import json
import datetime
import time

from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            (r"/", IndexHandler),
        ]
        settings = dict(
            template_path=os.path.join(os.path.dirname(__file__), "templates"),
            #static_path=os.path.join(os.path.dirname(__file__), "static"),
            debug=True,
            )
        tornado.web.Application.__init__(self, handlers, **settings)

class IndexHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    @tornado.gen.coroutine
    #@tornado.gen.engine
    def get(self):
        city = self.get_argument('city', '400040')
        print("city=[{}]".format(city))
        print("len(city)={}".format(len(city)))
        url = "http://weather.livedoor.com/forecast/webservice/json/v1"
        if (len(city) != 0): url = url + "?city=" + city
        print("url={}".format(url))

        client = tornado.httpclient.AsyncHTTPClient()
        #response = client.fetch(url)
        #response = yield tornado.gen.Task(client.fetch,url)
        response = yield client.fetch(url)
        print("response={}".format(response))
        data = json.loads(response.body)
        print("data={}".format(data))
        print(json.dumps(data, indent=2))

        print()
        print()
        print(data['location'])
        print(data['location']['city'])
        city=data['location']['city']
        print(data['location']['prefecture'])
        prefecture=data['location']['prefecture']
        print(data['location']['area'])
        area=data['location']['area']
        print(data['publicTime'])
        publicTime=data['publicTime']
        dateTime=publicTime.split('T')
        print("dateTime={}".format(dateTime))
        date=dateTime[0]
        time=dateTime[1]
        time=time.split('+')
        print("time={}".format(time))
        print(data['description']['text'])
        text=data['description']['text']

        self.render(
            "index.html",
            area = area,
            prefecture = prefecture,
            city = city,
            date = date,
            time = time[0],
            text = text,
        )

if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = Application()
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

続く...