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: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)