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-22

Blogger Glass でスタイルを定義する #2

前回からの続き。今回は、実際に CSS によるスタイルを定義する。スタイルのベースになるのは、Style Repository で LOG+REPO 用に提供しているものだ。

画面(ビュー)の構造: 詳細編

共通部分 (src/base.html) の構造は前回示した通り。今回は、画面ごとに変化する領域の構造を示す。

個別画面の構造は、ソース上、以下のファイルで定義されている。

ちなみに、前回は書かなかったが、以下に示す HTML の構造図では、主に CSS のセレクタに記述する書式で HTML の要素を書いている。h1blockquote はそのまま HTML の要素名で、pre.code のように . (ピリオド) に続くのはクラス名だ。また # (シャープ) に続くのは ID を表している。ただし、div 要素については、他と混同する恐れがない限り .pager#contents のように要素名を省略して書くことにする。

一覧表示画面

テンプレート (src/listview.html) を見てもわかる通り、この画面の特徴となる構成要素は、ブログ記事のタイトル一覧(内容表示画面へのリンク付き)とページ切り替え用のリンクだ。

#view-header
|   +-- h2
|   +-- p
#contents
|   +-- ol
|       +-- li
|       +-- ...
#view-footer
    +-- nav
        +-- .pager
            +-- span
            +-- span.current-page
            +-- span.other-page
            |   +-- a
            +-- span
内容表示画面

内容表示画面では、#contents 領域にブログの記事がそのまま流し込まれる。よって、この画面の HTML の構造は src/postview.html と記事の部分の構造を融合したものになる。以下の構造図では、典型的な記事の構造を挿入してある。

#view-header
|   +-- h3
|       +-- a
#contents
|   +-- h4
|   +-- p
|   +-- blockquote
|   +-- h4
|   +-- h5
|   +-- .right
|   |   +-- img
|   +-- p
|   +-- h5
|   +-- p
|   +-- table
|   |   +-- caption
|   |   +-- thead
|   |   +-- tbody
|   +-- h4
|   +-- pre.code
|   +-- pre.terminal
|   +-- ul
|   +-- ...
#view-footer
    +-- #labels
        +-- span.label
        +-- ...
設定画面

主要素の formtable 内に配置されている。table は 第 1 行 (theadに収められている) と 2 行目以降の第 1 列が見出しになる標準的な構造だ。form の要素 (input) は2 行目以降の第 2 列に置かれている。

#view-header
|   +-- h3
#contents
|   +-- form [action='/settings/', method='post']
|   |   +-- table
|   |       +-- caption
|   |       +-- thead
|   |       |   +-- tr
|   |       |   |   +-- th
|   |       |   |   +-- th
|   |       +-- tbody
|   |       |   +-- tr
|   |       |   |   +-- th
|   |       |   |   +-- td
|   |       |   |       +-- input [name='blog_id']
|   +-- form [action='/settings/', method='post']
|       +-- form-buttons
|           +-- input [type='hidden', name='delete', value='yes']
|           +-- input [type='submit', value='Delete Settings']
#view-footer
    +-- (text)
        +-- span.timestamp

デフォルトスタイル

いずれ、デバイスによるスタイルの切り替えを実装する(BG 内に作り込むか、または Style Repository で提供する)が、まずは Mac (または PC) 用のものをデフォルトスタイルとして BG 内に用意することにした。また、画面ごとに専用のスタイルを用意するかどうかは検討中で、デフォルトスタイルは 3 つの画面すべてに共通のものにしている。

スタイルの定義は Style Repository で LOG+REPO 用に提供しているものをほぼすべて流用している。HTML の構造の違いを調整した他は、フォントサイズ、色などはそのまま使っている。全体の横幅と要素ごとのマージンとパディングについてはまだ調整していない。

関連リンク

関連記事

twitter より (2010-10-21)

  • 12:00  何だか良くわかんないラインナップだなあ > new MacBook Air。11インチだけなら「世界で一番薄くて軽いMac」でわかりやすかっただろうに。 → http://bit.ly/a7VMie
  • 12:03  あと、バッテリーの駆動時間が MacBook の半分ってどういうことだ? 計測の基準が違うのか? いくら薄くて軽くてもバッテリーが持たないんじゃ持ち歩きにくいだろうに。やっぱ、良くわからん。
  • 12:15  Lion はまだ1年近く先か。Mac App Storeを別にすれば、LaunchPadもフルスクリーンアプリもMission Controlも、ぜんぶ画面の小さなMac用に思えるよ。→ http://www.apple.com/jp/macosx/lion/
Powered by twtr2src.

2010-10-21

Blogger Glass でスタイルを定義する #1

Blogger Glass (以下、BG)は Blogger で作ったブログの記事を表示するためのアプリだ。その目的は、Blogger の表示用テンプレートを変更することなく、独自の外観でブログ記事を見ることにある。Blogger の提供する(複雑きわまりない構造の)テンプレートに手を入れることなく、記事の見た目を変える。それが BG を作った動機だ。

記事の一覧から内容の表示を作り込み、ブログ閲覧用アプリとしては最小限の機能を備えるようになった。まだ追加したい機能もあるが、このあたりで最初の目的に立ち返り、独自の外観を与えることにしたい。つまり、スタイルを定義するということだ。

テンプレートのリファクタリング

現在、BG には、一覧表示、内容表示、設定という 3 つの画面が定義されている。それぞれ、listview.htmlpostview.html そして settingsview.html というテンプレートから作られている。まずは、このテンプレートたちをリファクタリングすることから始める。

GAE に組み込まれているのテンプレートシステムの Django では、基本となる共通のテンプレートを定義しておき、画面ごとに差分を定義するという方法を使うことができる。ちょうど、(クラスベースの)オブジェクト指向プログラミング言語における、継承による実装の共有のようなことができるのだ。

以下のように、共通となるテンプレートに {% block foo %}{% endblock %} で上書き可能なブロックを定義しておく。

(src/base.html より)
[...snip...]
<div id="contents">
{% block content %}
<p>main content area</p>
{% endblock %}
</div>
[...snip...]

個別のテンプレート側では上書きしたいブロックを定義すれば良い。共通テンプレートの内容から、そのブロックだけを置き換えたものが実際のテンプレートになる。

(src/listview.html より)
{% extends 'base.html' %}
[...snip...]
{% block content %}
<ol>
{% for entry in view.content %}
  <li><a href="/post/?id={{ entry.get_post_id }}">{{ entry.title.text }}</a></li>
{% endfor %}
</ol>
{% endblock %}

画面(ビュー)の構造

続いて、画面の構造の見直し。以下がその概要だ。/html/body/section が、個別の画面により変化する領域で、他は(少なくとも構造は)共通になる。

html
+-- head
|   +-- meta[charset]
|   +-- title
|   +-- link[stylesheet]
+-- body
    +-- header
    |   +-- nav
    |   |   #top-menu
    |   |   +-- span.menu-item
    |   |   +-- ...
    |   +-- h1
    +-- section
    |   +-- #view-header
    |   +-- #contents
    |   +-- #view-footer
    +-- footer

個別画面で変化する画面要素と内容の対応を以下に示す。一部、未実装の内容もふくまれている。

画面要素と表示内容
画面 view-header contents view-footer
一覧表示 ページ番号 記事一覧 ページ切り替えリンク
内容表示 記事タイトル 記事本文 ラベル (未実装)
最終更新日時 (未実装)
設定 「Settings」 設定項目 最終更新日時 (未実装)

今日はここまで。実際の CSS の定義は次回に。

関連リンク

関連記事

2010-10-20

GAE アプリ用に PyUnit を使って単体テストを書く

単体テスト

(「アジャイルソフトウェア開発の奥義」p.34)
テストを最初に書くことによって、これまでとは違った視点で物事を見ざるをえなくなるということだ。テストを書こうとすれば、呼び出す側の立場でプログラムを見るようになる。[...snip...] テストを最初に書くことは、ソフトウェアを呼び出しやすい形式に設計することにつながるのだ。

その上、テストを最初に書こうとすれば、プログラムをテスト可能な形式に設計しようと努力するようになる。[...snip...] テストを最初に書くという行為は、ソフトウェアの分離を強いるのだ

もうひとつの効果は、テストそのものが有益なドキュメントになるということである。[...snip...] この「ドキュメント」はコンパイルも実行も可能であるばかりでなく、常に最新だし、間違いはありえあない。

本体のコードを書く前にテストコードから書き始める、というテスト駆動開発の教義を文字通り実行しないとしても、テストを書くことで違った視点が得られることは確かだ。また、これも良く言われるようにリファクタリングを実行するための後押しにもなる。

そして、何より大事なことは、テストは「動くドキュメント」になるという点だ。

PyUnit

Python には標準添付ライブラリに、単体テストを書き、実行するためのモジュール(unittest)がふくまれている。このモジュールは JUnit の Python 版と言われるだけあって、JUnit を使ったことがあれば、テスト実行の仕組みを理解するのにも、実際にテストを書くのにも苦労しないはずだ。

(単体テストサンプル)
import unittest

# target module 
import foo

class FooTest(unittest.TestCase):
    def test_bar(self):
        self.assert_([...snip...])
    [...snip...]

GAE アプリに適用するには

テスト用のディレクトリを用意する

Python 版の GAE アプリではアプリのソースコードがそのままサーバに送られる。だが、テスト自体のコードをサーバに送るのはムダだ。となれば、アプリのソースとテストはディレクトリを分ける方が良い。

たとえば、こんな風に。

(GAE アプリのディレクトリ構造)
$(APPTOP)/
    +--- src/
    |    +--- app.yaml
    |    +--- main.py
    |    |    ...
    +--- tests/
         +--- run_all_tests.py
         |    ...
モジュールサーチパス

テストをアプリのコードとは別のディレクトリに置く場合、そのままでは import に失敗する。tests ディレクトリにあるテストから src にあるテストの対象となるモジュールを import するには、src をモジュールサーチパスに入れてやらなければならない。

また、GAE のコードであれば、SDK のモジュールをあれこれ import しているはずだから、これもモジュールサーチパスに入れておく必要がある。GData ライブラリのような外部ライブラリを使っているのであればそれも。

今回、モジュールサーチパスを設定するために用意したコードがこれになる。アプリコード用の src に加え、GData ライブラリの atomgdata、そして GAE の SDK のパスも入れてある。

(tests/pathconf.py)
 1: #!/usr/bin/python2.5
 2: 
 3: import os
 4: import sys
 5: 
 6: current_dir = os.path.dirname(__file__)
 7: extra_path = [
 8:     current_dir + '/../src',
 9:     current_dir + '/../src/atom',
10:     current_dir + '/../src/gdata',
11:     '/usr/local/google_appengine',
12:     '/usr/local/google_appengine/lib/antlr3',
13:     '/usr/local/google_appengine/lib/django',
14:     '/usr/local/google_appengine/lib/fancy_urllib',
15:     '/usr/local/google_appengine/lib/ipaddr',
16:     '/usr/local/google_appengine/lib/webob',
17:     '/usr/local/google_appengine/lib/yaml/lib',
18:     ]
19: sys.path = extra_path + sys.path
20: 
21: if __name__ == '__main__':
22:     print sys.path

したがって、実際のテストは以下のように書く。

(tests/skelton_testcase.py)
 1: #!/usr/bin/python2.5
 2: 
 3: import unittest
 4: 
 5: import pathconf
 6: # target module 
 7: import foo
 8: 
 9: class FooTest(unittest.TestCase):
10:     def test_bar(self):
11:         self.assert_(False)
12: 
13: def suite():
14:     return unittest.TestSuite((
15:             unittest.makeSuite(FooTest, 'test'),
16:             ))
17: 
18: if __name__ == '__main__':
19:     unittest.TextTestRunner().run(suite())
テストできない機能も出てくる

単にモジュールサーチパスを整えただけでは、GAE アプリの実行環境すべてをシミュレートできるわけではない。当然、単体テスト環境では実行できない機能も出てくる。

その制約の中で、いかにテスト対象のコードを GAE の実行環境から独立させるかを考えることが、結果としてコードの独立性を高めることにつながるのだ。

テストを書くだけでも効果がある(かも)

Blogger Glass (以下、BG)にテストを書き始めて(というより、テストの組み込み方をあれこれ試していて)すぐにバグが 1 つ見つかった。BG ではsrc/config.py でアプリの設定を src/config.yaml というファイルから読み込んでいる。その部分のコードは以下のようになっていた。

(src/config.py より)
settings = {}

with open("config.yaml") as f:
    for line in f:
        md = re.search('^([^:]+):\s*([^\s].*)$', line)
        if md:
            settings[md.group(1)] = md.group(2)

これを test ディレクトリに置いたテストケースから import すると、config.yaml が見つからずエラーになる。アプリのコードでは import するコード (最新版では src/util.py) も同じディレクトリにあるためエラーにならない。しかし、それでは config モジュールを利用する側に条件を付けることになってしまう。言葉を換えれば情報隠蔽に失敗している。

このコードの open は、正しくは以下のように書かれるべきだったのだ。

(src/config.py の修正)
with open(os.path.join(os.path.dirname(__file__), "config.yaml")) as f:

これなら、src 以外のディレクトリから import してもエラーにはならない。

テストを書こうとすれば、コードを違った視点で見るようになることも、分離を強いられる(コンポーネントの独立性が高まると言っても良い)ことも間違いないようだ。

参考文献

アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技
ロバート・C・マーチン
ソフトバンククリエイティブ ( 2008-07-01 )
ISBN: 9784797347784
おすすめ度:アマゾンおすすめ度

「第4章 テスティング」がテストに関する内容。前半が単体テストを使ったテスト駆動開発について書かれたものになっている。

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

モジュールサーチパスについては、p.416 〜 421 に記述がある。

関連リンク

関連記事

2010-10-19

Blogger Glass に Google Data Python Library を組み込む #2

今回は、実際に Google Data Python Library (以下、GData ライブラリ) を組み込んでみる。feed/{core,parse,post}.py の代わりに GData ライブラリを使うわけだ。

試してみてわかったことだが、Blogger で作った公開ブログの記事を取得するだけであれば、GAE アプリで GData のための認証は必要ない。

準備

ライブラリのソースをコピーする

GData ライブラリのパッケージを展開すると、src 以下に atomgdata の 2 つのディレクトリが見つかる。これがライブラリ本体。GAE アプリで使う場合は、この 2 つを丸ごとアプリのディレクトリ(app.yaml が置かれているところ)にコピーする。こんな感じ(↓)。

[imac] mnbi% tar xzf ~/Downloads/gdata-2.0.12.tar.gz
[imac] mnbi% cd gdata-2.0.12/src
[imac] mnbi% ls
atom/ gdata/
[imac] mnbi% cp -R atom gdata ~/projects/bloggerglass/src

このようにコピーしたら、リクエストハンドラのコードなどから以下のように import できる。

(GData ライブラリの使い方)
import gdata.blogger.client
クライアントとサービス、どちらを使うべきか?

GData ライブラリの内容を見ていると、XxxxClientXxxxService という 2 系統のクラスが存在していることに気付く。ドキュメントやサンプルにも、Client系を使っているものとService系を使っているものの両方がある。

(Client 系)
atom.client.AtomPubClient
+--- gdata.client.GDClient
     +--- gdata.blogger.client.BloggerClient
(Service 系)
atom.service.AtomService
+--- gdata.service.GDataService
     +--- gdata.blogger.service.BloggerService

実装されている機能(メソッド)を見ると、どちらも同じ機能を実装しており、混在させて使う必要はない。XxxxClient を使うなら XxxxService は使わないし、逆もまたしかり。なぜ 2 系統、用意されているのだろう? 公式ドキュメントにはこの理由は書かれていない(少なくともわたしは見つけられなかった)。

BloggerClientBloggerService の機能比較 (抜粋)
BloggerClient BloggerService 機能
get_blogs GetBlogFeed ブログ一覧を取得する。
get_posts GetBlogPostFeed ブログの記事を取得する。
get_post_comments GetPostCommentFeed ブログの記事へのコメントを取得する

ライブラリのソース中のコメントなどから推測すると、どうやら XxxxService は古いライブラリのようだ。いずれ deprecated 扱いになるようで、これは GData のプロトコルのバージョン(1 と 2 がある)と関係があるらしい。

簡単に言えば、XxxxClient を使え、ってことだ。Blogger Data API の場合、gdata.blogger.client.BloggerClient を使うことになる。

リクエストハンドラの変更

一覧表示

基本は gdata.blogger.client.BloggerClient をインスタンス化して、そのメソッド get_posts を呼び出せば OK。Blogger Glass では記事の一覧を一定数ごとにページに分けるので、目的のページに合わせて start_index を調整し、クエリパラメータとして、このメソッドにわたす。

(src/main.py より)
import gdata.blogger.client
[...snip...]
PAGESIZE = 25

class MainHandler(webapp.RequestHandler):
    [...snip...]
    def fill_viewinfo(self, page):
        client = gdata.blogger.client.BloggerClient()
        settings = get_settings()
        start_index = page * PAGESIZE + 1
        q = gdata.blogger.client.Query(start_index=start_index,
                                      max_results=PAGESIZE)
        feed = client.get_posts(settings.get('blog_id'), query=q)
        if feed:
            [...snip...]
            self.viewinfo.content = feed.entry
            [...snip...]

ページごとの記事数をデフォルト値(25)から変更しない限り、max_results を指定する必要はない。ここでは start_index を計算するために記事数(PAGESIZE)を使っているので、そのまま max_results としてわたしている。こうしておけば、PAGESIZE の値を変えることで取得する記事数も変わるようになる。さらに一歩進めて、ページごとの記事数も settings から参照できるようにすれば、実行時に記事数を変えることもできる。ま、それはまた別の機会に。

クエリパラメータとして指定できる項目については、Protocol Reference - Google Data Protocol にまとめられている。実際に、コードでどう指定するかについては、gdata.client.Query のソースに書かれたコメントを参照すること。

ただし、Blogger API では qauthor はサポートされていない、とのこと(→ Blogger query parameters reference - Reference Guide - Blogger APIs)。また、updated-minupdated-max を指定する際には、orderby に "updated" を指定しなければならない、とのこと。

上記の get_posts の戻り値は gdata.blogger.data.BlogPostFeed のインスタンスになる。このインスタンスの entry 属性は gdata.blogger.data.BlogPost のインスタンスのリストになっていて、これが個々の記事の内容になっている。このため、self.viewinfo.contentgdata.blogger.data.BlogPost のリストを指すことになる。

(src/listview.html より)
<div id="content">
<ol>
{% for entry in view.content %}
  <li><a href="/post/?id={{ entry.get_post_id }}">{{ entry.title.text }}</a></li>
{% endfor %}
</ol>
</div>

よって、テンプレート中の記述で entryself.viewinfo.contentgdata.blogger.data.BlogPost のインスタンスを指している。つまり、entry.get_post_identry.title.textgdata.blogger.data.BlogPost の属性参照(とメソッド呼び出し)になっている。このような記述のしかたは、ある意味、バックエンドの仕組みがプレゼンテーション層に漏らしていることになるので、ちょっと気になる。とはいえ、データを別のオブジェクトに詰め直すのもムダだし、GData ライブラリはスタンダード的なものでもあるから、あえて独自のオブジェクトを作る必要はないと判断した。

内容表示

こちらの変更では少し苦労した。というのも、gdata.blogger.client.BloggerClient には、「Post ID」を指定して個別の記事の内容を取得するためのメソッドが用意されていなかったからだ。

そこで、gdata.blogger.client.BloggerClient を継承した BloggerPostClient を作り、そこに「Post ID」で記事の内容を取得するメソッド get_one_post を定義した。肝になるのは get_feed の呼び出しで desired_classgdata.blogger.data.BlogPost を指定している部分。GData プロトコルで「Post ID」を指定したときに返ってくるデータは、単純にブログ記事を要求したときとは違う形式になっている。GData ライブラリを使うときにはどういうデータが返ってくるのかに合わせて、どのデータクラスを使うかを変えなければならない。ちなみに、これらのデータクラスは gdata.blogger.data モジュールで定義されている。

(src/postview.py より; #1)
class BloggerPostClient(gdata.blogger.client.BloggerClient):
    def get_one_post(self, blog_id, post_id):
        """Get one post data specified with post_id,
        then return it as an instance of BlogPost.
        """
        if post_id != 0:
            url = (gdata.blogger.client.BLOG_POST_URL % blog_id) + ('/%s' % post_id)
            return self.get_feed(url, auth_token=None,
                                 desired_class=gdata.blogger.data.BlogPost,
                                 query=None)
        else:
            # when post_id was not specified, get the latest post.
            q = gdata.blogger.client.Query(max_results=1)
            feed = self.get_posts(blog_id, query=q)
            # Since 'get_posts' returns an instance of BlogPostFeed,
            # the 1st item (an instance of BlogPost) of BlogPostFeed.entry
            # must be return.
            if feed:
                return feed.entry[0]
            else:
                return None   

    GetOnePost = get_one_post

リクエストハンドラ(PostViewHandler)の方は、↑のクラスをインスタンス化して、get_one_post を呼び出すだけ。

(src/postview.py より; #2)
class PostViewHandler(webapp.RequestHandler):
    [...snip...]
    def fill_viewinfo(self, post_id):
        client = BloggerPostClient()
        settings = get_settings()
        entry = client.get_one_post(settings.get('blog_id'), post_id)
        if entry:
            self.viewinfo.title = entry.title.text
            self.viewinfo.permalink = entry.get_html_link().href
            self.viewinfo.content = entry.content.text

ここでは、テンプレートにわたす情報として gdata.blogger.data.BlogPost そのものではなく、PostViewInfo (のインスタンス)に「詰め直し」をしている。こうすることでテンプレート側は修正する必要がなくなったが、一方で、一覧表示の方との実装方針(上述)に齟齬をきたしているとも言える。ま、些細なことなんだがね。

おまけ: GAE アプリで favicon.ico を指定する

GAE の管理コンソールでは、エラーの発生したリクエスト(の URI)を見ることができる。Blogger Glass を稼動させて以来、ずっと /favicon.ico がエラーになっていた。これが気になったので、favicon を置くことにした。ちなみに、→ がそのデザイン(意匠)だ。

ただし、単に favicon.ico をアプリのトップディレクトリに置いてもダメ。ググってみたところ、GAE 公式ドキュメントの FAQ 「Google App Engine の一般的質問」の中に favicon.ico の置き方が書かれていた。

Blogger Glass では、favicon.icoimages ディレクトリに置き、app.yaml に以下の記述を追加した。

(src/app.yaml より)
- url: /favicon.ico
  static_files: images/favicon.ico
  upload: images/favicon.ico

関連リンク

関連記事

2010-10-18

Blogger Glass に Google Data Python Library を組み込む #1

まずは調査。Google Data Python Library (以下、GData ライブラリ)ってどうやって使うのか? もちろん、GAE のアプリに組み込むことを前提として。

GData ライブラリとは?

(「Developer's Guide Overview - Google Data Protocol」より)
The Google Data Protocol provides a secure means for external developers to write new applications that let end users access and update the data stored by many Google products. External developers can use the Google Data Protocol directly, or they can use any of the supported programming languages provided by the client libraries.

Blogger や Google Docs を使ってユーザが蓄えたデータを見たり、編集したりするプログラムを作るための仕組み(プロトコル)が GData だ。それを使うために各種プログラミング言語用にライブラリが用意されていて、Python 用のものもその 1 つ。

GData (プロトコル) の基本は、HTTP (と XML) で GET や POST のリクエストを投げれば、結果が AtomPub XML で返ってくる、というものだ。このとき、肝になるのは以下の 3 つ。

  1. 認証
  2. リソースの指定
  3. データのフォーマット
認証

公開されているデータ(Blogger のブログ記事とか)は別として、ユーザのデータを閲覧、更新できるのは、それを作ったユーザ自身(と特別に許可された他のユーザ)だけだ。このため GData でデータをやったり取ったりするためには、まずそのプログラムが正当な許可を持っていることを確認しなければならない。それが「認証」。

GData で利用できる認証方式は以下の 3 つ。

このうち、ClientLogin はローカルな環境(例: Mac や iPhone)で動くプログラムで使う方式で、他の 2 つはウェブアプリ(例: GAE アプリ)で使うためのものだ。それぞれの詳細は、上記の各項目に張ったリンク先を参照のこと。

今回 Blogger Glass に組み込むのは、GAE 公式ドキュメントの記事で使われている AuthSub 方式にする。

AuthSub は、簡単に言うと、ウェブアプリの URL に対して認証を要求し、その証明としてトークンと呼ばれるデータを受け取る、という方式だ。受け取ったトークンは GData に対する HTTP リクエストのヘッダに埋め込んで利用する。

このあたりのことは、↑で挙げた GAE 公式ドキュメントの記事にサンプルコードとともに示されている。

リソースの指定

目的が閲覧にせよ、更新にせよ、まずは対象になるデータを特定しなければならない。この、対象となるデータの特定に利用する仕組みがクエリだ。クエリでは「○○という条件に合ったデータ」というように指定する。条件としては、個々のデータに割り当てられた ID であったり、カテゴリ(Blogger ではラベルと呼んでいる)であったり、作成日時や更新日時で絞り込むものなどがある。また、これらの条件はリクエストの URL として表現するので、ウェブアプリ中にリンクとして埋め込める。

Blogger のデータの場合、以下のような URL を用いることになる。

ブログ一覧
http://www.blogger.com/feeds/[user_id]/blogs
ブログの記事一覧
http://www.blogger.com/feeds/[blog_id]/posts/default

と、まあ、これはすでに Blogger Glass でも使っているものだ。GData ライブラリを使えば、これらの URL を知る必要はなくなる。gdata.blogger.BloggerClient のインスタンスに対して、get_blogs (ブログ一覧)や get_posts (記事一覧) を呼び出すだけだ。

データのフォーマット

データのフォーマットは、わたすのも受け取るのも Atom 形式になる。

Blogger Glass では Atom の解析を(必要最小限の分だけ)自前でやっているが、GData ライブラリを使うようになれば、そこもライブラリ任せにできる。

まとめ

GAE のドキュメントやサンプルコードを調べてわかったことは、これまで Blogger Glass の実装で、自前でやっていたフィードの取得や解析を GData ライブラリに任せてしまえる、ということだ。また、認証については、公開されているデータ(Blogger の記事など)であれば必要ない。なので、以下のようなコードで、BLOG_ID のブログの最新記事のデータを取得できる。

(GData ライブラリの利用方法)
import gdata.blogger.client
[...snip...]
client = gdata.blogger.client.BloggerClient()
query = gdata.blogger.client.Query(max_results=1)
feed = client.get_posts(BLOG_ID, query=query)

関連リンク

関連記事

twitter より (2010-10-17)

  • 14:29  「0を1に」「1を10に」「10を100に」っていう分類が(分類そのものではなく、その表現が)おもしろい。もう少し下世話に表現するなら「発明家」「起業家」「経営者」って感じかな。→ http://docs.komagata.org/4633
  • 14:34  なくなるというならケータイに乗り換えても良いんだけど(もうあきらめた)、電話番号だけはそのまま使いたい。あちこちに連絡先として登録してあるから変更するのがメンドウだよ。 → http://bit.ly/99vJnd
Powered by twtr2src.

2010-10-17

Blogger Glass に Google Data Python Library を組み込む #0

今日は予告だけ。

Blogger Glass に対する次の変更は、機能追加ではなく、Blogger からの記事データの取得を Google Data Python Client Library を使って書き直すこと。

それで何が変わるかはまだわからない。記事の検索(ラベルを使った検索)あたりを実装しやすくなると期待しているんだが……。

何事も練習だから。

関連リンク

関連記事