Google App Engine Python:スケジュール、キャッシュ、テンプレートによる効率的更新(1)。- cron.yaml
Google App Engine は、アプリケーションを決まった時刻に起動できるサービス「cron」を使用したスケジュール・タスクを提供する。
また、Google App Engine には、メモリ・オブジェクト・キャッシュ・システム「memchace」で、データをキャッシュできる仕組みが用意されている。
これらと template/テンプレート を併用することで、WEBページのコンテンツを効率よく更新することができる。
例えば、ランキングデータを取得して、ページに表示することを考えよう。
ランキングデータが1日に1回しか更新されない場合、ランキングデータの取得も1日に1回行えば十分である。そして、取得したデータを元に、テンプレートに埋め込む HTML の断片を生成して、memcache に保存する。ページへのリクエストが合ったときは、memcache から HTML の断片を取得して、これをテンプレートに埋めて表示する。
もし、cron を利用できなかったとしたら、ページへのリクエストがある都度、ランキングデータを取得しなければならない。同じ内容しか返ってこないのに、その度にランキングデータのリクエストを行うことは、そのリクエストを送る側も、リクエストに応える側にとっても、無駄な負荷を生じさせることになる。まったくの無駄だ。
これからもわかるように、タスク・スケジュール cron は大変に有用だ。
Google App Engine の「タスク・スケジュール 」は、app.yaml に記述する。
cron: - description: daily ranking job url: /cron/daily schedule: every day of month 00:12 timezone: Asia/Tokyo
description は、このタスクに付けた名前もしくは説明だ。
url と schedule は必須。schedule で定めた時刻や間隔で、url で示されるスクリプトが実行される。
timezone は省略可能。指定した timzone/タイムゾーン の時刻となる。timezone の指定がない場合は、UTC(GMT)となる。
上の例だと、毎日、日本時間の0時12分に /cron/daily にマップされたスクリプト
が実行される。
なお、リクエストに対するマッピングは、app.yaml で次のように記述しておく。
application: example version: 1 runtime: python api_version: 1 handlers: - url: /cron.daily script: daily.py login: admin
「login: admin」という設定は、管理者のみにアクセスを制限するもの。この設定をしておかないと、「http://example.appspot.com/cron/daily」に外部からアクセスがあった場合に、設定したスケジュール外で「daily.py」が実行されてしまうことになる。
単なるランキングデータの取得なら影響はないが、データをリフレッシュしてしまうようなスクリプトの場合には、期待通りの結果が実現できなくなる。外部からのリフレッシュを期待する場合を除いて、「login: admin」の設定にしておいたほうがよい。
schedule の書式
schedule の定め方には、間隔指定と時刻指定がある。
間隔指定
schedule: every N (hours|mins|minutes)
5分間隔なら "every 5 minutes"、半日間隔なら、"every 12 hours"
時刻指定
schedule: ("every"|ordinal) (days) "of" (monthspec) (time)
具体的な時刻指定の例。
#3月の第2及び第3の月曜、水曜、木曜の 17:00 2nd,third mon,wed,thu of march 17:00 #毎月曜の 09:00 every monday of month 09:00 #9,10,11月の第1月曜の 17:00 1st monday of sep,oct,nov 17:00
カンマ区切りで複数を指定できる。
(ordinal) 第何週か(first, または 1st など)。
(days) 曜日(manday, または mon など)。毎日は "every day" 。
(monthspec) 月(january, または jan など)。毎月は"month"。
(time) 24時間方式の「HH:MM」スタイル。
Python:xml.etree.ElementTree, 外部 RDF。
前回は、ローカルな RDFファイルの item 要素から title を取り出した。
今回は、外部の RDF を扱ってみる。
前回のソースコードで変えたのは次の部分。
rootTree=etree.parse(source)
parse メソッドは、内部のファイルを取り込めるが、外部ファイルは取り込めないようだ。
外部ファイルにアクセスするには、Google App Engine の場合、urlfetch がある。これを利用しよう。
そして、fetch したテキストに対して、etree の fromstring() メソッドを適用して、ElementTree オブジェクトを生成する。
サンプルで用いる外部 RDF は、何でもよいが、goo のランキング RDF を今回は使用する。
if __name__ == "__main__": from google.appengine.api import urlfetch import xml.etree.ElementTree as etree url='http://ranking.goo.ne.jp/rss/keyword/keyrank_all1/index.rdf' xml=urlfetch.fetch(url).content rootTree=etree.fromstring(xml) titles = rootTree.findall('.//{http://purl.org/rss/1.0/}title') for i in titles: print i.text.encode('utf-8','ignore')
Python:xml.etree.ElementTree, RDF titleの取得
前回の記事で、xml.etree.ElementTree モジュールを利用する際には、パス指定の名前空間部分が必要とわかったので、今回は、ローカルな RDFファイルから item 要素から title を取り出してみる。
rootTree.findtext('.//{http://purl.org/rss/1.0/}title')
if __name__ == "__main__": import xml.etree.ElementTree as etree source='sample.rdf' rootTree=etree.parse(source) titles = rootTree.findall('.//{http://purl.org/rss/1.0/}title') for i in titles: print i.text.encode('utf-8','ignore')
なお、item の title だけ取得したい場合は、次のとおり。
if __name__ == "__main__": import xml.etree.ElementTree as etree source='sample.rdf' rootTree=etree.parse(source) titles = rootTree.findall('.//{http://purl.org/rss/1.0/}item{http://purl.org/rss/1.0/}title') for i in titles: print i.text.encode('utf-8','ignore')
'./{http://purl.org/rss/1.0/}item{http://purl.org/rss/1.0/}title'でも
'/{http://purl.org/rss/1.0/}item{http://purl.org/rss/1.0/}title'でも
'{http://purl.org/rss/1.0/}item{http://purl.org/rss/1.0/}title'でも
OK。
次回は、urlfetch を使って、ネット上の RDF ファイルを扱う。
Python:xml.etree.ElementTree
RDF からデータを取り出したいと思い、はじめて xml.etree.ElementTree モジュールを利用しました。ハマったのは、パス指定の名前空間部分。
参考にしたコードサンプルでは「.//タグ名」で子孫の要素を参照できると書いてあった。そこで、title のテキストを取得しようと思い、次のように記述したのだが。
if __name__ == "__main__": import xml.etree.ElementTree as etree source = 'sample.rdf' rootTree = etree.parse(source) title = rootTree.findtext('.//title') print title.encode('utf-8','ignore')
何も出力されなかった。
rdf:RDF channel title link description iterms rdf:Seq rdf:li(rdf:resource) item item
そこで、イテレータで順番に各要素を参照してみた。
if __name__ == "__main__": import xml.etree.ElementTree as etree source='sample.rdf' rootTree=etree.parse(source) print rootTree # for i in rootTree.getiterator(): if i.tag:# print 'tag : '+ i.tag
なぜか 「print rootTree」のように1行文字列を出力しておかないと、それ以降のものが出力されない。
また、tag がないノードもあるので、「if i.tag:」のように条件を付与する必要がある。
なお、属性もみたいときは、次の文を加えればよい。
if i.attrib: print str(i.attrib)
上記のスクリプトを実行すると、次のような結果に。
<xml.etree.ElementTree.ElementTree instance at 0xe44490> tag : {http://www.w3.org/1999/02/22-rdf-syntax-ns#}RDF tag : {http://purl.org/rss/1.0/}channel tag : {http://purl.org/rss/1.0/}title tag : {http://purl.org/rss/1.0/}link tag : {http://purl.org/rss/1.0/}description tag : {http://purl.org/dc/elements/1.1/}publisher tag : {http://purl.org/dc/elements/1.1/}date tag : {http://purl.org/dc/elements/1.1/}language tag : {http://purl.org/rss/1.0/}items tag : {http://www.w3.org/1999/02/22-rdf-syntax-ns#}Seq tag : {http://www.w3.org/1999/02/22-rdf-syntax-ns#}li tag : {http://www.w3.org/1999/02/22-rdf-syntax-ns#}li (中略) tag : {http://purl.org/rss/1.0/}item tag : {http://purl.org/rss/1.0/}title tag : {http://purl.org/rss/1.0/}link tag : {http://purl.org/rss/1.0/}description tag : {http://purl.org/dc/elements/1.1/}creator tag : {http://purl.org/dc/elements/1.1/}publisher tag : {http://purl.org/dc/elements/1.1/}date tag : {http://purl.org/dc/elements/1.1/}rank tag : {http://purl.org/dc/elements/1.1/}point tag : {http://purl.org/dc/elements/1.1/}arrow tag : {http://purl.org/rss/1.0/}item (以下略)
上の結果、tag の値というのは、名前空間付であることがわかった。
一番最初のスクリプトを次のように書き換えた。
title = root.findtext('.//{http://purl.org/rss/1.0/}title')
ちゃんとタイトルが表示された。
名前空間を押さえなければいけないことがわかりました。
Google App Engine:ファイルの読み込み
Google App Engine のスタートガイドでは、「静的ファイルの使用」について説明がありますが、これ以外の方法でも、静的ファイルを表示する方法があります。それは、スクリプト内でファイルを読み込んで書き出す方法です。
これには、組み込み関数の open() を使います。
そして、read() を組み合わせれば、ファイル全体の読み込みができます。
"index.html" を読み込んで、表示する例を掲載します。
import cgi from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app class IndexPage(webapp.RequestHandler): def get(self): html = open('index.html').read() self.response.out.write(html) application = webapp.WSGIApplication([ ('/index.html', IndexPage), ], debug=True) def main(): run_wsgi_app(application) if __name__ == "__main__": main()
なお、外部スタイルシートやイメージファイル、JSファイルなどを利用する場合には、app.yaml 内で次のようにリクエストハンドラにマッピングしておく必要があります。css, img, js というディレクトリにそれぞれ css ファイル、画像ファイル、 JSファイルを置く場合の記述例を示します。
handlers: - url: /css/(.*) static_files: css/\1 upload: css/(.*) - url: /img/(.*) static_files: img/\1 upload: img/(.*) - url: /js/(.*) static_files: js/\1 upload: js/(.*)
Google App Engine:環境変数 HTTP_REFERER などを取得する。
os モジュールをインポートすると、環境変数が扱えます。
環境変数は、os.environ にマップ・オブジェクトとして格納されます。
また、os.getenv() メソッドは、第一引数に環境変数の名前を指定して、その値を取得できます。第二引数を指定すると、該当する環境変数が存在しない場合にその第二引数(指定しない場合は None)を返します。
Google App Engine でWEBアプリケーションを構築する場合、HTTP_REFERER などの環境変数を扱いたいときもあります。そのような時は、この os.getenv() メソッドを利用するといいでしょう。
開発用サーバーで試したスクリプトを掲載しておきます。
import cgi from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app import os class MainPage(webapp.RequestHandler): def get(self): _env = ( 'HTTP_ACCEPT' ,'HTTP_ACCEPT_CHARSET' ,'HTTP_ACCEPT_ENCODING' ,'HTTP_ACCEPT_LANGUAGE' ,'HTTP_CONNECTION' ,'HTTP_HOST' ,'HTTP_PRAGMA' ,'HTTP_REFERER' ,'HTTP_USER_AGENT' ,'QUERY_STRING' ,'CONTENT_TYPE' ,'GATEWAY_INTERFACE' ,'CONTENT_LENGTH' ,'REMOTE_ADDR' ,'REMOTE_HOST' ,'REMOTE_IDENT' ,'REMOTE_PORT' ,'REMOTE_USER' ,'REQUEST_METHOD' ) _listEnv = [] for i in _env: print i + ':' print os.getenv(i, '**Not Exist**') application = webapp.WSGIApplication([ ('/.*', MainPage) ], debug=True) def main(): run_wsgi_app(application) if __name__ == "__main__": main()
次のものが、"**Not Exist**" と表示されました。
- HTTP_ACCEPT_ENCODING
- HTTP_PRAGMA
- REMOTE_HOST
- REMOTE_IDENT
- REMOTE_PORT
- REMOTE_USER
Google App Engine:データストアに保存したテンプレートを使う。
Google App Engine のテンプレート・モジュールは、google.appengine.ext.webapp パッケージからインポートし、rendar() メソッドで、テンプレート・ファイルのパスとテンプレート・データを指定して実行するものです。
これはこれで、非常に簡潔で便利なものですが、テンプレート・ファイルを静的ファイルとしてアップロードしなければなりません。ちょっと直したいときには、少し不便です。もし、テンプレートの編集がブラウザ上で可能ならば、便利かと思います。
前回の「Google App Engine:django.template を使う。」で紹介した方法を用いれば、テンプレート・ファイルのパスを指定するのではなく、テンプレート文字列に直接、テンプレート・データを埋め込むことができます。
テンプレート文字列を取得する先として、データストアを利用すれば、ブラウザ上で編集が可能です。
このような考えで、データストアに保存したテンプレートを使って見たところうまくいきました。
テンプレートのためのデータストア用のクラス
まず、テンプレートのためのデータストア用のクラスを用意しましょう。
# MyTemplate.py from google.appengine.ext import db class MyTemplate(db.Model): title = db.StringProperty() content = db.TextProperty()
項目としては、最低限、タイトルと内容があれば十分です。
タイトル(title)には、「example.tpl」など、テンプレートを識別するための文字列を入れ、内容(content)には、テンプレートを入れます。
入力用フォーム
次に、これに入力するためのフォームを作成します。
<!-- MyTemplateForm.html --> <form action="/myTemplateCreate" method="post"> <input name="title" type='text' size="60" value='example.tpl' /> <textarea name="content" rows="3" cols="60"></textarea> <input type="submit" value="Submit"> </form>
テキストボックスには、django の記述方法に従ったテンプレートの内容を入力します。
テンプレート例
{% for entry in entries %} <div>entry: {{ entry.val }}</div> {% endfor %}
入力処理
このフォームに入力して送信します。
そうすると、「/myTemplateCreate」にリクエストが POST メソッドで送られます。
app.yaml でリクエストハンドラを次のように設定しておけば、「myTemplateCreate.py」にマッピングされます。
- url: /myTemplateCreate script: myTemplateCreate.py
「myTemplateCreate.py」の内容は、次のようなものでよいでしょう。
# MyTemplateCreate.py import cgi from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app from google.appengine.ext import db from myTemplate import MyTemplate class MyTemplateCreate(webapp.RequestHandler): def post(self): myTemplate = MyTemplate() myTemplate.title = self.request.get('title') myTemplate.content = self.request.get('content') myTemplate.put() self.redirect('/') #適当な先にリダイレクト application = webapp.WSGIApplication([ ('/myTemplateCreate', MyTemplateCreate), ], debug=True) def main(): run_wsgi_app(application) if __name__ == "__main__": main()
これで、入力したテンプレートが、データストアに保存されます。
後は、このテンプレートを呼び出して、テンプレート・データを埋め込むためのスクリプトを作成して、表示させます。
表示用スクリプト
そのスクリプト・ファイルを「showEntires.py」とでもしておきましょう。
「app.yaml」のリクエスト・ハンドラに次の記述を追加します。
- url: /showEntries script: showEntries.py
「showEntries.py」の内容は次のようなものになります。
# showEntries.py import cgi from google.appengine.ext import webapp from google.appengine.ext.webapp.util import run_wsgi_app from google.appengine.ext import db import django.conf try: django.conf.settings.configure( DEBUG=False, TEMPLATE_DEBUG=False, ) except (EnvironmentError, RuntimeError): pass import django.template from myTemplate import MyTemplate from entry import Entry class ShowEntries(webapp.RequestHandler): def get(self): template_all = MyTemplate.all() title="example.tpl" tpl = template_all.filter("title =",title).get().content t = django.template.Template(tpl) entries_all = Entry.all() entries_q = entries_all.order('-date') entries = entries_q.fetch(10) c = django.template.Context( {'entries':entries}) self.response.out.write(t.render(c)) application = webapp.WSGIApplication([ ('/showEntries', ShowEntries), ], debug=True) def main(): run_wsgi_app(application) if __name__ == "__main__": main()
テンプレート・データ
なお、ここにある「Entry」は、データストアに保存されている記事を指します。サンプルコードを実行する際には、データを保存しておき、テンプレートとのマッピングを行って下さい。
# entry.py from google.appengine.ext import db class MyTemplate(db.Model): val = db.StringProperty() date = db.DateTimeProperty(auto_now_add=True)