2010-11-13

ソースコードの色付け - google-code-prettify を使う

古いブックマークを整理していて google-code-prettify を紹介するマイコミジャーナルの記事を見つけた(→「【ハウツー】ハイライトもGoogle流 - "google-code-prettify"でソースコードに色付けを」)。

ブログの記事に埋め込むコードの色付けをしたくて調べていたときに見つけたものだったのだろう。ブックマークしただけで放置して(忘れて)しまっていたのは、外部ファイル(JavaScript と CSS)を使うその仕組みが Blogger で使うには不向きだったから。

しかし今は、GAE を外部ファイルの置き場所として使う方法を知っている(→「外部ファイルの置き場所としての Google App Engine」)。さらに Blogger Glass は、まさにこういうモノを組み込むためにあると言って良い。Blogger Glass に組み込み、さらに Blogger のテンプレートにも組み込んでみた。Blogger のテンプレートではスタイルシートと同様 Style Repository に google-code-prettify を置いた。

使い方

マイコミジャーナルの記事にも簡単な使い方が書かれている。公式なドキュメントとしては README が用意されている。

一番単純な使い方。pre 要素のクラスとして prettyprint を指定する。

class ListViewHandler(webapp.RequestHandler):
    def __init__(self):
        self.app = info.App()
        self.view = info.ListView()

    def get(self):
        util.save_url(self.request, self.response)
        util.fill_app_attrs(self.app, self.request.uri)
        label = self.request.get("label", default_value=None)
        page = int(self.request.get("page", default_value="1"))
        if page > 0:
            self.fill_view_attrs(label, page)
            self.response.out.write(util.render_template(self.app, self.view))
        else:
            self.view = info.MetaInfoView()
            self.fill_metainfo(label)
            self.response.out.write(util.render_template(self.app, self.view))

主要な言語は自動的に認識してくれるが、クラスとして明示的に指定することもできる。クラス属性に prettyprint の後に lang-* という形式で追加する。「*」の部分の書き方は README の「How do I specify which language my code is in?」を参照のこと。Python なら py、Ruby なら rb、つまりはソースファイルの拡張子だと思って良いようだ。

class ListViewHandler(webapp.RequestHandler):
    def __init__(self):
        self.app = info.App()
        self.view = info.ListView()

    def get(self):
        util.save_url(self.request, self.response)
        util.fill_app_attrs(self.app, self.request.uri)
        label = self.request.get("label", default_value=None)
        page = int(self.request.get("page", default_value="1"))
        if page > 0:
            self.fill_view_attrs(label, page)
            self.response.out.write(util.render_template(self.app, self.view))
        else:
            self.view = info.MetaInfoView()
            self.fill_metainfo(label)
            self.response.out.write(util.render_template(self.app, self.view))

行番号を表示させることもできる(5行ごと)。これは linenums を追加する。行番号を 1 以外から開始させるには linenums:15 のように番号を指定すれば良い。

class ListViewHandler(webapp.RequestHandler):
    def __init__(self):
        self.app = info.App()
        self.view = info.ListView()

    def get(self):
        util.save_url(self.request, self.response)
        util.fill_app_attrs(self.app, self.request.uri)
        label = self.request.get("label", default_value=None)
        page = int(self.request.get("page", default_value="1"))
        if page > 0:
            self.fill_view_attrs(label, page)
            self.response.out.write(util.render_template(self.app, self.view))
        else:
            self.view = info.MetaInfoView()
            self.fill_metainfo(label)
            self.response.out.write(util.render_template(self.app, self.view))

既存のスタイルと組み合わせることも可能。以下の例は、このブログでこれまで使ってきた code クラスと prettyprint クラスを同時に指定したものだ。ちなみに、複数のクラスを適用する場合はクラス名を空白で区切って並べれば良い。↓の例では class="code prettyprint" と書いている。

class ListViewHandler(webapp.RequestHandler):
    def __init__(self):
        self.app = info.App()
        self.view = info.ListView()

    def get(self):
        util.save_url(self.request, self.response)
        util.fill_app_attrs(self.app, self.request.uri)
        label = self.request.get("label", default_value=None)
        page = int(self.request.get("page", default_value="1"))
        if page > 0:
            self.fill_view_attrs(label, page)
            self.response.out.write(util.render_template(self.app, self.view))
        else:
            self.view = info.MetaInfoView()
            self.fill_metainfo(label)
            self.response.out.write(util.render_template(self.app, self.view))

スタイルの調整

prettyprint クラスのスタイルは、配布パッケージにふくまれている prettyprint.css で定義されている。ただ、主にトークンの種類による色分けの定義のみで、フォントサイズやフォントの種類については定義されていない。つまり、ブログのスタイルを引き継ぐことになる。

prettyprint.css を組み込む位置に注意すれば、ブログの CSS 中で prettyprint クラスのスタイルを追加(あるいは上書き)することができる。

実際、上の例では以下のようなスタイルをブログ側のスタイルで定義している(Blogger Glass の場合)。

pre.prettyprint {
    margin: 0 2em;
    padding: 5px 1em 5px 1em;
    border: #cccccc 1px dotted; /* silver */
    font-size: 10pt;
    font-family: "Monaco", monospace;
    overflow: auto;
    background-color: #ffffff;
}

これは従来の pre.code のスタイル定義と背景色以外が同一のものだ。

関連リンク

関連記事

twitter より (2010-11-12)

Powered by twtr2src.

2010-11-12

Apple TV、買った

Apple TV をリンゴマークを使って「tv」のように表記するのは良いけれど、これ Mac (と iOS デバイス) 以外で見ている人にはきちんと表示されないだろうな。Apple のサイトでもテキストには「Apple TV」と書いてある。「tv」という表記にはすべて画像が使われている。その画像を真似て、ロゴを作ってみた。フォントは Lucida Grande。同サイズだとリンゴマークが大きいので「tv」のサイズを大きくして調整。ほぼ Apple のサイトにある画像と同じになった。少し、「tv」の文字が細いかな。

さて、tv の国内販売開始について知ったのが昨日の夜(22:15 ごろ)。同時に iTunes Store で「映画」の販売・レンタルが開始されたことも知った。

iTunes を開き、Store を覗くと、もう「映画」というカテゴリができていた。最近のものから、古い(マニアックな)ものまで、結構、数も揃っているように見えた。試聴(予告編と呼ばれている)もできて、しかも 3 分と長い。iMac (の iTunes) で見るうち、「ああ、やっぱ TV の方で見たいわ」と思ってしまった。さらに……

tv を買えば TV に映せるよな」
「む? 円高のおかげで ¥8,800.- なのか」
「お! Apple Store で 24 時間以内に出荷になってる。」
「あ! ポチっちゃった」

そろそろ 24 時間たつけど、まだ「出荷のお知らせ」は届かない。届くのは早くても日曜(2010-11-14)かな。

追記@2010-11-17:
届いたのは 2010-11-13。設置と第一印象についてはこちら → 「Apple TV、届いた!

ライバルは PlayStation®Store

ウチの TV には PS3 がつながっているので、PlayStation®Store でのビデオ販売・レンタルを利用している。始まった当初はアニメが数本という程度だったが、ゆっくりとその数が充実するとともに映画の配信も始まり「これはこれで良いかも」とそこそこ楽しんでいた。もっとコンテンツの幅(ジャンルという意味)を増やして欲しいとか、コンテンツによってはセル(販売)しかないってどういうことだとか、新しいものはともかく古いのはもう少し安くても良いんじゃないかとか、少々の不満はあってもネット配信の手軽さは魅力的だ。

実を言えば、PlayStation®Store で実際に体験するまでは映像配信に関しては懐疑的だった。その理由は 2 つ。(1) 映像の容量(とくに HD のそれ)に対してネットワークの速度(と帯域)が不足していると思っていたこと、(2) 映像を大量に保存するにはローカル(個人が用意できるという意味)のストレージの容量が不足していると思っていたこと。

とくに (1) のネットワーク速度(と帯域)が不足する(はず)という思いは強かった。光だ何だと言ったところで、しょせんは皆で分け合う方式。利用者が増えれば、コンテンツが大きくなればいつだって不足するのだ。そういう思い込みが強いから、そこにギガバイトクラスのデータを流すサービスはなかなか信用することができない。

だから、PS3 という「すでにそこにあるきっかけ」がなかったら、映像配信サービスを利用してみようとは思わなかったはずだ。ゲームのために買い、TV とネットワークにつないだ。その環境はそのまま映像配信にも使えるものだった。気付けば、もうボタンを押すだけで映像配信を体験できるようになっていたのだ。

そして、実際に体験してみると、現状のネットワーク速度(帯域)も十分に対応できることがわかった。

というのも、映像データの場合、30 分のデータだとして、その全体を一度に必要とするわけではない。PlayStation®Store の場合、PS3 でデータを受信しつつ再生することができるため、最初の数分間分のデータが受信できていれば十分なのだ。何も数百メガ、数ギガのデータがすべてダウンロードできるまで TV の前でボーっと待っている必要はないのだ。

わかってしまえば当たり前のことなんだけどね。

また、配信によるレンタルという形式に慣れてしまうと、わざわざセル(販売)コンテンツが必要だとは思えなくなる。同じ映画をそうしょっちゅう観るものでもないからね。ストレージもそれほど必要ではないってことだ。実際、ウチの初代の PS3 (HDD は 60GB) で外付けの増設 HDD も付けていないが、レンタル中心なので困ったことはない。

見終わった後で、せっかくダウンロードした数百メガ、数ギガを削除するときには「もったいないなあ」と思うけどね。まあ、それはわたしがオールドタイプってことだろう。

予告編だけでも楽しそう

話を tv に戻そう。すでに PS3 で映像配信を(そこそこ)楽しめているのに、今さら tv を買おうと思ったのはなぜか? 実は予告編(プレビュー)の長さだ。

PlayStation®Store に対する不満の 1 つにプレビューできないことが挙げられる。iTunes Store ではそれができる。それも 3 分だ。3 分と言えばそこそこ長い。ちょっとしたヒマつぶしに最適じゃないだろうか?

予告編だけを次々に見ていくのも楽しそうだと思った。それが tv を買った理由だ。いや、コンテンツもそこそこ豊富そうだから、レンタルするのも楽しみだけどさ。

関連リンク

関連記事

twitter より (2010-11-11)

Powered by twtr2src.

2010-11-11

Snow Leopard アップデート (10.6.4 → 10.6.5)

10.6.4 のアップデートは 2010-06-15 だったようなので、およそ 5 ヶ月を経ての更新になる。ちなみに 10.6.3 は 2010-03-29 、10.6.2 は 2009-11-09 にそれぞれリリースされている。

10.6.2 へのアップデートは記録してあるが(→ 「Snow Leopard アップデート (→ 10.6.2)」)、10.6.4 と 10.6.3、そして 10.6.1 のときは忘れたようだ。10.6 が届いたときのことは記録がある(→ 「Snow Leopard、届いた。」、「Snow Leopard、MacBook にインストールした。」)。

iMac & MacBook

iMac (Mid 2010) は届いた時点で 10.6.4 だったから、これが最初の OS アップデートになる(セキュリティアップデートは除く)。

Snow Leopard 10.6.5 update
iMac (Mid 2010)
680.1MB にはちょっと驚いた。CD-ROM に 1 枚に収まらないかも?
MacBook (Early 2008)
517.3MB。iMac 用よりはかなり小さいが、それでも……。

今回のアップデートはかなり巨大だ。変更点を読んでも、ユーザに取って目立った修正はないようだけど。

右のスクショはアップデート後の iMac の「About Mac」パネルだ。

Mac mini Server

サーバ版のアップデートも巨大だ。10.6.2 のときのアップデートが、同じ mini に対して 524.5MB でその大きさに驚いたが、今回はその 1.5 倍に近い。

Snow Leopard Server 10.6.5 update
Mac mini Server (Late 2009)
741.6MB。こちらは確実に CD-ROM じゃ配布できない。

mini の「About Mac」パネルのスクショも貼っておく。iMac の方と違うのは「Server」の文字の有無だけだ。

カーネルバージョン

10.6.2 のときに記録したので、今回も記録しておく(バックスラッシュで折り返してある)。今回も、サーバとクライアントで全く同一のバージョンになっている。

[mini] mnbi% uname -a
Darwin mini.private 10.5.0 Darwin Kernel Version 10.5.0: \
Fri Nov  5 23:20:39 PDT 2010; root:xnu-1504.9.17~1/RELEASE_I386 i386
[imac] mnbi% uname -a
Darwin imac.private 10.5.0 Darwin Kernel Version 10.5.0: \
Fri Nov  5 23:20:39 PDT 2010; root:xnu-1504.9.17~1/RELEASE_I386 i386

Darwin Kernel Version の 2 つ目(10.5.0 の 5 の部分)が Snow Leopard のバージョンの 3 つ目 (10.6.5 の 5 の部分) と一致しているのかも。

ふと、カーネルファイルの情報を見てみたくなって file コマンドで確認してみた。

[imac] mnbi% file /mach_kernel                                            [~]
/mach_kernel: Mach-O universal binary with 3 architectures
/mach_kernel (for architecture x86_64): Mach-O 64-bit executable x86_64
/mach_kernel (for architecture i386): Mach-O executable i386
/mach_kernel (for architecture ppc): Mach-O executable ppc

PPC Mac をサポートしない Snow Leopard のカーネルにどうして ppc 用のバイナリがふくまれているんだろう(・д・)?

他のコマンドも見てみた。まずはシェル。

[imac] mnbi% for shell in /bin/*sh; do; file $shell; done                 [~]
/bin/bash: Mach-O universal binary with 2 architectures
/bin/bash (for architecture x86_64): Mach-O 64-bit executable x86_64
/bin/bash (for architecture i386): Mach-O executable i386
/bin/csh: Mach-O universal binary with 2 architectures
/bin/csh (for architecture x86_64): Mach-O 64-bit executable x86_64
/bin/csh (for architecture i386): Mach-O executable i386
/bin/ksh: Mach-O universal binary with 2 architectures
/bin/ksh (for architecture x86_64): Mach-O 64-bit executable x86_64
/bin/ksh (for architecture i386): Mach-O executable i386
/bin/sh: Mach-O universal binary with 2 architectures
/bin/sh (for architecture x86_64): Mach-O 64-bit executable x86_64
/bin/sh (for architecture i386): Mach-O executable i386
/bin/tcsh: Mach-O universal binary with 2 architectures
/bin/tcsh (for architecture x86_64): Mach-O 64-bit executable x86_64
/bin/tcsh (for architecture i386): Mach-O executable i386
/bin/zsh: Mach-O universal binary with 3 architectures
/bin/zsh (for architecture x86_64): Mach-O 64-bit executable x86_64
/bin/zsh (for architecture i386): Mach-O executable i386
/bin/zsh (for architecture ppc7400): Mach-O executable ppc

なんでウチで使っている zsh だけ...。

/bin/usr/bin を簡単に調べたところ、1/3 ぐらいはまだ ppc 用のバイナリを持っているようだ。

[imac] mnbi% ls -1 /bin /usr/bin | wc -l
    1119
[imac] mnbi% for f in /bin/* /usr/bin/* ; do; file $f; done | fgrep 'Mach-O executable ppc' | wc -l
     396

Snow Leopard って ppc を切り捨てて身軽になったっていうのがウリの 1 つじゃなかったっけ?

サーバ管理ツールもアップデート

OS 自体のアップデートと関係しているのかどうかはわからないが、Snow Leopard サーバに付属のサーバ管理用のツール(クライアントの Mac にインストールして使う)もアップデートされている。左のスクショはその通知。

ウチではこのツールを(Mac mini Server を遠隔管理するために) iMac と MacBook にインストールしているが、OS のアップデートが完了してからこの通知が現れた。OS のアップデート前には「ソフトウェア・アップデート」で検索させても出てこなかった。(ツールの)バージョンも 10.6.5 だし、サーバ OS と連動しているのかもしれない(今回だけ?)。

関連リンク

関連記事

2010-11-10

Time Machine から古いバックアップを削除する

iMac の Time Machine によるバックアップ先には Mac mini Server (Late 2009) の 2 台目の HDD を指定している。この HDD は同時に mini 自身の Time Machine も使っている。この HDD の容量が少なくなってきた。もともと、mini に内蔵の HDD だから、2.5 inch で 500GB のものだ。2 つの Mac のバックアップを引き受けるには容量不足なのかも。

外付けの HDD を(iMac あるいは mini に)増設することも考えたが、まずは落ち着いてゆっくり、本当に必要なのかを考えてみた。

まず、第一に、わたし自身のコンピューティングのスタイルとして Time Machine のバックアップに頼ることがほとんどないという事実がある。実際、これまで Time Machine からファイルを復元したのは、MacBook、mini、そして iMac の 3 台を通じて、ほんの 1、2 度だ。Time Machine のバックアップは HDD (iMac なら SSD) が壊れた場合の保険だと考えている。日常のファイル操作に対しての「あともどり」のためのものだとは思っていない。「あともどり」が必要なファイルなら git のような管理ツールを使う。

HDD が壊れた場合の保険なら、現状の復旧ができれば良いわけで、古いバックアップは必要ない。

第二に、iMac を買う前は mini をデスクトップ Mac として使っていたため、古いバックアップにはサーバ用途には必要のないファイルが多数含まれているはず(デスクトップとして必要なものは iMac に移行ずみ)。実際、現状の mini の Server HD (1 台目の HDD) は 25GB 程度しか使われていない。

この 2 つの点から、今の mini のバックアップデータにはムダが多いと判断した。必要なモノのためなら HDD を新設するのにやぶさかではないが、ムダなモノを入れるためには買いたくない。

結論として、mini 自身のバックアップファイルを削除することにした。それも丸ごとだ。今の mini はサーバ用途なので、現状が維持できればそれで良い。古いバックアップは必要ないのだ。

削除の手順

以下の手順は、これまでのバックアップを丸ごと捨てるためのものだ。乱暴な方法なので、少しでもバックアップに未練があるならやらない方が良い。Time Machine のバックアップから古いファイルを削除する正しい方法はヘルプに書かれているものだ(→「バックアップディスクからファイルを削除する」)。

  1. Mac mini に管理者でログインする。
  2. 「システム環境設定」>「Time Machine」を開き、Time Machine を「切」にする。
  3. Finder から Macintosh HD2 (mini の Time Machine のバックアップ先に指定したボリューム)を開く。
  4. Backups.backupdb というフォルダを開く。
  5. Mac mini の名前のフォルダがあるので、それをゴミ箱に入れる。
  6. ゴミ箱を空にする。
  7. 「システム環境設定」>「Time Machine」を開き、Time Machine を「入」にする。
  8. 初回のバックアップが完了するのを待つ。

現在、削除中……

今、手順の 6。3 時間以上かかってもまだ終わらない。Finder から削除しようとしたのは失敗だったか。ターミナルから rm すべきだったかも...orz

現状、mini のディスク使用量は約 25 GB 程度なので、180 GB ほどの空きが増えることになる。iMac で HD な podcast (動画)を取ってきたりしなけば、当分持つだろう。

こんなことが必要なのかどうか、実は良くわからない。Time Machine はディスクが一杯になれば古いバックアップを削除するらしい。放っておけばディスクが一杯になって、古いバックアップが消えていく。それなら何もする必要はない。気になるのは、1 台のディスクに複数の Mac のバックアップを置いている場合、どの Mac のバックアップが消えることになるのか、という点。

ウチの場合、mini の(古い)バックアップが消える分には構わないけれど、iMac の方が消えるとちょっとイヤ。mini の Time Machine が動いている時にディスクが一杯になったのなら mini のバックアップが消えそうだけど、iMac の Time Machine がバックアップしている最中に一杯になったら iMac の方が消えそうだ。

もっとも、そうなったとして実害はない。なにせ、最初に書いたように Time Machine のバックアップは HDD が壊れた場合の現状復旧が目的だから。ただ、mini の古いバックアップがずっと HDD に居座って、iMac の(比較的)新しいバックアップが追い出されるかもしれない、っていう状況が気に入らないだけ。

MacBook の Time Machine のバックアップ先にしている Time Capsule の方は比較的余裕がある。なんなら、mini の Time Machine のバックアップ先も Time Capusle にすれば……。これを書いている最中にそんなことを思い付いたが、本末転倒なのでやめた。そうするぐらいなら iMac の Time Machine をそちらに向けるべきだろう。

ま、次に一杯になりそうになったときは、素直に iMac に Time Machine 用の外付け HDD を買うことにしよう。

そう言えば、MacBook の HDD も空き容量が少なくなってきていたんだった。こちらは HDD を換装するか。あるいは、iOS デバイスの母艦としての役割を iMac に移すか。後者の方が適切な使い方なのは確かだな。

関連リンク

関連記事

2010-11-09

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

Blogger Glass (以下、BG)を作ったのは「Blogger で作ったブログを Blogger とは独立した表示システムで見る」ためだ。そもそもそんなことを考えるようになったのは、Blogger のブログ表示用テンプレート(とスタイル)の構造が複雑だったから。御仕着せのスタイルはどれもしっくりこない。かといって自前でスタイルを定義しようとすればテンプレートの複雑さが立ちはだかる。

それでも複雑なテンプレートと格闘し、どうにか自前のスタイルで見られるようになり、iOS デバイスにも対応した。が、そこで力尽きた。同じことを繰り返す気力が失せた。けれど、この先きっと、デバイスは増えるだろうし、スタイルにも飽きるに違いない。いずれ、また、どうにかしたくなるときが来る。

やがて BG に至る「アプリのかけら」を作り始めたときには、ただのプログラミングの練習のつもりでしかなかった。Ruby で書いてみてPython に書き直し、せっかくだから GAE に載せた

記事の内容が表示できラベルで検索もできる。iPhone でアクセスすれば、iPhone アプリっぽく見えるように外観も整えた。ここまで来れば、BG だけで自分のブログを読むことができる。実際、最近は LOG+REPO の記事を探したり読んだりするのに BG だけで済んでいる。

しかし、まだ 1 つ、機能が足りない。BG だけで(つまり、blogspot にアクセスすることなく)ブログを読むためには、どうしてもあと 1 つ欲しい機能がある。それは、ブログの記事本文に置かれた内部リンク(同じブログの他の記事へのリンク)の貼り替えだ。

(ラベル検索の結果をふくむ)一覧表示画面から記事の内容を表示させることはできる。しかし、その記事本文に張られた内部リンクは blogspot のエントリ(すなわち permalink)を指したままだ。記事表示画面から関連記事を開けば blogspot に行ってしまう。BG だけで読み続けることができないのだ。

そう、最後に残る(基本)機能は、記事中の内部リンクの置き換えになる。

デザイン (意匠と設計)

この機能は外観には影響を与えない。記事中の一部のリンク先が変わるだけだから。よって意匠的には付け加えるものも、変更になるものもない。

一方、仕組みの設計としてはいくつか(これまでにない)考慮点がある。

記事の permalink と (BG が記事表示に利用する) ポスト ID については、どちらも Blogger から取得できるフィードにふくまれており、GData Python ライブラリを使って簡単に取り出すことができる。ポスト ID は一覧表示画面でフィードから取り出し記事表示画面へのリクエスト URL の作成に使っているし、permalink も記事表示画面中で記事タイトル部分にリンクとして貼り付けてある(つまり BG の記事表示画面で記事タイトルをたどると blogspot の同じ記事が開く)。記事本文から permalink に張られたリンク要素を抽出し、href 属性の値を /post/?id=1234567890 のようなもので置き換える。これも正規表現を使うなどすれば難しいことではない。

ここまでは問題ない。問題なのはここから。

問題は大きく 2 つある。すなわち、(a) フィードをいつ、どうやって取得するか。(b) permalink とポスト ID の組み合わせを、どこにどのように(そしていつまで)保持するか。この 2 つだ。

簡単なのは (b) の方。こちらは主に選択の問題。GAE ではデータの保存先は memcache かデータストアの 2 種類だ。どちらを使うかは容量と保存期間で選ぶことになる。あるいは、両者を組み合わせるか。いつまで保持するかについては、ユーザに管理させる(設定画面にクリアボタンを付ける等)か、アプリ側で適当な時期に消すか。データ量としては大した量でもないから前者もアリだが、アプリ(の管理者)にとっては後者の方が良い。

(a) が難しい理由はユーザ体験に関わるから。ブログの全記事を毎回取得するという方法は設計と実装が単純になるが、取得に時間がかかる可能性がある。今の LOG+REPO 程度の記事数(300足らず)なら体感するほどの遅れはないかもしれないが、対話型システムにとって大量のデータを同期的にやったり取ったりするのはできるだけ避けたいもの。それに、iPhone で 3G 回線を使っているときのことを考えれば転送量は少ない方が良い。

今、考えている方法としては、(a1) 他の目的で取得したフィードにふくまれている分だけを記録していく方法、(a2) ユーザに明示的に全取得を指示してもらう方法、の 2 つ。(a1) では一覧表示画面や記事表示画面を作る際に取得するフィードから、そこにある分だけ permalink とポスト ID の組み合わせを拾い出して記録しておき、リンクの置き換えに際しては記録にある分だけを対象とするというものだ。(a2) は設定画面等で記事データの取得ボタンを付けるというようなもの。

(a1) の場合、データの取得は、これまでも内部リンクの置き換え機能の有無に関係なく行っていることで、ユーザ体験として変わるところはない。一度も取得していない記事へのリンクは置き換えられないことになる。とはいえ、記事の内容とは違い、permalink とポスト ID の対応は通常変わることはないから、BG で一度でも表示していれば(一覧表示でも、検索結果表示でも良い)情報が記録されることになり、実用上はこれで十分と言えるかも。

まずは、(a1) 方式で作ってみよう。この場合、むしろ「いつ消すか?」の方が難しいか。「適当な時期」っていつかな。単純に経過時間で切るか。あるいは、参照頻度を加味するか。まずは「消さない」ように作るか(というより「消す」コードを書かないって表現すべきだな)。

追記@2010-12-08

「内部リンク置き換え」機能の実装については、以下の後続記事を参照。

関連リンク

関連記事

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 をキーとしてグローバルデータとして保存するのでも良い。とりあえず、セッション固有データとして保存する方式で作ってみようか。

関連リンク

関連記事