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 デバイス対応にする」の「まとめ」参照)。記事一覧を表示して完成ではない。まだ、先は流そうだ。

関連リンク

関連記事

0 件のコメント:

コメントを投稿