TwitterTrendInfo in Japan

TwitterAPIを使って、Twitterトレンド情報を取得し、一覧表示するWebアプリを公開しました。
GoogleAppEngine上に作成しています。
TwitterTrendInfo in Japan

・しくみ
1位時間に1回、Twitterトレンド情報を取得し、GAEのDataStoreに保存。
DataStoreから最新のデータを読込んでWebブラウザ上に一覧表示。

レスポンシブWebデザインをかじってみたので、スマートフォンでもストレスなく参照できます(たぶん)。

収集したデータがたまってきたら、集計してみようかな。

「Python + Tweepy」で、Twitterからのレスポンスの、HTTPヘッダを見たい。

Tweepyを使って、TwitterAPIを使ってみると、TwitterからのレスポンスとしてJSON形式データの内容を取得できます。
HTTPヘッダの内容もみたいんだけど、どこにあるのかわからない。

なぜ、HTTPヘッダの内容を見たいのかというと、アクセス制限の値をみたいから。
こちらのマニュアルによると、「HTTPヘッダにアクセスしたAPIに対するLimitが入ってる」、とのこと。
(参考)REST API Rate Limiting in v1.1 | Twitter Developers

X-Rate-Limit-Limit: the rate limit ceiling for that given request
X-Rate-Limit-Remaining: the number of requests left for the 15 minute window
X-Rate-Limit-Reset: the remaining window before the rate limit resets in UTC epoch seconds

APIの「GET application/rate_limit_status」を使えば、現状のLimitがわかるみたいなので、こちらを使ってもいい。
ただ、アプリのリトライ回数の制御とかを実装するときに、いちいち「GET application/rate_limit_status」を呼ぶのは、無駄にアクセス回数が増えてしまいイマイチよくない。

というわけで、Tweepyのソースを少しいじって、HTTPヘッダを取得してみた。

僕が使っている環境
Windows7(64Bit)
Python 2.7.3
Tweepy 2.0
Google App Engine SDK for Python 1.7.1
Aptana Studio 3.2.2


Tweepyのbinder.pyの中に「def bind_api」があります。
その中のメソッド(?)、def execute(self):の中で、最終的にresultが返却(179行目)されています。

このresultには、resp.read()をparseした結果が入っており、レスポンスの本文が抜き出されている模様です。なので、この中にはHTTPヘッダは含まれていません。
レスポンスの内容の全体は、147行目の「resp = conn.getresponse() 」にあるrespに入っています。

なので、def execute(self):の最後のreturnのところ(179行目)で、resultといっしょにrespも返却するようにしてみました。

            #return result
            return result,resp

実際に使うときは、こんな感じ。

以下は、フォローしている一覧を取得して、そのときのHTTPヘッダを取得してみた内容です。
screen_name=’******’ のところは、自分の名前に置き換えてください。
resp.getheaders()のところで、HTTPヘッダを表示しています。
(3/10追記 僕はGoogleAppEngine上で開発しているので、self.response.write()でWebブラウザにHTMLを返却して表示しています)

        auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
        auth.set_access_token(access_token, access_token_secret)
        api = tweepy.API(auth)

        (results,resp) = api.friends_ids(screen_name='******')

        self.response.write('resp.getheaders=%s<br>' % resp.getheaders())

HTTPヘッダを表示してみるとこんな感じになる。

resp.getheaders=[('status', '200 OK'), ('x-rate-limit-remaining', '10'), ('content-length', '141'), ('x-transaction', 'b709c8aa5837b816'), ('set-cookie', 'lang=ja, guest_id=v1%3A136255494281906933; Domain=.twitter.com; Path=/; Expires=Fri, 06-Mar-2015 07:29:02 UTC'), ('expires', 'Tue, 31 Mar 1981 05:00:00 GMT'), ('x-access-level', 'read-write'), ('server', 'tfe'), ('last-modified', 'Wed, 06 Mar 2013 07:29:02 GMT'), ('x-rate-limit-reset', '1362555233'), ('pragma', 'no-cache'), ('cache-control', 'no-cache, no-store, must-revalidate, pre-check=0, post-check=0'), ('date', 'Wed, 06 Mar 2013 07:29:02 GMT'), ('x-rate-limit-limit', '15'), ('x-frame-options', 'SAMEORIGIN'), ('content-type', 'application/json;charset=utf-8')]

きちんと、X-Rate-Limit-Limit、X-Rate-Limit-Remaining、X-Rate-Limit-Resetが取得できています。


3/10追記
個別の値を表示させるときは、
resp.getheader(‘x-rate-limit-limit‘)
といった感じ。

「GAE + python + tweepy」で、WARNING「Stripped prohibited headers from URLFetch request: [‘Host’]」が出力される件

GoogleAppEngine上でTwitterAPIにアクセスするアプリを作っていたら、
「Stripped prohibited headers from URLFetch request: [‘Host’]」が出力された。

調べてみたら、GAEでは、セキュリティ上の理由から、以下のHTTPヘッダは許可されていないようだった。
(参考)Stripped prohibited headers from URLFetch request: [‘Host’] · Issue #91 · tweepy/tweepy · GitHub

 
For security reasons, the following headers cannot be modified by the application:

    Accept-Encoding
        Content-Length
        Host
        Vary
        Via
        X-Forwarded-For

These headers are set to accurate values by App Engine, as appropriate.

僕が使っている環境
Windows7(64Bit)
Python 2.7.3
Tweepy 2.0
Google App Engine SDK for Python 1.7.1
Aptana Studio 3.2.2


Tweepyのソースを見てみると、binder.pyの67行目あたりで、

self.headers['Host'] = self.host

が設定されていた。おそらく、これが原因。
ためしに、コメントアウトしてみると、WARNINGが出なくなった。

ただ、少し悩んだ結果、そのまま(コメントアウトしないこと)にしておくことにした。
たとえば、ブラウザでインターネットに接続したとき、ブラウザから送信されるHTTPリクエストには、自動的に

Host: www.xxx.zzz

が設定される。
そして、実際にインターネットに接続された場合、プロバイダのホスト名に置き換わって、リクエストが送信される。
HTTPヘッダに「Host:~~」が設定されるのは、よくあることかなって思った。

あと、GAEの本番環境にアプリをUploadして動かしてみると、ログにWARNINGが出ていない。
なので、Tweepyのソースを回収しなくてもいいかなって、そういう結論に落ちついた。

GoogleAppEngineでの、Cronの書き方でハマった

GoogleAppEngine(for Python)では、cron.yaml にタスクをスケジューリングしておくことで、定期的にジョブ実行できます。
僕の場合、1時間に1回、ジョブ実行したいと思っています。

しかし、設定内容に、

  every 1 hours synchronized

と書くと、確かに1時間ごとに実行されるんだけれども、cron.yamlをGAE上にアップロードした時間にジョブが初回実行される。
そのため、毎回異なる開始時間でスタートしてしまう

時には15分からスタートしたり、
13:15
14:15

6分からスタートしたり、
13:06
14:06

僕の場合、1時間ごとに「**時10分”」に実行したい!!

が、マニュアルにはそれらしき記載が見当たらない・・・。
(マニュアル)Python 用クローンを使用したスケジュールされたタスク – Google App Engine — Google Developers

僕が使っている環境
Windows7(64Bit)
Python 2.5.4
Google App Engine SDK for Python 1.7.1
Aptana Studio 3.2.2


・やったこと
こう書くと、開始・終了時間を設定できます。

schedule: every 1 hours from 00:00 to 09:59

この場合、1時間ごとに**時00分に実行されます。
1日中動かす場合は、
from 00:00 to 23:59
と指定すればいい。

僕の場合、「**時10分”」に実行したい、あと、1日中動かし続けたい。
なので、設定内容をこうしました。

cron:
- description: GetTrend job
  url: /twitter_trend/get_trend
  schedule: every 1 hours from 00:10 to 00:09
  timezone: Asia/Tokyo

終了時間が翌日の00:09になるので、設定ファイルにどう書こうか迷ったけど、そのまま記載して問題なくCron実行できました。
あと、念のためTimeZoneも設定しておきました。

GoogleAppEngineでの、日本時間(JST)表示で微妙にハマった

GoogleAppEngineでは基本的にUTC(協定世界時)を使いますが、Web画面上に日時を表示したいので、JST(日本標準時)に変換する必要がありました。

いろいろ調べてみたら以下のやり方があって、

・カスタム datetime.tzinfo を書く
・ライブラリpython-dateutil を使って変換する
・ライブラリpytz を使って変換する
・Djangoテンプレートで変換する

すべてを試したわけでないんだけど、それぞれについて書いてみたいと思います。
(ちなみに、僕は最終的にdateutilを採用することにしました。)

僕が使っている環境
Windows7(64Bit)
Python 2.5.4
Google App Engine SDK for Python 1.7.1
Aptana Studio 3.2.2


(1)はじめに
まず僕がやろうとしていることを簡単に。

GoogleAppEngine上でPythonを使って、Twitterのトレンド情報を取ってみました。
取得したトレンド情報は、いったんDataStoreに保存しておきます。

でそのあとに、DataStoreからデータを取出して、ブラウザ上で一覧表示できるようにしました。
取得した情報には、集計時間がTimeZone UTCで記載されています。この集計時間を、TimeZone JSTに変換して表示させたい。

って感じ。


(2)カスタム datetime.tzinfo を実装してみる
GAEのマニュアルを見ると、カスタム datetime.tzinfo を実装すればいいって書いてあったので、とりあえずそれを試してみました。

(参考)
型とプロパティ クラス – Google App Engine — Google Developers
5.1.6 tzinfo Objects

が、マニュアルに記載されているサンプル内容(class Pacific_tzinfo(datetime_module.tzinfo))がさっぱりよくわからなかったので、こちらのページを参考にして、Class定義してみました。
(参考)例のあれ(仮題)- Google App EngineのタイムゾーンがUTCなので。

実際は参考にしたというより、そのままコピペしたんだけど、動作確認してみたコードを以下に載せておきます。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import datetime

#########################

class UtcTzinfo(datetime.tzinfo):
    def utcoffset(self, dt):
        return datetime.timedelta(0)

    def dst(self, dt):
        return datetime.timedelta(0)

    def tzname(self, dt):
        return 'UTC'

    def olsen_name(self):
        return 'UTC'

class JstTzinfo(datetime.tzinfo):
    def utcoffset(self, dt):
        return datetime.timedelta(hours=9)

    def dst(self, dt):
        return datetime.timedelta(0)

    def tzname(self, dt):
        return 'JST'

    def olsen_name(self):
        return 'Asia/Tokyo'

#################

print '-- 現在時刻を表示 ' + '-' * 20

print u'tzinfo なし=%s' % datetime.datetime.now()
print u'tzinfo JST=%s' % datetime.datetime.now(JstTzinfo())
print u'tzinfo UTC=%s' % datetime.datetime.now(UtcTzinfo())

print '-- JSTにセットして表示 ' + '-' * 20
dt= datetime.datetime.strptime('2013-02-23T09:59:13Z', "%Y-%m-%dT%H:%M:%SZ")
dt_JST = dt.replace(tzinfo=UtcTzinfo()).astimezone(JstTzinfo())

print 'dt    =%s' % dt
print "dt_JST=%s" % dt_JST

print '=' * 20

###############

時間をセットする処理のところで、日時のフォーマットが「2013-02-23T09:59:13Z」になっているんだけど、あまり気にしないように。
これはTwitterTrendAPI側から返ってくる日時フォーマットに合わせて書いているだけです。
このコードを実行するとこんな感じになります。

-- 現在時刻を表示 --------------------
tzinfo なし=2013-03-02 00:54:29.299000
tzinfo JST=2013-03-02 00:54:29.299000+09:00
tzinfo UTC=2013-03-01 15:54:29.299000+00:00
-- JSTにセットして表示 --------------------
dt    =2013-02-23 09:59:13
dt_JST=2013-02-23 18:59:13+09:00
====================

とりあえずやりたいことはできたけど、やっぱりライブラリ使ったほうがいいかなっておもった。
JST以外のタイムゾーンを使いたくなった時に、いちいちClassを書くのがめんどいし、書き方もよくわからん。


(3)ライブラリpython-dateutil を使ってみる
python-dateutilのダウンロードはこちらから。
python-dateutil 2.1 : Python Package Index

ただし、dateutilのバージョン2.**は、Python3用なので、上記のサイトからこちらのdateutilホームページに行き、python-dateutil-1.5.tar.gz をダウンロードします。
python-dateutil – Labix

ダウンロードしたファイルを展開すると、dateutilというフォルダがあるので、これをGAEのプロジェクトフォルダ直下へコピーして使います。
動作確認してみたコードを以下に載せておきます。

#!/usr/bin/python
# -*- coding: utf-8 -*-

import datetime
import dateutil.tz

#########################

print '-- 現在時刻を表示 ' + '-' * 20

print u'tzinfo なし=%s' % datetime.datetime.now()
print u'tzinfo JST=%s' % datetime.datetime.now(dateutil.tz.gettz('Asia/Tokyo'))
print u'tzinfo UTC=%s' % datetime.datetime.now(dateutil.tz.tzutc())

print '-- JSTにセットして表示 ' + '-' * 20
dt= datetime.datetime.strptime('2013-02-23T09:59:13Z', "%Y-%m-%dT%H:%M:%SZ")
dt_Tokyo = dt.replace(tzinfo=dateutil.tz.tzutc()).astimezone(dateutil.tz.gettz('Asia/Tokyo'))

print 'dt      =%s' % dt
print "dt_Tokyo=%s" % dt_Tokyo

print '=' * 20

日時のフォーマット「2013-02-23T09:59:13Z」は、TwitterTrendAPI側から返ってくる日時フォーマットに合わせて書いています。
このコードを実行するとこんな感じになります。

 
-- 現在時刻を表示 --------------------
tzinfo なし=2013-03-02 01:23:49.375000
tzinfo JST=2013-03-02 01:23:49.637000+09:00
tzinfo UTC=2013-03-01 16:23:49.637000+00:00
-- JSTにセットして表示 --------------------
dt      =2013-02-23 09:59:13
dt_Tokyo=2013-02-23 18:59:13+09:00
====================

Djangoテンプレート側では、以下の書き方でDateTime型のフォーマットを変えられます。

{{dt_Tokyo|date:"Y/n/d G:i T(O)"}}

この場合、2013/3/01 1:07 JST(+0900) って表示されます。

(参考)
オトなか: DjangoでDateTime型をテンプレート内でフォーマッティングする方法
Built-in template tags and filters | Django documentation | Django


(4)ライブラリpytz
とりあえず僕はdateutilを使うことにしたので、pytzは試していません。
ただpytzをGAE上で使うときの注意点があるみたいなので、そのへんを書いておきます。

pytzのダウンロードはこちらからできます。
pytz 2012j : Python Package Index

ただし、上記のもの(pytz 2012j)はGAE上で使うのはダメです。GAE上ではこちらのものを使う必要があります。
gaepytz 2011h : Python Package Index

このgaepytz 2011hのページに記載があるんですが、「pytz 2012jはGAE上でパフォーマンスの問題がある」とのことです。
pytz has a severe performance problem that impedes its usage on Google App Engine.

ためしにダウンロードしてみるとわかるんですが、pytz 2012j のほうは、zoneinfoファイルが500個以上あります。これを毎回、読みにいってしまいパフォーマンスに影響する模様。(GAE上で使うときだけ?問題が起きるのはなぜかなのか、という素朴な疑問がわいたけど、よくわからない)
(gaepytz 2011hのほうは、zoneinfoファイルがzip圧縮されています。)

あと、gaepytz 2011hを使うときは、こんな感じでインポートするとのこと。
from pytz.gae import pytz

pytzの使い方は、このへんのサイトが参考になります。
PythonでJST日付をUTC(GMT)に変換する。: 俺の砂箱
Pythonのタイムゾーンを扱うモジュールpytzインストールメモ | JWAVER


(5)Djangoテンプレートで変換する

DjangoのCustom template filters機能と思うんだけど、こちらにやり方が載っています。

技術メモ: Google App Engineで簡単に日本時間(JST)を扱う方法

自分でタイムゾーンを変換するClass定義しておき、DjangoのTemplateから処理を呼び出しています。

GoogleAppEngineでDjangoのバージョン上げたらUnicodeEncodeError発生した

Google App Engine では、Django: 1.2, 1.1, 1.0, 0.96が使える。で、SDKには、Django 1.2と 0.96 が含まれていて、SDKでは通常0.96が使われています。

(参考)Third-party Python Libraries in Python 2.5 – Google App Engine — Google Developers
The App Engine Python environment includes these versions of Django: 1.2, 1.1, 1.0, and 0.96. Django 1.2 and 0.96 are included with the App Engine SDK. Legacy Django version 0.96 is currently imported by default when an app imports the django package.

この状態で、GoogleAppEngineの本番環境にアプリをUploadして動かしていると、ログにこんなWarningメッセージが出ていた。

You are using the default Django version (0.96). The default Django version will change in an App Engine release in the near future. Please call use_library() to explicitly select a Django version. For more information see

近いうちにバージョン上がりますって話。
というわけで、自分の環境で使っているDjangoバージョンを1.2に上げたら、テンプレートの描画の処理のところでUnicodeEncodeErrorが出て、ブラウザからWeb画面がまったく見れなくなった。。

エンコード変換の記述をなおしたら、正常に戻ったんだけど、
実はこのUnicodeEncodeErrorについては、Django 0.96の時にも発生したことがあって、その時にOKだった対処がダメになって、ちょっとなやまされた。

僕が使っている環境
Windows7(64Bit)
Python 2.5.4
Google App Engine SDK for Python 1.7.1
Aptana Studio 3.2.2


(1)はじめに

Pythonで日本語を取り扱う場合はUnicode文字列をつかうことになります。
文字コードUTF-8を使っているときは、uをつけて
print u‘Helloと表示’

とか書きます。

GoogleAppEngineで、とりあえずPrint文みたいなことをする場合、self.response.out.write()を使いますが、そこでもuをつければOKです。
self.response.out.write(u‘パラメータは=%s<br>\n’ % param)

といった感じ。

今回僕がハマったのは、
self.response.out.write(template.render(path, template_values))

のところ。Django テンプレートを使用してHTMLを描画する部分です。

エラー内容はこんな感じ。

UnicodeEncodeError: 'ascii' codec can't encode characters in position 69-79: ordinal not in range(128)

これのやっかいなところは、自分のPC上(Google App Engine SDK)では、エラーが起きないってこと。
Webの本番環境でのみ発生する。


(2)Django 0.96での対処

テンプレートで使用している変数の中身が、ユニコードで格納されていないのか? って思ってハマったんだけど、結局、template.renderのところに、decodeをつけたら正しく動いた。

self.response.out.write(template.render(path, template_values).decode('utf-8'))

なぜencode(‘utf-8’)じゃないんだろう? という素朴な疑問がわいたけど、とりあえずそれ以上悩むのはやめた。


(3)Django 1.2での対処

Djangoのバージョン変更のやり方は、先に記載したページに載っている。

(参考)Third-party Python Libraries in Python 2.5 – Google App Engine — Google Developers

「アプリのソースコードに文言を記載して、そのモジュールのみに対応させるやり方」と、
「ファイルに設定してアプリ全体に適用させるやり方」がある模様。
とりあえず僕は、アプリ全体に適用することした。

やり方は、
アプリケーションのルート ディレクトリ(app.yaml とおなじ場所)に、appengine_config.py というファイルを作成し、以下の内容を記述する。

webapp_django_version = '1.2'

その後、アプリを動かすと、UnicodeEncodeErrorが起きた。今回もまた、
自分のPC上(Google App Engine SDK)では、エラーが起きない、Webの本番環境でのみ発生する。

悩まされてた結果、decodeをやめてencodeにしたら動いた。というかエンコード処理を書かなくても動く。

self.response.out.write(template.render(path, template_values))
もしくは
self.response.out.write(template.render(path, template_values).encode('utf-8'))

どちらでもOK

なにこれ。バグが治ったってこと?

チュートリアルを見ても、このあたりの記述が見つけれられなかったので、
とりあえず僕は、encodeの記述なしで行くことにした。
template.render(path, template_values).encode(‘utf-8’)

僕は、Python 2.5.*を使っているので、こちらも早めにPython 2.7に上げておいたほうがよさそう。