ラベル python の投稿を表示しています。 すべての投稿を表示
ラベル python の投稿を表示しています。 すべての投稿を表示

2010-12-03

Google App Engine SDK がアップデート (1.3.8 → 1.4.0)

2010-12-02 付けで Python 版 Google App Engine SDK がバージョンアップしている。iMac で作業をしていると、Google アプリのアップデータが起動して、GoogleAppEngineLauncher.app が更新されたと告げられた。そのままアップデートを実行。Java 版のことはわからないが、公式サイトからダウンロード可能な SDK のバージョンは Python 版と同じ 1.4.0 になっている。

で、何が変わったかを調べようと公式ドキュメントのページから Release Note を開いた。以下に、ざっくりと超直訳してみる。誤解、曲解、いろいろふくまれている可能性がある。訳の品質に期待しすぎないように。

  • Always On 機能はアプリが 3 つのインスタンスを常時実行状態のまま保持することのできる機能だ(課金対象)。アプリの遅延を著しく削減することができる。
  • 開発者は Warmup リクエストを有効にできるようになった。アプリの app.yaml 中でハンドラを指定すると、App Engine はアプリの新しいインスタンスがユーザからのリクエストを受け付け始める前に、その初期化のために Warmup Request を送ろうとする。これにより、エンドユーザがアプリの初期化にともない感じる遅延を削減できる。
  • Channle API がすべてのユーザで利用できるようになった。
  • Task Queue が公式にリリースされ、もはや実験的な機能ではなくなった。'labs' を使った API のインポートパスは非推奨となった。Task Queue で使うストレージはアプリ全般のストレージ割り当て量として計量され、課金の対象となる。
  • Task Queue と Cron リクエストのデッドラインが 10 分に引き上げられた。それらのリクエスト中であっても、データストアと API のデッドラインは以前から変更されていない。
  • Task Queue に対して、開発者は queue.yaml でタスクの retry_parameters を指定できる。
  • 課金を有効にしているアプリでは Task Queue API で待ち行列(queue)を 100 個まで使える。
  • データストアに対して種別、名前空間および実体のプロパティを問い合わせるメタデータクエリが使えるようになった。
  • URLFetch ではレスポンスのサイズとして 32 MB まで許されるようになった。リクエストのサイズは引き続き 1MB までとなっている。
  • イメージ API に対するリクエストとレスポンスのサイズが 32BM に増やされた。Memcach のバッチ操作の合計サイズが 32 MB に増やされた。Memcache の個別のオブジェクトに対する 1MB の制限は引き続き適用される。
  • 送信メールへの添付のサイズが 1MB から 10MB に増やされた。受信メールのサイズ制限は引き続き 10MB となっている。
  • データストア上のバッチ方式による get/put/delete の操作に対する大きさと数量の制限は取り除かれた。個別の実体は引き続き 1MB に制限されているが、データストア全体に対するデッドラインに余裕があれば、バッチ方式で望むだけ多くの実体を同時に get/pub/delete 処理することができる。
  • クエリー結果をもとに反復する場合、データストアサービスは非同期に結果を先読みするようになった。これにより遅延を 10 - 15 % 削減できる場合がある。
  • 管理コンソールの Blacklist ページは拒絶された訪問者の上位の一覧を表示する。
  • 画像のサムネイルの自動生成サービスは 1600px までの任意の切り取りサイズをサポートする。
  • 管理コンソールに表示される全般的なインスタンス遅延の平均値は、インスタンスごとの QPS に応じた平均値になった。
  • アプリのあるバージョンをアップロードした開発者は appcfg.py download_app コマンドを使ってそのバージョンのコードをダウンロードできる。この機能はアプリごとに管理コンソールの Permissions タブで無効にできる。一度無効にすると、この機能を再度有効にすることはできない。
  • 独自ドメインで Google Appes を使っているユーザに対して、カスタム管理コンソールのページが機能していなかった問題を修正。
  • Python 実行環境では、リクエストハンドラが DeadlineExceededError を起こした場合は、インスタンスは強制終了された後に再開される。これは、Django を使っているときに SystemErrors が周期的に起きる問題と関連して修正されるべきだ。
    http://code.google.com/p/googleappengine/issues/detail?id=772
  • webapp.template とピュア Django を混在させたときに起きる Django のバージョンの不一致を避けるため、Django の初期化を appengine_config.py に移動できるようになった。
    http://code.google.com/p/googleappengine/issues/detail?id=1758
  • SSL 上の OpenId の問題を修正。
    http://code.google.com/p/googleappengine/issues/detail?id=3393
  • dev_appserver で login/logout のためのコードが Python 2.6 で動かない問題を修正。
    http://code.google.com/p/googleappengine/issues/detail?id=3566
  • dev_appserver で get_serving_url が透明で長さの足りない(cropped) PNG に対して機能しない問題を修正。
    http://code.google.com/p/googleappengine/issues/detail?id=3887
  • DatastoreFileSub の問題を修正。
    http://code.google.com/p/googleappengine/issues/detail?id=3895

Blogger Glass に関係する変更としては、Task Queue が公式に GAE のサービスの一部としてリリースされたことぐらいのようだ。API が変わったとは書いていないから、今までのコードはそのまま動くってことだろう。とりあえず、taskqueue の import から labs のネームスペースを削除しておいた。appspot に配備ずみ。

関連リンク

関連記事

2010-11-17

タスクキューを使う - 内部リンクを置き換える #3 (Blogger Glass)

内部リンクの置き換えをデータストアを使って単純に実装すると、「一覧画面」表示リクエストの処理に時間がかかるようになってしまい(データストアへの書き込みが発生するときのみ)、GAE の管理コンソール(の Current Load)に赤文字が出るようになってしまった(→「CPU 使い過ぎ? - GAE 管理コンソール上の警告」参照)。

今回は、GAE のタスクキュー API を使って「一覧画面」リクエストの処理時間を短くすることに挑戦してみた。

タスクキュー API の使い方

GAE でタスクキュー API を利用して何らかの処理を行う際には以下の 3 つが必要になる。

  1. タスクを登録するキューの設定
  2. タスクを処理するハンドラの定義
  3. タスクを登録するコードの追加

以下で、これらを順番に今回の実装を例に説明する。ただし、公式ドキュメントにもあるように、タスクキュー API はまだ experimental 扱いなので、この先、仕様変更の可能性は高い。ここに書かれている内容もいつまでも正しいとは限らない。

タスクを登録するキューの設定

GAE のタスクキュー API で使用するキューには、名前(name)、処理速度(rate)、実行量(bucket_size) を設定することができる。詳細については「Python Task Queue Configuration」を参照。設定は queue.yaml という名前の YAML ファイルに記述する。1 つの GAE アプリが持つことのできるキューは 10 までとなっている。

タスクキューにはデフォルトキューが用意されており、これを(デフォルトの設定のままで)使う分には特に設定しなくて良い(つまり、queue.yaml を作らなくても良い)。以下にデフォルトキューの(デフォルトの)設定を示す。

Default Queue Settings
項目設定値
name default
rate 5/s
bucket_size 5

簡単に言うと、デフォルトキューでは 1 秒間に 5 つのタスクを処理するようになっている。

今回の実装では、デフォルトキューをデフォルトのまま使っている。このため queue.yaml は作っていない。

タスクを処理するハンドラの定義

タスクを処理するコードは、通常のリクエストハンドラと同様に定義する(google.appengine.ext.webapp.RequestHandler を継承)。タスク登録時に指定しない限り、タスクは HTTP の POST メソッドで処理される。よって、タスクのハンドラでは最低限 post() 関数を定義することになる。

また、他のリクエストハンドラと同様に app.yaml にも登録する。以下は今回の実装で追加した定義だ。login: admin はリクエスト実行にアプリ管理者の権限を要求する設定。こうすることで、ユーザがタスクハンドラを直接実行することを防ぐことができる(タスクハンドラを呼び出すタスクキューは管理者権限を持っている)。

- url: /store_pol
  script: store_pol.py
  login: admin

今回、実装したタスクハンドラは以下の通り。リクエストパラメータとして permalink と Blog ID を受け取り、さらにひもづけられた Post ID を memcache から取り出して、データストアに保存している。また、データストアへの書き込みはトランザクションとして実行するようになっている。これはタスクが並列で処理されるからだ。

model.bind_post_id_with_permalink() 関数は以前に示したものから変わっていない。

# store_pol.py: process tasks to store a pair of links to the data store.

from google.appengine.api import memcache
from google.appengine.ext import db
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app

import model
import util

class StorePolHandler(webapp.RequestHandler):
    def post(self):
        permalink = self.request.get('permalink')
        blog_id = self.request.get('blog_id')
        post_id = util.get_post_id_from_memcache(permalink, blog_id)
        if post_id:
            def txn():
                model.bind_post_id_with_permalink(permalink, blog_id, post_id)
            db.run_in_transaction(txn)

def main():
    application = webapp.WSGIApplication([('/store_pol', StorePolHandler)],
                                         debug=True)
    run_wsgi_app(application)


if __name__ == '__main__':
    main()
タスクを登録するコードの追加

タスクを登録するには google.appengine.api.labs.taskqueue.add() 関数を使う(デフォルトキューの場合)。今回の実装では、permalink と Post ID の「ひもづけ情報」を登録する関数にこれを組み込んでいる(src/util.py 185 〜 186行目)。

def register_pair_of_links(entry):
    """
    Register a binding of a permalink and a post_id.
    The registration won't change, since the binding must be permanent.
    """
    permalink = entry.get_html_link().href
    blog_id = entry.get_blog_id()
    post_id = entry.get_post_id()
    # save the pair into the memcache, if it hasn't been stored.
    if memcache.get(permalink) is None:
        memcache.add(permalink, (blog_id, post_id))
    # issue a task, if the pair hasn't been stored yet in the data store.
    if model.get_post_id(permalink, blog_id) is None:
        taskqueue.add(url='/store_pol',
                      params={'permalink': permalink, 'blog_id': blog_id})

引数 url がタスクを処理するハンドラの URL 、params はハンドラに渡されるパラメータだ。また、URL は通常のリクエスト URL と同様、app.yaml に定義したタスクハンドラの URL と一致していなければならない。

結果はどう?

上記のバージョンを GAE に配備して動かしてみたところ、「一覧画面」の表示にかかる時間は確かに改善された。管理コンソールの赤文字も消えた。実装を(少し)複雑にしただけの効果はあったようだ。

結局、GAE で実用的なアプリを作るには、memcache - taskqueue - datastore の三段構えは必須なのだ。だからこそ、標準で用意されているのだろう(taskqueue はまだ experimental となっているけど)。

参考文献

Programming Google App Engine
Dan Sanderson
Oreilly & Associates Inc ( 2009-11-15 )
ISBN: 9780596522728

関連リンク

関連記事

2010-11-15

内部リンクを置き換える #2 (Blogger Glass)

#1 のデザイン(設計)にしたがい、最小限の機能を実装できた。

実装

permalink と Post ID をひもづける

ひもづけ情報としてデータストアに保存するのは、(1) permalink、(2) Blog ID、(3) Post ID の 3 つ。このうち、(1) をデータ実体へのキーに使う。というのも、この情報を利用するときには permalink に一致する Post ID を引き出すことになるからだ。permalink をキーにしておけばデータストアに問い合わせるクエリを組み立てる必要がない。

以下が、「ひもづけ情報」を保存するデータ実体の定義と、それを利用するための関数だ(src/model.py)。

class PairOfLinks(db.Model):
    """
    PairOfLinks binds a permalink to a post_id of a blog specified
    with a blog_id.
    Each entity of PairOfLinks must be created specifying a permalink
    as its key.
    """
    blog_id = db.StringProperty()
    post_id = db.StringProperty()

def get_post_id(permalink):
    post_id = None
    pol = db.get(db.Key.from_path('PairOfLinks', permalink))
    if pol:
        post_id = pol.post_id
    return post_id

def bind_post_id_with_permalink(permalink, blog_id, post_id):
    pol = db.get(db.Key.from_path('PairOfLinks', permalink))
    if not pol:                 # new bind
        pol = PairOfLinks(key_name=permalink)
        pol.blog_id = blog_id
        pol.post_id = post_id
        pol.put()

get_post_id 関数は permalink にひもづけた Post ID を取り出すためのもの。記事のテキストを置換する際に用いることを想定している。permalink に対してひもづけられた Post ID がないときには None を返す。

bind_post_id_with_permalink 関数は permalink と Post ID のひもづけを保存するもの。「一覧」系の画面を作る際に用いることを想定している。指定された permalink に対して、すでにひもづけが存在した場合には何もしない。これは、permalink と Post ID の組み合わせが変わることはない、という前提に立つもの。Blogger の場合、記事をポストした後にこの 2 つを変更する手段がないから、この前提が成り立つ。

ひもづけを生成する部分のコードは以下のようになる(src/listview.py)。

        feed = util.get_posts(q)
        if feed:
            [...snip...]
            for entry in feed.entry:
                util.regist_pair_of_links(entry)

実際に、ひもづけ情報を保存する regist_pair_of_links 関数は util モジュールで定義している。上述の bind_post_id_with_permalink 関数を呼び出すだけのものだ。

def regist_pair_of_links(entry):
    model.bind_post_id_with_permalink(entry.get_html_link().href,
                                      entry.get_blog_id(),
                                      entry.get_post_id())

permalink へのリンクを置き換える

記事テキスト内のリンクの置換は以下の関数で行う(src/util.py)。

def replace_permalinks(original):
    pat = re.compile('<a[^>]*href=\"([^"]*)\"[^>]*>')
    new_text = original
    pos = 0
    while pos < len(new_text):
        m = pat.search(new_text, pos)
        if not m:
            break
        permalink = m.group(1)
        logging.info("Permlink: %s" % permalink)
        post_id = model.get_post_id(permalink)
        if post_id:
            postlink = "/post/?id=%s" % post_id
            new_text = new_text.replace(permalink, postlink)
            pos = m.end() - len(permalink) + len(postlink) + 1
        else:
            pos = m.end() + 1
    return new_text

正規表現を使い href 属性を持った a 要素を探し、href 属性の値を取り出す。その値を permalink としてひもづけられた Post ID が記録されていれば、それを Blogger Glass の「記事」画面へのリクエスト URL に置き換える。この処理はテキスト中から a 要素が見つからなくなるまで行う。

テキスト中を(正規表現で)探す際には、開始位置を pos で指定しているが、置換を実行した場合は次の開始位置がずれるため 185 行目で調整している。

これを呼び出す部分が src/postview.py 中にある。

    def fill_view_attrs(self, post_id):
        [...snip...]
        entry = client.get_one_post(settings.get('blog_id'), post_id)
        if entry:
            [...snip...]
            self.view.content = util.replace_permalinks(entry.content.text)

これで完成か?

#1 でも書いたように、データストアに保存したひもづけ情報を「いつ消すか」という問題が残っている。経過時間で切るなら保存した実体の中に記録した日時を保管しなければならない。加えて、「消す」作業を開始するトリガーをどうするかという問題もある。「設定」画面でユーザに明示的に消させるか。その場合、リンクの置き換えが機能するにはログインが前提となる。それでも構わないかな。

関連リンク

関連記事

2010-11-08

「Labels」ボタンの実装 (BloggerGlass)

「Labels」ボタンは、Blogger Glass の iPhone 専用画面のうち、記事表示画面の下部に付いているボタンだ。その名の通り、記事に付けられたラベルの一覧を表示させるためのボタンとして置いてある。これまでは、対応する機能を実装しておらず、ただの飾りでしかなかった。今回はそれを実装した。

ラベルの取得

ブログ記事のデータは GData Python ライブラリを使って読み込んでいる。ラベルのデータも同ライブラリが処理してくれている。

記事画面の場合

記事画面(を表示するリクエストハンドラ)で扱うデータは gdata.blogger.data.BlogPost だ。記事に付けたラベルは、このオブジェクトの category 属性として保持されている(atom.data.Category オブジェクトのリスト)。atom.data.Category オブジェクトの term 属性がラベルの文字列になる。

記事画面(のハンドラ)ではこれまでもラベルを上記の方法で取り出している。これまでは画面表示用に使うだけだったが、今回から「ラベル一覧画面」用に別途保存することになる。util.save_lables() については後述。

該当する部分のコードは以下のようになる。

(src/postview.py より)
        entry = client.get_one_post(settings.get('blog_id'), post_id)
        if entry:
            [...snip...]
            labels = []
            for label in entry.category:
                labels.append(label.term)
            labels.sort()
            self.view.labels = labels
            util.save_labels(labels, self.request, self.response)

ちなみに、Python のリストのソートメソッドは自分自身を返さないので注意が必要。たとえば上記のコードで、ソートした labels を代入するつもりで self.view.labels = labels.sort() とすると、None が代入されることになる。今回もふくめて何度も痛い目に会ってきたので、忘れないようにここに書き留めておく(それでもまた忘れるんだろうな)。

一覧画面の場合

一覧画面および検索結果画面(を表示するリクエストハンドラ)で扱うデータは gdata.blogger.BlogPostFeed になっている。このオブジェクトは entry 属性として記事データ(gdata.blogger.data.BlogPost)のリストを保持している。したがって、ここから個々の記事に付けられたラベルをすべて集めるには以下のようなコードが必要だ。

(src/util.py より)
def collect_labels(entries):
    """
    'entries' must be a list of gdata.blogger.BlogPost.
    """
    labels = []
    for entry in entries:
        for label in entry.category:
            if label.term not in labels:
                labels.append(label.term)
    labels.sort()
    return labels

これを呼び出すコードはこうなる。

(src/main.py より)
        q = gdata.blogger.client.Query(start_index=start_index,
                                       max_results=info.Pager.PAGESIZE)
        feed = util.get_posts(q)
        if feed:
            [...snip...]
            # collect labels for each post, then save them.
            labels = util.collect_labels(feed.entry)
            util.save_labels(labels, self.request, self.response)

ラベルの保存と読み込み

ラベルの保存には GAE が提供する memcache サービスを使う。データストアとは異なり揮発性のストレージだが、その分高速に動作するとのこと。前回の記事(「Back」ボタンの実装)にも書いたように、ラベル情報の保存先とその形態にはいくつか方法が考えられるが、とりあえずはセッション固有データとして memcache に保存する方式にしてみた。

ラベルの保存と読み取りは以下の 2 つの関数で行う。

(src/util.py より)
def save_labels(labels, request, response):
    sess = session.Session(request, response)
    if memcache.get('labels', namespace=sess.id) is None:
        memcache.add('labels', labels, namespace=sess.id)
    else:
        memcache.set('labels', labels, namespace=sess.id)

def load_labels(request, response):
    sess = session.Session(request, response)
    return memcache.get('labels', namespace=sess.id)

memcache へのデータの書き込みには、keynamespace という 2 つの情報を指定できる。ここでは key として 'labels' という文字列を、namespace としてセッション ID を指定している。セッション ID を使うことで、セッション固有の情報として保存と読み取りが可能になる。

「Labels」ボタン用のリクエスト

以下が今回追加したリクエスト形式になる。iPhone 用の「記事画面」のボタンだけでなく、「アプリメニュー」にも「Labels」項目を追加しておいた。当初は「記事画面」の時だけラベル情報を保存するつもりだったが、「一覧」と「検索結果」でも表示した分の記事に付いたものを集めて保存することにしてみた。これにより「一覧」系の画面から呼び出しても(それなりに)意味のある機能になったため、アプリメニューにも付け加えた次第。

すべての記事に付けられたすべてのラベルを集めて表示することも考えたが、そのためには全記事データの取得が必要でまとまった時間が必要になる。むしろ別機能として実現すべきだと考え、今回は表示した分の記事に付いたものだけを集める、という機能にした。

リクエスト形式 機能
/lables/ 直前の画面で表示された記事に付いていたラベルの一覧を表示する。

このリクエストを処理するハンドラを新たに追加してある。

その他の変更点

以下のコミットを参照。

関連リンク

関連記事

2010-11-07

「Back」ボタンの実装 - セッション管理 (Blogger Glass)

iPhone アプリらしい外観には「Back」ボタンが必要だ。Safari の「戻る」ボタンを使ったのではアプリらしくない。そして「Back」ボタンを実装するためには画面の履歴を記録しておくための仕組みが必要だ。

履歴を記録する仕組みは大きく 2 つの部分に分かれる。履歴そのものを(文字列のリストとして)記録しておくための部分と、そのリストをクライアント(ブラウザ)ごとに持つための部分だ。前者の実装は「リングバッファを作る」で説明した。また、後者を実現するためのセッション管理の肝となる部分も「Cookie の使い方」で説明ずみだ。

今回は、これまでに実装した「かけら」を Blogger Glass に組み込みセッション管理を実現するとともに、セッションデータとして履歴を保持させることで、「Back」ボタンを実現する。

セッション管理

session モジュールに定義した Session クラスは Cookie を用いたセッション管理を実現する。また、セッション固有のデータを data 属性として保持している。このセッション固有データは GAE のデータストアサービスで保存される。

セッションの作り方(ID の生成、ブラウザとの Cookie の授受)は「Cookie の使い方」に書いたコードをほぼそのまま流用している。

セッション管理のほとんどは Session オブジェクトの初期化時に完了している。この初期化時には、リクエストハンドラから RequestResponse のオブジェクトがわたってくることを想定しており、ブラウザとの Cookie の授受も、この 2 つのオブジェクトを通して行っている。

セッション管理の利用者(リクエストハンドラ)側では、Session オブジェクトを初期化し、セッション固有データとしての data 属性を読み書きするだけで良い。

セッション固有のデータ

現在のところ、セッション固有データとして保持するのはリクエスト履歴のみで、これは URL (文字列)のリストになるため、データストア用のモデルは以下のようになる。

(src/model.py より)
class SessionData(db.Model):
    history = db.StringListProperty()

セッション管理をリクエストハンドラに組み込む

Session クラスを使ってセッション管理(とセッション固有データの保存)には以下のようなコードを書く。この関数自体は util モジュールで定義している。

(src/util.py より)
def save_url(request, response):
    history = RequestHistory()
    sess = session.Session(request, response)
    history.import_history(sess.data.history)
    logging.info("Hisotry: %s" % history)
    history.push(request.uri)
    sess.data.history = history.export_history()
    sess.data.put()

リクエストハンドラの get メソッド等で以下のような関数を呼び出す。

back リクエストの追加

セッション固有データとして保存された履歴をたどって、前の画面に戻る動作を実現するのは back リクエストを受けるハンドラになる。

back リクエストの基本的な処理は、(セッション固有データから)リクエスト履歴を 2 つ読み取り、2 つ目のリクエスト URL にリダイレクトする、というものだ。履歴の 1 つ目は back リクエストを送ってきた画面(つまり「Back」ボタンが配置されている画面)であり、戻るのはそのさらに 1 つ前(履歴の 2 つ目)のリクエスト URL になる。ただし、画面によってはユーザ体験として戻る意味のない画面もあり(例: メニュー画面)、そこはスキップし、さらに前の画面に戻るようにしてある。また、戻るべき履歴が空の場合は、いつもトップ画面に戻る。

セッション固有データからの履歴の読み取りは util モジュールで定義した以下の関数で行う。

(src/util.py より)
def load_url(request, response):
    sess = session.Session(request, response)
    history = RequestHistory()
    history.import_history(sess.data.history)
    from_url = history.pop()
    back_url = history.pop()
    if not back_url:
        back_url = '/'
    sess.data.history = history.export_history()
    sess.data.put()
    return (from_url, back_url)

その他の変更点

以下のコミットを参照。

次の一手

「Back」ボタンが完成したので、「iPhone アプリらしく #2 - 実装 (Blogger Glass)」で(画面には配置しておきながら)未実装のままにしておいた部品のうち、残っているのは「Labels」となる。これは、「ポスト画面」で表示中の記事に付けられたラベル一覧を表示する画面に移るためのものだ。

これを実現するには、「Back」ボタンと同様にセッション固有のデータに保存しても良いし(保存先は memcache で十分か)、ブログ ID とポスト ID をキーとしてグローバルデータとして保存するのでも良い。とりあえず、セッション固有データとして保存する方式で作ってみようか。

関連リンク

関連記事

2010-11-02

一意性の確保 - ハッシュ関数

以下は実験中の Blogger Glass のコード、そのセッション管理を担当するモジュールの一部でセッション ID を生成す関数になっている。

(src/session.py より)
def generate_session_id(seed):
    m = hashlib.sha1()
    m.update(datetime.datetime.now().isoformat())
    m.update(str(seed))
    return m.hexdigest()

以前、ウェブアプリにおけるセッションを病院に治療に来る患者の関係にたとえた(→「Cookie の使い方 - GAE におけるセッションの保持」)。病気(やケガ)の治療では病院側が患者についてさまざまな情報を記録しておき(カルテ)、その記録と患者個人をひもづけるために番号を使うと書いた。この番号に相当するのがセッション ID なのだ、とも書いた。

カルテと患者のひもづけで大事なことはそれが一通りに定まることだ。これが混乱すると(あるカルテが別の患者のものだと扱われる等)大変なことになる。これを確実にするための 1 つの方法は、空白のカルテ用紙にあらかじめ重複しない通し番号を振っておくというものだ。番号の一意性 (uniqueness) が確保できていることが、患者という個人の識別に利用できる理由だ。

ウェブアプリにおけるセッション ID についても、同じことがそのままあてはまる。すなわち、ウェブアプリとクライアント(ブラウザ)のひもづけには、一意性の保証された何かを利用しなければならない。↑で示した関数は、この一意性の保証された何かを、ハッシュ関数(一方向ハッシュ関数とも言う)によって生成している(標準ライブラリの hashlib)。

ところで、このハッシュ関数が生成する「何か」は本当に一意 (unique) なのだろうか? あるいは完全に一意なものではないとしてどれぐらい一意なのだろう?

一方向ハッシュ関数

先のコードで使っている hashlib.sha1 という関数はハッシュ関数の中でも一方向ハッシュ関数(Wikipedia:ja では暗号学的ハッシュ関数)と呼ばれるものだ。

(「入門SSH」p.44 より)
一方向ハッシュ関数は、任意の長さの入力から固定長の出力を返す関数です。一方向ハッシュ関数の出力は一方向性(出力から入力を得るのが困難)と衝突困難性(同じ出力を得る 2 つの入力を得るのが困難、衝突耐性)を持ちます。この特徴から、ファイルなどが改ざんされていないことのチェック(もしファイルが改ざんされていれば、出力が異なるはず)や電子署名のために入力を短く固定長にするためなどに利用されます。[...snip...]

代表的な一方向ハッシュ関数には、MD5、SHA-1、SHA-256、SHA-512 があります。

もともと、一方向ハッシュ関数はデータの改ざんの有無を検査するような(セキュリティに関係する)目的に使われるが、上記の 2 つの特性のうち「衝突困難性」があるため「一意性の保証された何か」を生成するためにも利用できるわけだ。

さて、hashlib.sha1 で使われている SHA-1 という一方向ハッシュ関数では常に 160 ビットの値を生成する。言い換えると、生成できるのは高々 2^160 個の「何か(符号なしの整数値)」でしかない。もし、毎回異なる値を生成できるとしても、2^160 回以上 ID を生成させれば同じ ID が出てくることになる。そう、これで生成できる「何か」は完全に一意なものではないのだ。

ほどほどの一意性

確かに一方向ハッシュ関数では「完全な一意性」を得ることはできない。でも、良く考えてみれば 2^160 (2 の 160 乗) というのは、結構大きな数だ(→ 「2^64 (2 の 64 乗) って、どれぐらい?」)。うん、いや、結構っていうか、日常生活のスケールでは決して出会うことのないぐらいの大きさだ。ちょっと、Python で計算させてみた。

[imac] mnbi% python
Python 2.6.1 (r261:67515, Feb 11 2010, 00:51:29) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> n = 2 ** 160
>>> len(str(n))
49
>>> print n
1461501637330902918203684832716283019655932542976

10 進数で 49 ケタ。つまり、10^48 より大きく、10^49 より小さい。読み方は、10^48 が「極(ごく)」なので……

(2^160 の 10進数表現の読み方)
1    (極)
4615 (載)
0163 (正)
7330 (澗)
9029 (溝)
1820 (穣)
3684 (じょ)
8327 (垓)
1628 (京)
3019 (兆)
6559 (億)
3254 (万)
2976

……となる。つまり「一極四千六百十五載(飛んで)百六十三正七千三百三十澗九千(飛んで)二十九溝千八百二十穣三千六百八十四じょ八千三百二十七垓千六百二十八京三千(飛んで)十九兆六千五百五十九億三千二百五十四万二千九百七十六」だ。

意図的に(たいていは悪意を持って)同じハッシュ値を作ろうとしない限り、同じ値が生成されることはないと言って良い。「完全な」ではないにしろ、「ほどほどの一意性」(実際には「実用的な」と言うべきだろうな)は保証されているようだ。

ちなみに「新版暗号技術入門 秘密の国のアリス」によれば、SHA-1 の「強衝突耐性」は 2005 年に破られたとのこと(同 p.176)。ここで「強衝突耐性」とは

(「新版暗号技術入門 秘密の国のアリス」p.170 より)
強衝突耐性とは、ハッシュ値が一致するような、異なる 2 つのメッセージを見つけ出すことが非常に困難である、という性質です。

のことだ。

もっとも、ウェブアプリとクライアント(ブラウザ)のひもづけるセッション ID としてハッシュ値を使う場合、セッション ID を知られただけでアウトだけどね。病院のたとえで言えば、診察券に書かれた患者番号を誰かに知られてしまい、診察券を偽造されてしまうかもってところか。ま、病院と患者の場合は、そんなことをしても意味がないがね(診察券に病院で治療を受ける以外の使い道があれば別)。

ウェブアプリのセッションを管理する「何か」としてハッシュ値を使う場合、大事なことは異なるクライアントに同じ「何か」をわたさないようにすることだ。「何か」を(通信の)途中で盗まれないようにすることは別問題。

そもそも、Blogger Glass のような「公開情報を読み取り専用で扱う」アプリでセッションを盗まれたとしても大した問題にはならないよ。

参考文献

入門SSH (My UNIX series (04))
春山 征吾
アスキー ( 2004-11 )
ISBN: 9784756145536
おすすめ度:アマゾンおすすめ度
新版暗号技術入門 秘密の国のアリス
結城 浩
ソフトバンククリエイティブ ( 2008-11-22 )
ISBN: 9784797350999
おすすめ度:アマゾンおすすめ度

「6.4 ハッシュ法」の一部としてハッシュ関数を扱っている(p.487 〜 492)。ただし、扱っているのは一般的なハッシュ関数であり一方向ハッシュ関数ではない。

関連リンク

関連記事

2010-11-01

リングバッファを作る - リクエスト URL をためておく仕組み (BloggerGlass)

「Back」ボタンを実装するためには、戻るべき画面を記録しておかなければならない。現状の画面遷移だけなら 1 つ前の画面を覚えておけば十分なはずだが、将来のことも考えて、画面遷移の履歴を保持するための仕組みを作ってみることにした。

履歴保存用クラス

実体はリクエスト URL (文字列)を保存しておくためのリスト(Ruby で言うなら配列)に過ぎない。ただし、「前の画面に戻る」ための履歴だから、最後に登録した URL を最初に取り出すことになる。つまり、LIFO (Last In First Out)と呼ばれるデータ構造になる。別の呼び方にスタックというのもある。

スタックなら、操作のためのインタフェースは以下のようになる。

スタックの操作
名称 機能
is_empty 空かどうかを返す
push データを 1 つ追加する
pop 最後に追加したデータを取り出す(取り出しされたデータはスタックから削除する)
peek 最後に追加したデータを見る(取り出さない)

最低限必要なのは push と pop で、他はあると便利なものだ。

また、この履歴保存用クラスは、スタックであると同時にリングバッファでもある。これは平たく言えば、バッファが一杯になったとき、古いデータが新しいデータで上書きされていくデータ構造だ。

リングバッファにしたかったのには理由がある。直前の画面に戻るための履歴なのだから、バッファが一杯になったからと言って履歴が保存できなくなるのは困る。一方で、古い履歴よりも新しい履歴の方が重要だ。そういう意味で、新しいデータを常に保存することができる(その代わり古いデータは消えていく)リングバッファは最適だと言える。

export_historyimport_history は、履歴をセッションデータとしてデータストアに収めるときに使用する(ことになるはず)。

単体テスト

RequestHistory クラスは、以下のような単体テストを書きながら、そしてテストしながら実装した。50 行足らずの短いプログラムだと言うのに意外に難しかった。何度「これで良い(はず)!」と思ってからテストに失敗したことか。こういう一般的なプログラムを書くときは、本当にテストが役に立つ。

(tests/util_test.py より)
import unittest

import pathconf
# target module 
import util

class RequestHistoryTest(unittest.TestCase):
    def setUp(self):
        self.history = util.RequestHistory()

    def test_is_empty(self):
        """empty?(EMPTY) == True"""
        self.assert_(self.history.is_empty())

    def test_peek(self):
        """peek(push(EMPTY, A)) == A,
        then pop() also returns A
        """
        self.history.push('/1')
        self.assertEqual(self.history.peek(), '/1')
        self.assertEqual(self.history.pop(), '/1')

    def test_pop_against_empty(self):
        """pop(EMPTY) == None"""
        self.assertEqual(self.history.pop(), None)

    def test_push_1_pop_1(self):
        """pop(push(EMPTY, A)) == A"""
        self.history.push('/')
        self.assertEqual(self.history.pop(), '/')

    def test_push_2_pop_2(self):
        """pop(push(push(EMPTY, A), B)) == B
        pop(pop(push(push(EMPTY, A), B))) == A
        """
        self.history.push('/')
        self.history.push('/foo')
        self.assertEqual(self.history.pop(), '/foo')
        self.assertEqual(self.history.pop(), '/')

    def test_push_10_pop_10(self):
        """H = push(push(...(push(EMPTY, 0), 1), ...), 9)
        then, pop(pop(...(pop(H))...)) = 0
        """
        for i in range(10):
            self.history.push("/%d" % i)
        for i in reverse_range(10):
            self.assertEqual(self.history.pop(), ("/%d" % i))

    def test_push_11_pop_10(self):
        """H = push(push(...(push(EMPTY, 0), 1), ...), 10)
        then, pop(pop(...(pop(H))...)) = 1
        """
        for i in range(11):
            self.history.push("/%d" % i)
        r = reverse_range(11)
        r.pop()
        for i in r:
            self.assertEqual(self.history.pop(), ("/%d" % i))

    def test_export_history(self):
        for i in range(10):
            self.history.push("/%d" % i)
        h = self.history.export_history()
        for i in range(10):
            self.assertEqual(h[i], ("/%d" % i))

    def test_import_history(self):
        h = []
        for i in range(10):
            h.append("/%d" % i)
        self.history.import_history(h)
        r = range(10)

    def test_pickle(self):
        """Make sure that it can be pickled in and out."""
        for i in range(10):
            self.history.push("/%d" % i)
        pickled = pickle.dumps(self.history)
        history = pickle.loads(pickled)

        for i in reverse_range(10):
            self.assertEqual(history.pop(), ("/%d" % i))

def reverse_range(n):
    r = range(n)
    r.reverse()
    return r

def suite():
    return unittest.TestSuite((
            unittest.makeSuite(RequestHistoryTest, 'test'),
            ))

if __name__ == '__main__':
    unittest.TextTestRunner().run(suite())

関連リンク

関連記事

2010-10-31

Cookie の使い方 - GAE におけるセッションの保持

前回(「iPhone アプリらしく #2」)にも書いたように、Blogger Glass は「画面遷移だけの(とても古臭い)ウェブアプリ」だ。これを iPhone アプリらしく見せるためには、ぜひとも「Back」ボタンが必要だ。というのも、iPhone アプリでは、ある画面からボタンやら何やらを押すことで子画面を開き、そこで作業が完了すると親画面に戻る、という操作が良く実装されている。このユーザ体験を実現することは、iPhone アプリとしてごく標準的なことなのだ。

しかし、ウェブアプリでこれ(一つ前に開いていた画面に戻る)をやろうとすると、セッションの保持(と管理)という壁にぶつかる。ステートレスな HTTP 上に作られるウェブアプリの宿命だ。

Blogger Glass では、ここまでセッション管理にまつわることを避けてこれたが、それもそろそろ限界。このあたりで、ちゃんとセッション周りを実装することにしよう。

実現方法

ウェブアプリにおけるセッションの概念とは、たとえるなら病院(医者)とそこに治療に来る患者の間にあるものだ。初診時に治療セッションが始まり、完治によって終了する。

患者は病気なりケガなりの治療で数回、病院を訪れることになる(セッションの継続)。その度に、病院(医者)の側が患者のことをすっかり忘れてしまっては治療は成立しない。病院(医者)は個々の患者について、病気(やケガ)の状態と治療の経過を記録しておかなければならない。それが患者ごとに用意されるカルテと呼ばれる記録だ。

一方、カルテに書かれた記録を有効に利用するためには、患者一人一人を識別できるようにする必要がある。大抵は(カルテに書かれた)患者の名前で間に合うが、中には同姓同名の患者もいるから常に確実な方法とは言えない。そんなわけで患者に一意の番号を割り当て、それでカルテと患者をひもづける。でも、患者にとったら何桁にもなる番号を覚えるのは大変なので、病院はこの番号を記録したカード、すなわち診察券を用意して、初診時に患者にわたす。

病院がウェブアプリで患者がブラウザ、カルテはウェブアプリが記録するセッション情報で、カルテと患者をひもづける番号がセッション ID という対応関係になる。

残るは診察券に対応するモノ(ブラウザ側にセッション ID をわたすための仕組み)だが、これには 3 つの方法がある。すなわち、(1) URL に埋め込む方式、(2) HTML のフォームに隠し要素として埋め込む方式、(3) cookie を使ってわたす方式、の 3 つだ。

練習も兼ねて、今回は (3) の方式でセッションを実現してみる。

サンプルプログラム

以下のサンプルプログラムでは、セッション情報(整数値 1 つ)は GAE の提供するストレージサービスの 1 つである memcache を利用している。このサービスはもう 1 つのデータストアとは違い、「揮発性」のストレージだ。容量も(データストアに比べればかなり)小さい。その代わり、ずっと高速に動作するらしい。頻繁にアクセスする少量のデータは、memcache に置く方が良い。

このプログラムを GAE アプリのリクエストハンドラにしてアクセスすると、「Back」と「Forward」という 2 つのリンクを持つページが開く。Forward をたどると memcache 中のデータ(カウンタ)がインクリメントされ、Back をたどればデクリメントされる。ただ、それだけのプログラムだが、内部的にはセッション管理がなされており、memcache に保持される値はクライアントごとに用意される。実際に複数のブラウザで開けばそのことがわかる。

memcache に保存するカウンタとは別に、データストアにも 1 つ値を保存している。これはセッション管理そのものとは関係ない。この値は、セッションを作るために必須の ID (先の病院と患者のたとえで言うなら、カルテと患者をひもづける番号だ) を作るための「種」になっている。

セッション管理の仕組み自体は単純で、リクエストを受けたら(ハンドラの get メソッドが呼ばれたら)、ブラウザが送ってきた cookie からセッション ID を取り出す。次にセッション ID をキーとして memcache からカウンタの値を取り出す。

ブラウザが cookie を送ってこなかった、あるいは(このプログラム用の)セッション ID がふくまれていないときは、新しくセッション ID を生成しレスポンスヘッダに入れる。

「Back」ボタンを実装するには

Blogger Glass (の iPhone 用画面)に「Back」ボタンを実装するには、画面を開くときにリクエスト URL をセッション情報として保存すれば良い。「Back」ボタンには専用のリクエスト URL を用意しておく(たとえば /back)。そのリクエストハンドラでは、セッション情報から保存されたリクエスト URL を取り出し、そこにリダイレクトする。

まだ、コードを書いていないから確信はないけれど、だいたいこんな感じで動きそうだ。

関連リンク

関連記事

2010-10-23

Blogger Glass、ラベルで検索したい

一覧から記事を表示できるようにはなった。しかし、一覧だけから特定の記事を探すのはシンドい。Google の存在に慣れた身にとって「探す == 検索」であって欲しい。Blogger Glass にも「検索」機能が必要だ。

Blogger Data API では、データのいわゆる全文検索には対応していない(→ Blogger API Reference Guide)。可能なのはカテゴリを指定してフィードの内容を絞り込むことだけ。ここでいうカテゴリとは Blogger (の投稿画面など)ではラベルと呼ばれているもの。

たとえラベルによる検索だけだったとしても、何もないよりはマシだ。適切にラベル付けがされていれば効果的な絞り込みができるし、適切でない場合も多少の助けにはなる。

そんなわけで、今回はラベル検索を実装してみる。

調査

カテゴリによる検索

Google Data API (プロトコル) では、フィードを要求する際のパラメータとしてカテゴリを指定できる。これまでにも実装の中で使ってきた max-resultsstart-index などと同様に URL に埋め込む形で利用する。

これはそのまま GData ライブラリでも利用できる。具体的にはこうなる。

(カテゴリによる絞り込み)
        client = gdata.blogger.client.BloggerClient()
        q = gdata.blogger.client.Query(categories=[label.encode('utf-8')])
        feed = client.get_posts(blog_id, query=q)

この例では label にラベル文字列が入っている。複数のラベルで絞り込む場合(AND 結合)は gdata.blogger.client.Query 生成の際に categories に複数のラベルを詰めれば良い。OR 結合で指定する場合はラベルを "|" でつないた文字列を詰める。たとえば、'imac' と 'macbook' のどちらか一方をラベルに持つ記事を探すのであれば categories=['imac|macbook'] をわたす。

デザイン(意匠と設計)

検索結果画面

一般に検索結果は複数の記事になるから、その表示も一覧表示が適している。欲を言えば Google の検索結果のように記事の抜粋も付けたいところだが、現段階ではそこまでは望まない。よって、画面そのものは「一覧画面」と同様になる。また、結果が多数の場合は複数のページに分かれることになるから、これも「一覧画面」と同様にページ切り替えの仕組みも必要だ。唯一の違いは、「検索結果画面」にはどんな検索語(ラベル)で検索したかを表示すべきだという点だ。これは「view-header」の部分に表示する。

リクエスト形式

リクエスト形式として /search/ を追加する。以下に、これをふくめた定義ずみ全リクエスト形式を示す。

Blogger Glass 全リクエスト形式
リクエスト 機能
/ page=0 を指定した場合と同様。
/?page=<number> number で指定したページの一覧を表示する。
/post/ 最新の記事の内容を表示する。
/post/?id=<id> id で指定した記事の内容を表示する。
/search/?label=<string> ラベル string で記事を絞り込んむ。結果が複数ページにわたる場合は、最初のページ(ページ番号 0)を開く。
/search/?label=<string>&page=<number> ラベル string で記事を絞り込んだ結果のうち、number で指定したページを開く。
/settings/ 設定変更のための画面を開く。

実装

新規に追加したのは、SearchViewHandler (後述)を定義する src/searchview.py と「検索結果画面」の src/searchview.html だ。

src/app.yaml には新しいリクエスト形式 (/search/) を追加。src/info.py には「検索結果画面」に表示する情報を詰めるためのクラス(SearchViewInfo)を追加。また、「一覧画面」と「検索結果画面」で共通になるページ切り替えの仕組みは src/util.py に移動させた。src/postview.html への変更はラベルに検索リクエストへのリンクを張るためのものだ。

SearchViewHandler

結果として、ほぼ「一覧画面」の MainHandler と同じコードになった。違いはクエリ(gdata.blogger.client.Query)生成時にラベルを指定している程度だ。fill_viewinfo() をリファクタリングしてくくり出すことを考えても良いかも。

この実装における注意点は、1 つは GData Python ライブラリのバグ(後述)で、もう 1 つは、クエリにラベル文字列を追加する際にエンコードを指定する、という点だ。つまり、label がラベル文字列だったとして、このままクエリにわたすのではなく、label.encode('utf-8') でわたさなければならない、ということ。さもないと、GData ライブラリの奥で以下のようなエラーが出る。

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

そう言えば、以前同じような現象に遭遇したことがあった(→ Ruby で書いたフィルタを Python で書き直す #2)。文字列のエンコードの問題には、こっちが忘れた頃に出くわすなあ。

GData Python ライブラリのバグ?

実は、今回の実装の過程で、GData Python ライブラリのバグだと思われるものにぶつかった。それは上述のようにクエリにカテゴリを指定しても絞り込みが行われないというものだ。原因は gdata/client.py 中にあった。

(gdata/client.py のオリジナル; 827 行付近)
 759: class Query(object):
[...]   [...snip...]
 824:   def modify_request(self, http_request):
 825:     _add_query_param('q', self.text_query, http_request)
 826:     if self.categories:
 827:       http_request.uri.query['categories'] = ','.join(self.categories)

この 827 行目は正しくはこうなるようだ。

(gdata/client.py 修正版; 827 行目)
 827:       http_request.uri.query['category'] = ','.join(self.categories)

要は http_request.uri.query に詰められたキーと値が最終的に Google Data API にわたるが、このキー名が間違っているのだ。複数形の "categories" ではなく単数形の "category" がプロトコルとして正しいクエリ文字列だ。

ま、次のバージョンでは直ってくるだろう。

追記 (@2010-10-24)

このバグはすでに問題として報告されていて、かつすでに修正されているようだ。といっても修正されたのは最近(この 10 月に入ってから)のようだけど。

  • Issue 315 (gdata-python-client; Project Hosting on Google Code)

今後の展望

Blogger Glass はブログを「見る」ことに特化したアプリだ。その意味では「検索」機能は重要な機能だと言える。今回実装した「検索」機能は、とくにそのユーザ体験において必要最低限のものにも到達していない。なにしろ、個々の記事を開いたときにしか検索機能を使うことができないのだ、ユーザにとって使いやすいものではない。せめて、ラベルの一覧表示は欲しい。できれば一覧の他にタグクラウドのような表示も欲しい。また、ラベルを複数指定できればさらに実用性が高まるだろう。GData ライブラリ(と API)ではカテゴリを複数指定できる。これに関して足りないのは単純にアプリ側の作り込みなのだ。

一歩前進すれば、より遠くが見えるようになる。ゴールのように見えていた場所は、単なる中継地点でしかなかったことがわかる。

とはいえ、記事の一覧と個々の記事の内容が独自のスタイルで表示できるようになったことで、Blogger Glass は実用性のあるアプリになった。今回、指定できるラベルが 1 つに限定されているとは言え、ラベルによる絞り込み(検索)ができるようになり、さらに実用性は高まったと言える。もう記事の投稿の時以外にオリジナルの Blogger の方を開く必要はないかも。

さっき数えたら、Blogger Glass に関係する記事が 28 本になっていた。そろそろ全体の「まとめ記事」を書いて整理した方が良いな。前ばっかり見ていると、いつまでたっても近付かないゴールに(なにしろゴールが動くからな)やる気を削がれかねない。来し方を振り返り積み重ねたものを実感することで、また一歩を踏み出すモチベーションになる。

関連リンク

関連記事

2010-10-16

クラス変数とインスタンス変数 (Python編)

恥ずかしい告白をしなければならない。今日、「初めてのPython」の 「23 章 クラスのコーディング (基礎)」を読むまで、Python におけるクラス変数とインスタンス変数の記述の仕方を混同していた。というより、class ステートメントの内側に記述した(代入した)変数は、すべてインスタンス変数になると勘違いしていたのだ。

ここで、「クラス変数」とは、クラスのインスタンスにではなく、クラス自体が(つまりクラスオブジェクトが)持つ変数のことで、そのクラスのすべてのインスタンスオブジェクトで共有されるものを指す。一方で「インスタンス変数」は、それぞれのインスタンスオブジェクトに固有の変数のことだ。「初めてのPython」では、変数ではなく属性という言葉で表現しているが、この記事内では(わたし自身に馴染のある)変数という言葉で呼ぶことにする。

混同の原因は表記によるもの?

Python ではクラス変数は、

(クラス変数)
class Foo:
    value = 100

のように定義する。一方で、インスタンス変数は、以下のようにメソッド中で self に関連付けて定義する。

(インスタンス変数)
class Bar:
    def __init__(self, val):
        self.value = val

ややこしいのは、どちらも <インスタンスオブジェクト>.<変数名> としてアクセスできることが。つまり、以下のようなコードが書ける(FooBar は上述の定義のもの)。

(クラス変数とインスタンス変数へのアクセス)
foo = Foo()
bar = Bar(300)
print foo.value, bar.value

これを実行すると、

100 300

と表示される。本来、Foo クラスのクラス変数 value へのアクセスは Foo.value と表記すべきだが、Foo クラスのインスタンスを通して foo.value でも参照できるのだ。さらにややこしいことに、foo.value に対して代入すると、今度はクラス変数の value への代入ではなく、新しく value という名のインスタンス変数が定義される。つまり、その時点で foo オブジェクトはクラス変数としての value とインスタンス変数としての value の両方を持つオブジェクトに変わる。ただ、代入でインスタンス変数が定義されるのは foo オブジェクトに対してだけなので、新しく Foo クラスのインスタンスを作ったなら、そのオブジェクトはクラス変数の value しか持っていない。

ああ、ややこしい。

サンプルプログラム

このことを確かめるためのサンプルプログラムを書いてみた。Foo クラスは上述のものと同じ。一方、Bar は同名のクラス変数とインスタンス変数を持ったものにしてある。

(variables.py)
 1: #!/usr/bin/python2.5
 2: 
 3: class Foo:
 4:     value = 100
 5: 
 6: class Bar:
 7:     value = 200
 8:     def __init__(self, val):
 9:         self.value = val
10: 
11: print "Foo:"
12: foo1 = Foo()
13: foo2 = Foo()
14: print "foo1.value = %d, Foo.value = %d" % (foo1.value, Foo.value)
15: print "foo2.value = %d, Foo.value = %d" % (foo2.value, Foo.value)
16: 
17: print "---- set 200 to foo1.value"
18: foo1.value = 200
19: print "foo1.value = %d, Foo.value = %d" % (foo1.value, Foo.value)
20: print "foo2.value = %d, Foo.value = %d" % (foo2.value, Foo.value)
21: 
22: print "---- set 50 to Foo.value"
23: Foo.value = 50
24: print "foo1.value = %d, Foo.value = %d" % (foo1.value, Foo.value)
25: print "foo2.value = %d, Foo.value = %d" % (foo2.value, Foo.value)
26: 
27: print "Bar:"
28: bar1 = Bar(300)
29: bar2 = Bar(400)
30: print "bar1.value = %d, Bar.value = %d" % (bar1.value, Bar.value)
31: print "bar2.value = %d, Bar.value = %d" % (bar2.value, Bar.value)
32: 
33: print "---- set 500 to bar1.value"
34: bar1.value = 500
35: print "bar1.value = %d, Bar.value = %d" % (bar1.value, Bar.value)
36: print "bar2.value = %d, Bar.value = %d" % (bar2.value, Bar.value)
37: 
38: print "---- set 600 to Bar.value"
39: Bar.value = 600
40: print "bar1.value = %d, Bar.value = %d" % (bar1.value, Bar.value)
41: print "bar2.value = %d, Bar.value = %d" % (bar2.value, Bar.value)

これの実行結果は以下のようになる

[imac] mnbi% python2.5 variables.py
Foo:
foo1.value = 100, Foo.value = 100
foo2.value = 100, Foo.value = 100
---- set 200 to foo1.value
foo1.value = 200, Foo.value = 100
foo2.value = 100, Foo.value = 100
---- set 50 to Foo.value
foo1.value = 200, Foo.value = 50
foo2.value = 50, Foo.value = 50
Bar:
bar1.value = 300, Bar.value = 200
bar2.value = 400, Bar.value = 200
---- set 500 to bar1.value
bar1.value = 500, Bar.value = 200
bar2.value = 400, Bar.value = 200
---- set 600 to Bar.value
bar1.value = 500, Bar.value = 600
bar2.value = 400, Bar.value = 600

14 〜 15 行目の出力で、Foo のクラス変数をインスタンスオブジェクト経由で参照できることがわかる。さらに 18 〜 20 行目では、<インスタンスオブジェクト>.<変数名> という表記に対する代入がインスタンス変数を(そのオブジェクトに対してのみ)定義することがわかる。また、23 〜 25 行目では、明示的にクラス変数を指定した代入が可能なこともわかる。

Bar を使った例では、クラス変数とインスタンス変数が同名の場合、<インスタンスオブジェクト>.<変数名> という表記ではインスタンス変数の方が優先されて参照されることがわかる。

これまでに書いたコードへの影響

この(恥ずかしい)混同のため、Blogger Glass のコードではクラス変数とインスタンス変数の区別が付いていない。よくもまあ動いているものだ。

実際のところは、参照だけなら(初期化後に一切代入していないなら)クラス変数であれ、インスタンス変数であれ、複数のインスタンスオブジェクトから参照されたとしても問題にならない。また、メソッドの中で(self 経由で)代入した途端、インスタンス変数が作られるのだから、他のインスタンスに影響を及ぼすこともない。ただ、意図しないところでインスタンス変数が作られたり、そのおかげでクラス変数が無駄になっていたりするだけだ。

たとえ影響がないとしても直さないと……。みっともないからな。

参考文献

初めてのPython 第3版
Mark Lutz
オライリージャパン ( 2009-02-26 )
ISBN: 9784873113937
おすすめ度:アマゾンおすすめ度

2010-10-01

Google App Engine SDK を動かす Python のバージョン

Python には 2010-10-01 時点で、バージョンの違いによる 2 つの系統が存在する。3 系と 2 系だ。GAE の本番環境では 2 系の 2.5 が使われている。開発環境と本番環境はできる限り同じ環境であることが望ましい。Mac で GAE SDK を使ってウェブアプリを開発するときにも、2.5 を使う方が良い。

実は数日前まで、GAE SDK (Mac 版の実体は「GoogleAppEngineLauncher.app」)には、Python の実行環境も本番と同一バージョンがふくまれていると思っていた。だから、Mac にインストールされているものが何であれ関係ないのだ、と。ところが、GAE Launcher の「Preferences」には Python のパスを指定するフィールドがある。これはつまり、SDK には Python の実行環境はふくまれておらず、Mac にインストールされているものを使う、という意味ではないだろうか?

で、確かめてみることにした。

Snow Leopard で利用可能な Python のバージョン

まず、Mac OS X Snow Leopard で利用可能な Python のバージョンについて整理しておこう。MacPorts を使えば最新版の 3 系もインストールできるが、ここでは標準でインストールされている Python だけを対象とする。確認は Snow Leopard のバージョン 10.6.4 で行った。ただし、そこに Xcode を中心とした開発ツール(実際には iOS SDK)一式がインストールされた状態のものだ。開発ツールが Python を上書きするかどうかはわからない。手元には素のままの Snow Leopard がないため確認できない。

Snow Leopard には Python の実行環境が 2 種類インストールされている。それも、2 系のものが 2 つだ。それぞれのバージョンを確認してみる。

/usr/bin/python

パス名を見ればわかるように、これが Snow Leopard 標準の Python だ。「標準」というのは、MacPorts 等を使っていない状態で、シェルのプロンプトから python として実行したときに起動されるもの、という意味。

[imac] mnbi% which python
/usr/bin/python
[imac] mnbi% python --version
Python 2.6.1
[imac] mnbi% file /usr/bin/python
/usr/bin/python: Mach-O universal binary with 3 architectures
/usr/bin/python (for architecture x86_64): Mach-O 64-bit executable x86_64
/usr/bin/python (for architecture i386): Mach-O executable i386
/usr/bin/python (for architecture ppc7400): Mach-O executable ppc
[imac] mnbi% python
Python 2.6.1 (r261:67515, Feb 11 2010, 00:51:29) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.version
'2.6.1 (r261:67515, Feb 11 2010, 00:51:29) \n[GCC 4.2.1 (Apple Inc. build 5646)]'
>>> ^D

バージョンは 2.6.1 で、64 ビット版のバイナリもあることがわかる。PowerPC 用のバイナリがあるのが不思議だ。インタプリタを起動して、sys.version を表示させると、より詳細なバージョン情報を知ることができる。

/usr/bin/python2.5

パス名が変えられていることから、こちらは互換性のために残されているのだろうと推測できる。

[imac] mnbi% which python2.5
/usr/bin/python2.5
[imac] mnbi% python2.5 --version
Python 2.5.4
[imac] mnbi% file /usr/bin/python2.5
/usr/bin/python2.5: Mach-O universal binary with 2 architectures
/usr/bin/python2.5 (for architecture i386): Mach-O executable i386
/usr/bin/python2.5 (for architecture ppc7400): Mach-O executable ppc
[imac] mnbi% python2.5
Python 2.5.4 (r254:67916, Feb 11 2010, 00:50:55) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.version
'2.5.4 (r254:67916, Feb 11 2010, 00:50:55) \n[GCC 4.2.1 (Apple Inc. build 5646)]'
>>> ^D

バージョンは 2.5.4 で、64 ビット版のバイナリはない。

GAE SDK を動かす Python はどれ?

GAE SDK (GoogleAppeEngineLauncher.app) をパッケージとして開いて、内容を確認してみると、やはり Python 本体は見当たらない。実際に、SDK 上でコードを動かして確認してみよう。GAE Launcher から「File」>「New Application... 」で新規アプリを作り、main.pyMainHandler を以下のように書き換える。"Hello, world!" の代わりに sys.version を表示させるわけだ。

import sys

class MainHandler(webapp.RequestHandler):
    def get(self):
        self.response.out.write(sys.version)

デフォルトの状態で実行させると、Safari に以下の文字列が表示される。

2.6.1 (r261:67515, Feb 11 2010, 00:51:29) [GCC 4.2.1 (Apple Inc. build 5646)]

↑で 標準の Python インタプリタに表示させたものと同一だ。

GAE Launcher の「Preferences」で Python 2.5 を指定してみる。

Google App Engine Launcher の Preferences

設定した後、バージョン表示用のアプリを立ち上げ直し、Safari でリロードすると以下の文字列が表示される。

2.5.4 (r254:67916, Feb 11 2010, 00:50:55) [GCC 4.2.1 (Apple Inc. build 5646)]

結論

  • GAE SDK (GoogleAppEngineLauncher.app) には Python の実行環境はふくまれていない。
  • このため Mac OS X にインストールされた Python 実行環境で SDK は動いている。
  • GAE の本番環境と合わせたいなら Launcher の「Preferences」で Python へのパスを指定する。

標準の Snow Leopard なら /usr/bin/python が使われるはず。MacPorts 等で他のバージョンの Python をインストールしてあり、かつ ~/.MacOSX/environment.plist でログイン環境(の PATH) をいじってあれば、他のバージョンが使われることもある。

2.5 と 2.6 (あるは 2 系最新の 2.7) にどんな差異があるのかは詳しく調べていないのでよくわからない。だから、2.6 を使ったとして GAE ウェブアプリの開発上で不都合があるかどうかもわからない。けれど、GAE の本番環境が 2.5 だと言うのだから、開発環境も 2.5 にしておく方が無難だ。そのためには、Launcher の「Preferences」で /usr/bin/python2.5 を指定する、と。

ブログの記事一覧を表示する」アプリでは、(実行環境のバージョンを気にしていなかったので) Snow Leopard 標準の(つまり、2.6)で動作確認していた。それでも何の不都合もなかったけどね。

関連リンク

関連記事

2010-09-29

Python で書いたフィルタを GAE に載せる #3

まず最初に、今回の記事で書こうと思っていることを並べてみる。

  1. ウェブアプリのテンプレートについて考えたこと
  2. 全体をフィルタ(っぽい構造)にこだわらずに書き直したこと
  3. Blogger Glass を github にプロジェクトとして登録したこと

そんなわけで、今回が「Python で書いたフィルタを GAE に載せる」シリーズの完結編。といっても、できあがったモノはようやくプロトタイプのレベルなんだけどね。

ウェブアプリのテンプレート

前回、GAE のテンプレートの仕組み (Django) では複雑なことはできないと書いた。後になって改めて考えてみたところ、これは見当違いな思い込みだと気付いた。テンプレート(というか HTML)も「何か」を記述するための言語であることは間違いないし、見方によってはプログラミング言語だと言えなくもない。しかし、その記述の仕方は手続きを積み重ねて行く Ruby や Python とは別物になる。

Python でプログラミングするなら「あれして、それして、その後こうする」と考えるし、そのようにコードに書く。言わば動的(ダイナミック)な思考方式だ。思考のかけらが時系列に並ぶようなイメージ。一方、HTML でページを作るときは「あれは○○、それは△△、そしてこれは□□」というように考えて、要素を配置する。こちらは静的(スタティック)な思考と言える。言葉を変えれば、HTML でページを作るときに「h1 の内容を、40 pt で出力してから、その後 h2 の内容を 24pt で右揃えで出力する。それが完了したら h3 の内容を 20 pt で左揃えで出力する」などとは考えない、ということだ。

テンプレートは、(汎用プログラミング言語で)プログラムを書くように作るものではない。テンプレートにループや条件分岐があるのは、そこに配置するオブジェクトを出力するための補助として、だ。

また、前回の「テンプレートとコードが密に絡んでしまっている」という評価は、リクエストハンドラからテンプレートを呼び出すと考えてしまったことでできた悪いデザイン(設計)に対するものだ。これは、本来、リクエストハンドラが「表示用データをくるんだオブジェクト」をテンプレートにわたす、と考えるべきだった。これなら、リクエストハンドラとテンプレートは(データオブジェクトを間に挟んで)分離していると言える。

言い換えると、リクエストを処理して結果のデータを用意するコードと(バックエンド層)、ユーザの目に見えるテンプレートから生成される HTML (プレゼンテーション層)を直接つなげるから(あるいは、つながっていると考えるから)ダメ(悪いデザイン)になる。ここは、両層は独立したものだと考えて、間をつなぐ「何か」をデザイン(設計)すべきなのだ。一般的には、この「何か」をプロトコルと呼ぶ。

多層構造にして、層間のプロトコルを定義する、と書くと大袈裟だけど、要はコード(バックエンド層)を考えるときにはテンプレートの構造ではなくて、処理結果の情報が何でそれをどうまとめるかを考える。次にテンプレート(プレゼンテーション層)を考えるときには、バックエンドで何が起こっているかは気にせず、下層から上がってくるデータ(をくるんだオブジェクト)を表示することに集中する。

こう考えてくると、フィルタ構造の中で HTML のリストを生成していたのはウェブアプリには適していないとわかる。フィードのタイトルとリンクをどう表示するかは、バックエンド層で決めることではないからだ。それはテンプレート(プレゼンテーション層)で扱う問題だ。順序つきリストにするかもしれないし、順序なしのリストにするかもしれない。あるいは、テーブルで表現するかもしれないし、div で囲むだけかもしれない。バックエンドはただ表示すべきデータを集めて上層に送るだけで良い。

フィルタ構造が足枷になっている

プログラムを、とくに入力を処理して出力に変えるようなモノを、いくつかの単純なフィルタの組み合わせとして実現することは、コーディング/テスト/デバッグのいずれの作業も簡単にしてくれる。しかし、ここで肝心なことはフィルタ(つまり標準入出力で互いにつながるというアーキテクチャ)そのものではなく、複雑な処理を独立した小さな処理に分割できること、そして分割した処理ごとに動作確認ができることにある。

ウェブアプリでは、(バックエンドの)処理を分割して、個別に動作確認するような手法は取りにくい、というか取れない。というのも出力をそのまま入力につなげることができないからだ。途中結果をテキストとして出力してブラウザの画面で確認することはできても、それを次の処理の入力にはできない。

つまり、フィルタとして実現したプログラムをウェブアプリに変える際に、元の構造にこだわることは足枷になる。すっぱりとフィルタのことは忘れてしまう方が良い。構造は忘れて、処理の中身だけを移すようにすれば良い。例の「ブログの記事一覧を作るプログラム」なら、フィードの取得や、そこから正規表現で記事のタイトルと URL を抽出する部分はそのまま使える。

今回の書き直しでは、Blogger のフィード自体をデータオブジェクトとして捉え直した。AtomFeed というクラスがそれだ。このクラスのインスタンスに対して、get メソッドをページ番号を引数として呼び出せば、Blogger からフィードを取得し、それを Atom としてパースし、内部リストに Entry クラスのオブジェクトとして蓄える。Entry クラスはフィードにふくまれる entry 要素(のデータ)を保持するためのもので、今のプロトタイプ版ではタイトルと URL だけを収めている。そして、この Entry クラスのオブジェクトこそ、バックエンド層(リクエストハンドラ)とプレゼンテーション層(テンプレート)をつなぐプロトコルになっている。つまり、リクエストハンドラは AtomFeed をインスタンス化し、リクエストされたページを指定して get を呼び出せば、後は Entry オブジェクトのリスト(AtomFeed オブジェクトの entries プロパティになっている)をテンプレートにわたせば良いことになる。(→ bloggerglass/src/feed/core.py)

テンプレートの方では、リクエストハンドラから渡されてきた Entry オブジェクトのリストから単純なループを使ってオブジェクトを取り出し、さらにその title プロパティと url プロパティの値から、順序つきリストを生成している。(→ bloggerglass/src/page.html)

ついに公開……そして今後の展望

GAE への配備はまだだが、先に github にプロジェクトとして登録した(関連リンク参照)。これが、初めての github プロジェクトなので、公開にあたっては多少の右往左往があったりもしたが、それは別途書く予定だ。

最初にも書いたが、今の段階ではまだアプリのプロトタイプにすぎない。「アプリのかけら」が少し成長したと言った程度だ。元々の目的である「Blogger で作ったブログを Blogger とは独立した表示システムで見る」を実現するには、まだ作業が必要だ。

今後は、まず iPhone/iPad での表示に最適化する。これは主に HTML (テンプレート)と CSS をいじる作業になる。その後は、個別の記事を Blogger Glass 内で表示する機能を追加する。記事のデータそのものはフィードとして取得しているのだから、後はそれを流し込むテンプレートを用意すれば良い。ただ、記事内に張ったブログ内の他記事へのリンクをどうするかが課題だ。これは Blogger Glass 内の記事ページへのリンクに付け替える等の処理が必要になる。ここまでできたら、Blogger Glass としては ver. 1.0.0 としても良いかと考えている。

関連リンク

関連記事

2010-09-28

Python で書いたフィルタを GAE に載せる #2

前回で、おおよそのデザイン(意匠と設計)ができた。今回は、コードを書き、GAE の SDK 上で動かしてみることが目標だ。

アプリ設定

初期状態と違うのは、スタイルシート用のディレクトリを静的コンテンツとして追加している点だ。

(app.yaml より)
 1: application: bloggerglass
 2: version: 1
 3: runtime: python
 4: api_version: 1
 5: 
 6: handlers:
 7: - url: /stylesheets
 8:   static_dir: stylesheets
 9: 
10: - url: .*
11:   script: main.py

リクエストハンドラ

PageFeedExtractTitles それに MakeList が前回までのフィルタを書き直したクラスだ。そのフィルタを適用している部分が 5 〜 8 行目になる。ついでに、全記事数もフィルタの 1 つ(フィードを取得するフィルタ)から取り出している。

(main.py より)
 1: class MainHandler(webapp.RequestHandler):
 2:     def retrieve(self, page):
 3:         pf = PageFeed(page)
 4:         filters = [pf, ExtractTitles(), MakeList()]
 5:         pipe = []
 6:         for f in filters:
 7:             f.input(pipe)
 8:             pipe = f.output()
 9:         self.total_posts = pf.total_posts()
10:         return pipe
11: 

以下が、リクエストハンドラの本体だ。フィードの取得から HTML のリストとしての整形までは上記の retrieve で完了する。ここでは HTML ページを作るための、その他の情報をまとめて、テンプレートにわたしている。

(main.py より)
12:     def get(self):
13:         stylesheet = "/stylesheets/default.css"
14:         total_pages = 10
15:         request_page = int(self.request.get("page", default_value="0"))
16: 
17:         lines = self.retrieve(request_page)
18:         total_pages = self.total_posts // PAGESIZE + 1
19:         pages = []
20:         for p in range(total_pages):
21:             page = Page()
22:             page.number = p
23:             if p == request_page:
24:                 page.current = True
25:             pages.append(page)
26: 
27:         template_values = {
28:             'lang': "ja",
29:             'title': "Blogger Glass [LOG+REPO]",
30:             'stylesheet': stylesheet,
31:             'total_pages': total_pages,
32:             'current_page': request_page,
33:             'lines': lines,
34:             'pages': pages,
35:             'home_url': "http://bloggerglass.appspot.com/",
36:             }
37: 
38:         path = os.path.join(os.path.dirname(__file__), 'page.html')
39:         self.response.out.write(template.render(path, template_values))

テンプレート

リクエストハンドラが使用するテンプレートを以下に示す。

特徴は、(a) 記事一覧を作るループ(17 〜 19 行目)、(b) ページ切り替えのためのリンクを作るループ(26 〜 32 行目) の 2 箇所だ。後者では、ループの中で条件判断を行い生成する内容を切り替えている。

(page.html)
 1: <!DOCTYPE HTML>
 2: <html lang='{{ lang }}'>
 3: <head>
 4: <meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/>
 5: <title>{{ title }}</title>
 6: <link type='text/css' rel='stylesheet' href="{{ stylesheet }}" />
 7: </head>
 8: <body>
 9: <header>
10: <h1>{{ title }}</h1>
11: <h2>Page #{{ current_page }}</h2>
12: <p>in {{ total_pages }} pages</p>
13: </header>
14: <section>
15: 
16: <div id="content">
17: {% for line in lines %}
18:   {{ line }}
19: {% endfor %}
20: </div>
21: </section>
22: 
23: <nav>
24: <div class="pager">
25:   <span>Gr</span>
26:   {% for page in pages %}
27:     {% if page.current %}
28:       <span class="current-page">a</span>
29:     {% else %}
30:       <span class="other-page"><a href="/?page={{ page.number }}">a</a></span>
31:     {% endif %}
32:   {% endfor %}
33:   <span>ss</span>
34: </div>
35: </nav>
36: 
37: <footer>
38: <div><a href="{{ home_url }}">Blogger Grass</a></div>
39: </footer>
40: </body>
41: </html>

GAE にふくまれているテンプレートシステムは Django と呼ばれるものだ。一見、{% ... %} の中には Python のコードがそのまま書けるように思えるのだが、実際には Python 風の独自の文法のようだ。ループや条件判断も書けるが、かなり制限が厳しい。その分、テンプレートを使うコードの方で工夫が必要になる。

上述のリクエストハンドラの 20 〜 25 行目は、テンプレートの 26 〜 32 行目の記述のための「工夫」だ。Django のテンプレートでは、条件判断は変数の値による真偽判定しかできない。変数の値を数値や文字列と比較するというようなことができないのだ。ここでは、現在のページと他のページで生成する内容を変えるため、ページ番号と現在のページか否かのフラグをオブジェクトにくるんで、ページ数分用意してリストに詰めるということをやっている。

Django のテンプレートは複雑な記述には向かない。今回のページ切り替え部分の記述程度でも、テンプレートとそれを使うコードが密に絡んでしまっている。これは将来の変更(デバッグやら拡張やら)の足枷になるにちがいない。

プロトタイプは動くことが大事

フィルタをもとに書き直した部分もふくめて、とりあえず動くものはできた。ただ、フィルタからの書き直し方や、ハンドラ内での情報の保持の仕組みが、どうにも気に入らない。また、Python のこと、GAE のこと、知らないことが多くて、手探り状態での作業になった。動かすので精一杯になっていた。もう少しプラットフォームに慣れないと、見通しの良い設計はできないな。

とはいえ、プロトタイプは動くことが肝心。作る過程、動かそうとする過程(デバッグとも言う)、そして動かすことで課題を見つけることがプロトタイプの役割りだ。

次回は、全体を見直しつつ、リファクタリングしてみよう。

関連リンク

関連記事

2010-09-26

Ruby で書いたフィルタを Python で書き直す #2

今回は、前回、後回しにした Atom 形式のフィードをパースする部分を Python で書いてみる。それには Python (2.5/2.6) が標準で備える SAX ライブラリを使う。そんなわけで、まずは「SAX って何(・д・)?」という疑問から始めよう。

SAX とは

Simple API for XML の略で、DOM と並んで XML 文書を扱うための API として広く利用されている。

(「SAXによるXML文書の操作」より)
SAXでは、DOMのようにXML文書をまるごとメモリに読み込んだあと処理するのではなく、XML文書の先頭から一行ずつ順番に処理をして行く。そのため、どんなに大きなXML文書を処理するときでも、メモリの使用量はそれほど負担にならず、処理も一般に高速だという利点がある。

DOM が言わば、XML 文書をメモリ上の構造に写し取る方法なのに対して、SAX は XML 文書を読み取りつつ(アプリケーションプログラムにとって)必要な部分だけを抽出して扱う方法だ。XML 文書の作成、編集のようなプログラムにとっては DOM が適している。一方、今回のフィルタのようなタイプのプログラムには、まさに SAX がピッタリとはまる。

SAX パーサの動作

以下に示すプログラムに XML データを入力すると、SAX のパーサの動作する様子が見てとれる。

(dummyHandler.py)
 1: #!/usr/bin/python
 2: # -*- coding: utf-8 -*-
 3: # dummyHandler.py: test the SAX parser's behavior.
 4: 
 5: from sys import stdin
 6: import xml.sax
 7: import xml.sax.handler
 8: 
 9: class DummyHandler(xml.sax.handler.ContentHandler):
10:     def startElement(self, name, attributes):
11:         print "start parse: %s" % name
12:         if name == "link":
13:             print "    rel = %s" % attributes['rel']
14: 
15:     def characters(self, data):
16:         print "start characters"
17: 
18:     def endElement(self, name):
19:         print "  end parse: %s" % name
20: 
21: parser = xml.sax.make_parser()
22: handler = DummyHandler()
23: parser.setContentHandler(handler)
24: parser.parse(stdin)

SAX によるパーサを利用するためには、アプリケーション側で ContentHandler のサブクラスを実装しなければならない。それを xml.sax.make_parser で作られるパーサにセットしてやると、パーサがデータを読み取る中で ContentHandler (のサブクラス)のメソッドが呼び出される。

つまり、SAX パーサは、XML データを読みつつ、要素の開始タグを見つけたらセットされたハンドラの startElement を呼び、要素の終了を検出したら endElement を呼ぶ、という動作をする。この他にも startDocumentcharactersprocessingInstruction なども ContentHandler のメソッドとして定義されており、必要ならこれらもハンドラで上書きする。

ちなみにこの DummyHandler は、ググって見つけた資料(→ Chapter 1: Python and XML) に載っていた例を真似て書いた。

Atom Syndication Format

続いて、扱うことになるデータの書式についても簡単に触れておこう。

Blogger のフィードは、特に指定しなければ Atom 形式になる。正確には「Atom 配信フォーマット」と呼ばれるものだ(→ Atom 参照)。

以下に Atom 形式のフィードのサンプルを示す。このブログのフィードから一部を切り出し、単純化したものだ(長い行はバックスラッシュで折り返している)。

(Atom 形式のフィードサンプル)
 1: <?xml version='1.0' encoding='UTF-8'?>
 2: <feed xmlns='http://www.w3.org/2005/Atom'>
 3:   <updated>2010-09-24T21:07:09.324+09:00</updated>
 4:   <title type='text'>lifeLOG + REPOsitory</title>
 5:   <author>
 6:     <name>mnbi</name>
 7:   </author>
 8:   <entry>
 9:     <published>2010-09-24T21:07:00.000+09:00</published>
10:     <updated>2010-09-24T21:07:09.333+09:00</updated>
11:     <category scheme='http://www.blogger.com/atom/ns#' \
          term='2. アプリの断片'/>
12:     <title type='text'>パイプとフィルタで複数のデータの流れを扱うには?</title>
13:     <content type='html'>
14:       前回示したパイプラインは単一のデータの流れを持つシンプルなフィ \
          ルタをつなげたものだった。本来の意味でのフィルタはこういうも \
          のだ。今回はこれを拡張して、最終結果の HTML を複数のファイル \
          に分割するプログラムを作る。各ページにはブログ記事のタイトル \
          (とリンク)が一定数ふくまれる。
15:     </content>
16:     <link rel='alternate' type='text/html' \
          href='http://logrepo.blogspot.com/2010/09/blog-post_24.html' \
          title='パイプとフィルタで複数のデータの流れを扱うには?'/>
17:     <author>
18:       <name>mnbi</name>
19:     </author>
20:   </entry>
21: </feed>

今回の題材となっているフィルタでは、このデータのうち 8 〜 20 行目の entry 要素だけが処理の対象だ。しかも、必要な情報はタイトルとブログ記事への URL のみだから、12 行目の title 要素と 16 行目の link 要素を抽出できれば十分。さらに言えば、記事のタイトルは、記事 URL を href 属性持つ link 要素の title 属性にもなっているから、この link だけを取り出せば事足りる。

Python による実装 (残り)

特定の要素を抽出する

↑で書いたように、link 要素だけを抽出しても目的は果たせるのだけど、今回は SAX を使う練習にもなるので、entry 要素および、そこに包含されている title 要素、link 要素を抽出してみる。

以下が、Python で書いた Atom 形式のフィードから記事のタイトルと URL を抽出するフィルタのコードになる。

(titles_atom.py)
 1: #!/usr/bin/python
 2: # -*- coding: utf-8 -*-
 3: # titles_atom.py: get post tiltes from Blogger Atom feed.
 4: 
 5: from sys import stdin, stdout
 6: import xml.sax
 7: import xml.sax.handler
 8: 
 9: class FeedHandler(xml.sax.handler.ContentHandler):
10:     def __init__(self):
11:         self.entries = []
12:         self.title = ""
13:         self.url = ""
14:         self.inTitle = False
15: 
16:     def startElement(self, name, attributes):
17:         if name == "entry":
18:             self.title = ""
19:             self.url = ""
20:         elif name == "title":
21:             self.inTitle = True
22:         elif name == "link" and attributes["rel"] == "alternate":
23:             self.url = attributes["href"]
24: 
25:     def characters(self, data):
26:         if self.inTitle:
27:             self.title += data
28: 
29:     def endElement(self, name):
30:         if name == "entry":
31:             self.entries.append((self.title, self.url))
32:         elif name == "title":
33:             self.inTitle = False
34: 
35: parser = xml.sax.make_parser()
36: handler = FeedHandler()
37: parser.setContentHandler(handler)
38: parser.parse(stdin)
39: 
40: for pair in handler.entries:
41:     line = '("%s" . "%s")\n' % pair
42:     stdout.write(line.encode('utf_8'))

FeedHandler の実装は単純なのでくどい説明は不要だろう。titleurl は一時変数で、entries が抽出した情報を蓄えておくリストになる。リストの各要素は記事のタイトルと URL のタプルになっている。

注意するのは、42 行目の line に対するエンコードの指定だ。これがないと、write メソッドでエラーが出る。pair が保持する文字列は Unicode 文字列だから、ファイル(標準出力)に書き出す際にエンコードが必要なのは当然なんだが、print を使うとエラーにはならない。print は 2 行目の coding を知っているということなのか。あるいは、print に対しては Python の構築時にデフォルトのエンコードが指定されているのか。

もちろん、UTF-8 を指定しているのは最終的な HTML を UTF-8 にしたいからだ。また、Mac OS X の「Terminal.app」なら UTF-8 にした日本語文字列をそのまま日本語として表示できるからフィルタの動作確認にも便利だ、という理由もある。

次の展開は?

これでようやく「ブログの記事一覧を作るプログラム」を Python (の標準添付ライブラリの範囲)で書き直すことができた。次の目標は、これを GAE に載せる、だ。

いまの作り方だと、Blogger から取得したフィードにふくまれるデータのほとんどを使い捨てている。そこが気になる点だ。取得したデータは GAE 上のストレージシステムに保持するべきだろうか? となると、Blogger で保管されているデータと二重に持つことになる。同期についても考慮しなければならない。まずは、取得したデータを使い捨てする方式で進めるか。

最終的にはブログの記事そのものも GAE 上のアプリ内で表示するようにしたい(→ 「Blogger で作ったブログを iOS デバイス対応にする」の「まとめ」参照)。記事一覧を表示して完成ではない。まだ、先は流そうだ。

関連リンク

関連記事

2010-09-25

Ruby で書いたフィルタを Python で書き直す #1

以前、「単純なフィルタをつなげて複雑なフィルタを実現する」で書いたプログラムを Python で書き直してみよう。

Ruby で書いたフィルタ

シンプルなパイプライン

このパイプラインを実現するために Ruby で書いたフィルタは以下の 4 つ。

  1. all (all_rss.rb)
  2. titles (titles_rss.rb)
  3. mklist (mklist.rb)
  4. mkhtml5 (mkhtml5.rb)

今回は、この内、titles_rss.rb を除く、3 つのフィルタを Python で書き直した。titles_rss.rb を除いたのは、このフィルタではフィードの XML データをパースする必要があるから。Python (2.5) の標準ライブラリにはフィードの XML を扱うライブラリが(RSS、Atom のどちらも)ふくまれていないから。外部のライブラリを使うにしろ、自前で簡単なパースをするにしろ、他のフィルタの書き直しよりは手間がかかるので後回しにした。

Python による実装

以下に示すコードを見ればわかるが、いずれも Ruby のコードとほぼ 1 対 1 で対応が付く。

フィードを取得する (all)
(all_atom.py)
 1: #!/usr/bin/python
 2: # -*- coding: utf-8 -*-
 3: # all_atom.py: get all feeds in the Atom format.
 4: 
 5: from sys import stdin, stdout
 6: import urllib
 7: import re
 8: 
 9: BLOGID = "put your blog id here"
10: FEEDURL = "http://www.blogger.com/feeds/%s/posts/default" % BLOGID
11: 
12: def get_atom(max_results):
13:     params = urllib.urlencode({'max-results': max_results})
14:     f = urllib.urlopen(FEEDURL + "?%s" % params)
15:     res = f.read()
16:     f.close()
17:     return res
18: 
19: # get meta information only
20: md = re.search('<openSearch:totalResults>(\d+)<\/openSearch:totalResults>', get_atom(0))
21: total_posts = int(md.group(1))
22: 
23: stdout.write(get_atom(total_posts) + '\n')
リスト形式に変える (mklist)
(mklist.py)
 1: #!/usr/bin/python
 2: # -*- coding: utf-8 -*-
 3: # mklist.py: make a HTML fragment contains an ordered list.
 4: 
 5: # SPECIFICATION:
 6: # input data must be written in the following format.
 7: # (<title> . <uri>)
 8: 
 9: from sys import stdin, stdout
10: import re
11: 
12: stdout.write('<ol>\n')
13: 
14: regex = re.compile('^\(\"(.*)\"\s.\s\"(.*)\"\)$')
15: for pair in stdin:
16:     md = regex.search(pair)
17:     if md:
18:         stdout.write("<li><a href='" + md.group(2) + "'>" + md.group(1) + "</a></li>\n")
19: 
20: stdout.write('</ol>\n')
HTML として出力する
(mkhtml5.py)
 1: #!/usr/bin/python
 2: # -*- coding: utf-8 -*-
 3: # mkhtml5.py: put input data into a well-formed HTML template.
 4: 
 5: from sys import stdin, stdout
 6: 
 7: stdout.write("""<!DOCTYPE HTML>
 8: <html lang='ja'>
 9: <head>
10: <meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/>
11: <title>Blog Posts</title>
12: </head>
13: <body>
14: """)
15: 
16: for line in stdin:
17:     stdout.write(line)
18: 
19: stdout.write("</body></html>\n")

残るはフィードのパース

次回は、Atom 形式のフィードデータをパースし、記事のタイトルと URL を抽出する部分を書き直そう。実際のフィードデータを見てみると、単に rel 属性が alternate になっている link を抽出すればすみそうなんだが、いまやっていることは Python の練習でもあるんだから、もう少しまじめにパースするコードを書いてみるつもりだ。

関連リンク

関連記事