2010-09-24

パイプとフィルタで複数のデータの流れを扱うには?

前回示したパイプラインは単一のデータの流れを持つシンプルなフィルタをつなげたものだった。本来の意味でのフィルタはこういうものだ。今回はこれを拡張して、最終結果の HTML を複数のファイルに分割するプログラムを作る。各ページにはブログ記事のタイトル(とリンク)が一定数ふくまれる。

今回のプログラムは、一応「パイプとフィルタ」の形をしているが、その範疇に収まらない構造になっている。なぜ、そうなるのか? それは、プログラムに対する要求がシンプルなパイプラインで実現できる範囲を超えているからだ。最終結果が複数の HTML ファイルである以上、標準出力だけでは作れない。

処理の分割

前回と同様に、まずは全体の処理を複数のステップに分割することから始めよう。

  1. フィードを取得する
  2. 特定の要素(タイトルと URL)を抽出する
  3. データを分割する
  4. 分割されたそれぞれをリスト形式(HTML の OL)に変える
  5. 分割されたそれぞれを HTML として出力する

大きな違いは 3. 追加されたこと。さらに、4. および 5. は処理自体は前回の 3.、4. と同じだが、今回は分割されたデータごとに処理を行う必要がある。

分割されたデータを標準入出力でつながったメインのパイプに流すことはできない。それとは別のデータの流れを用意しなければならない。今回は、単純に中間ファイルを作ることにする。また、中間ファイルの名前をメインのパイプ(標準入出力)に流す。こうすることで、後のステップを担当するフィルタプログラムは、分割の詳細(何個に分けられたのか、それぞれのファイル名は何か)を気にしなくても良くなる。

つまり「データを分割する」は、より詳しく書くと「データを分割し、それぞれをファイルに書き出し、その名前を標準出力に書く」となる。また、分割後のステップは「標準入力からファイル名を受け取り、そのデータに処理を加え、(新しい)ファイルとして書き出し、その名前を標準出力に書く」になる。

以上を踏まえると、今回のプログラムを構成するパイプラインは以下のようになる。

データフローが多重化されたパイプライン

最後の rename は、中間ファイルの命名規則で書き出された HTML を、最終結果の名前にリネームするだけのプログラムだ。このステップを設けることで、リスト化と HTML 化に共通の制御フィルタ(後述)を使うことが可能になる。

Ruby による実装

alltitles は前回の all_rss.rbtitles_rss.rb をそのまま使う。

データを分割する
(split.rb)
 1: #!/opt/local/bin/ruby1.9
 2: # -*- coding: utf-8 -*-
 3: # split.rb: split a long list into pieces.
 4: 
 5: PAGESIZE = 25
 6: 
 7: page = 0
 8: start_index = 0
 9: lines = STDIN.readlines
10: length = lines.length
11: 
12: while start_index < length
13:   name = "page_#{page}.dat"
14: 
15:   File.open(name, "w") { |file|
16:     lines[start_index, PAGESIZE].each { |line|
17:       file.puts line
18:     }
19:   }
20: 
21:   STDOUT.puts name
22:   page += 1
23:   start_index += PAGESIZE
24: end

標準入力から行を読み込み、PAGESIZE で設定した行数ごとに中間ファイルに書き出している。ちなみに、ここはちょっと手抜きで、「まとめて読んで」から処理するロジックになっている。

シンプルなフィルタを適用する制御フィルタ
(apply.rb)
 1: #!/opt/local/bin/ruby1.9 -w
 2: # -*- coding: utf-8 -*-
 3: # apply.rb: apply a given filter to specified files.
 4: 
 5: PROCESS_FILTER = ARGV.shift
 6: basename = File.basename(PROCESS_FILTER, ".*")
 7: page = 0
 8: 
 9: STDIN.each { |name|
10:   resultfile = "#{basename}_#{page}.dat"
11:   system("#{PROCESS_FILTER} < #{name.chomp} > #{resultfile}")
12:   STDOUT.puts resultfile
13:   page += 1
14: }

これが、リスト化と HTML 化で使う「制御フィルタ」だ。データに対する処理を担当する「処理フィルタ」は引数として(フルパスを)受け取る。標準入力から読み取るのは(split.rb または apply.rb 自身が作った)中間ファイルの名前だ。「処理フィルタ」の呼び出しは system 関数でコマンドとして呼び出している。

最終結果を収めたファイルをリネームする
(rename.rb)
 1: #!/opt/local/bin/ruby1.9 -w
 2: # -*- coding: utf-8 -*-
 3: # rename.rb: read filenames from STDIN, then rename each files to
 4: # appropriate name.
 5: 
 6: page = 0
 7: 
 8: STDIN.each { |tmpname|
 9:   newname = "page_#{page}.html"
10:   File.rename(tmpname.chomp, newname)
11:   STDOUT.puts newname
12:   page += 1
13: }

標準入力から読み取った名前を、page_*.html の形にリネームしているだけだ。

すべてをつなげる

前回は、実行例は示さなかったが、今回は長くなったのと引数の指定が必要になったので、以下に示しておく。

[imac] mnbi% ./all_rss.rb | ./titles_rss.rb | ./split.rb | ./apply.rb ./mklist.rb | ./apply.rb ./mkhtml5.rb | ./rename.rb

実行結果は page_{0..9}.html という名前で生成される。

多重データストリームを処理するパイプライン

冒頭に書いたように、今回のプログラムは Unix 由来の「パイプとフィルタ」とは呼べない構造になっている。もし、多重データストリームを扱うことができるようなパイプラインを構築することができるなら中間ファイルを使う必要もなくなり、「パイプとフィルタ」と呼べるようになるだろう。しかし、それをシェルのレベルで実現することはやはり難しい。やはり、このあたりが「小さなプログラムをシェルで組み合わせて複雑な処理を実現する」という方法の限界だろう。

今回のプログラムは、いわばコマンドライン版の「ブログ記事一覧を作成する」プログラムだ。ウェブアプリとして作るなら、リクエストごとに作るレスポンスは 1 つだから、最終結果が複数のファイルになることはない(将来のリクエストを見越してあらかじめ他のページを生成しておくなら別だが)。ウェブアプリ版としては、最終結果を標準出力に書き出す前回のものの方が完成した姿に近いと言える。

関連記事

2010-09-23

単純なフィルタをつなげて複雑なフィルタを実現する

フィルタとして実現したプログラムは、解こうとする問題を、より小さな(そしてより簡単な)複数の問題に分割して扱うことを可能にする。また、フィルタをパイプでつなげたパイプラインもやはりフィルタとして扱うことができる。これは、フィルタプログラムを、より小さな(そしてより簡単な)複数のフィルタプログラムに分割して実装できることを意味する。部分と全体を同一視が可能。つまり、「パイプとフィルタ」は「Composite パターン」になっているのだ。

以上を踏まえて、「Blogger で作ったブログの記事一覧を作成する」で書いた「かけら」に戻ってみよう。今回の課題は、これをより小さなフィルタをつなげたパイプラインとして実現することだ。

処理の分割

get_titles.rb は Blogger へのフィードのリクエストから、HTML 化までを 1 つのプログラムで処理するものだった。これを処理の流れに沿って分割するとこうなる。

  1. フィードを取得する
  2. 特定の要素(タイトルと URL)を抽出する
  3. リスト形式(HTML の OL)に変える
  4. HTML として出力する

それぞれを独立したフィルタプログラムとして書きパイプでつなげれば、get_titles.rb と同等の処理が実現できる。

シンプルなパイプライン

Ruby による実装

それぞれのプログラムを見ていこう。

フィードを取得する
(all_rss.rb)
 1: #!/opt/local/bin/ruby1.9 -w
 2: # -*- coding: utf-8 -*-
 3: # all_rss.rb: get all feeds in the RSS format.
 4: 
 5: require 'net/http'
 6: require 'uri'
 7: 
 8: BLOGID = "put your blog id here"
 9: 
10: def get_rss(max_results = 0)
11:   url = URI.parse('http://www.blogger.com/')
12:   res = Net::HTTP.start(url.host, url.port) { |http|
13:     http.get("/feeds/#{BLOGID}/posts/default?alt=rss&max-results=#{max_results}")
14:   }
15:   res.body
16: end
17: 
18: # get meta information only
19: /<openSearch:totalResults>(\d+)<\/openSearch:totalResults>/ =~ get_rss(0)
20: total_posts = Regexp.last_match[1].to_i
21: 
22: STDOUT.puts get_rss(total_posts)  

get_titles.rb との違いは、全記事数を取得するために、max-results=0 のリクエストを投げていること。さらに、そうして取得した全記事数をもとに、一度のリクエストで全ての記事を取得していること、だ。記事が何千、何万とあるなら別だが、たかだか 200 や 300 なら一度のリクエストでまとめて取得した方が効率が良い。最終結果の HTML を分割するなら、それはプログラムの中でやれば良いことだ。

特定の要素を抽出する
(titles_rss.rb)
 1: #!/opt/local/bin/ruby1.9 -w
 2: # -*- coding: utf-8 -*-
 3: # titles_rss.rb: get post tiltes from Blogger RSS feed.
 4: 
 5: require 'rss'
 6: 
 7: rss = RSS::Parser.parse(STDIN.readlines.join, false)
 8: rss.channel.items.each { |item|
 9:   STDOUT.puts "(\"#{item.title}\" . \"#{item.link}\")"
10: }

入力は、RSS 形式のフィード(XML のテキスト)。RSS として読み取り、記事のタイトルと URL だけを抽出している。

get_titles.rb との違うのは、出力の形式。Lisp の cons ペアとしても読み取れる形式にしてある。

リスト形式に変える
(mklist.rb)
 1: #!/opt/local/bin/ruby1.9 -w
 2: # -*- coding: utf-8 -*-
 3: # mklist.rb: make a HTML fragment contains ordered list.
 4: 
 5: # SPECIFICATION:
 6: # input data must be written in the following format.
 7: # (<title> . <uri>)
 8: 
 9: STDOUT.print <<LISTSTART
10: <ol>
11: LISTSTART
12: 
13: STDIN.each { |pair|
14:   if /^\(\"(.*)\"\s.\s\"(.*)\"\)$/ =~ pair
15:     md = Regexp.last_match
16:     STDOUT.puts "<li><a href='#{md[2]}'>#{md[1]}</a></li>"
17:   end
18: }
19: 
20: STDOUT.print <<LISTEND
21: </ol>
22: LISTEND

入力としてわたってくるのは、2 つの文字列が Lisp の cons ペア形式で書かれたもの。取り出すのには正規表現を使っている。

ここで、ちょっと寄り道をしてみる。この段階だけを Gauche (Scheme) で書くとどうなるか?

(mklist.scm)
 1: #!/opt/local/bin/gosh
 2: ; -*- coding: utf-8 -*-
 3: 
 4: ;;; mklist.scm: make a HTML fragment contains ordered list.
 5: 
 6: ;; SPECIFICATION:
 7: ;; input data must be written in the following format.
 8: ;; (<title> . <url>)
 9: 
10: (define (apply-filter proc port)
11:   (let ((s-exp (read port)))
12:     (if (not (eof-object? s-exp))
13:  [begin
14:    (proc s-exp)
15:    (apply-filter proc port)])))
16: 
17: ;; BEGIN
18: (display "<ol>\n" (standard-output-port))
19: 
20: ;; MAIN
21: (apply-filter
22:  (lambda (s-exp)
23:    (let ((title (car s-exp))
24:   (url (cdr s-exp)))
25:      (display #`"<li><a href=',|url|'>,|title|</a></li>\n"
26:        (standard-output-port))))
27:  (standard-input-port))
28: 
29: ;; END
30: (display "</ol>\n" (standard-output-port))

プログラムの骨格は「Gauche でフィルタを書く」で書いたものと共通だ。

↑に示した Ruby 版との違いは、読み取るデータの形式が Lisp のデータそのものになっているため正規表現によるパターンマッチが必要ない、という点だ。データの形式によっては特定の言語の方がすっきりと書けることもある。その実例だ、と言うにはちょっと恣意的なサンプルだけど。

HTML として出力する
(mkhtml5.rb)
 1: #!/opt/local/bin/ruby1.9 -w
 2: # -*- coding: utf-8 -*-
 3: # mkhtml.rb: put input data into a well formed HTML template.
 4: 
 5: print <<HEADER
 6: <!DOCTYPE HTML>
 7: <html lang='ja'>
 8: <head>
 9: <meta content='text/html; charset=UTF-8' http-equiv='Content-Type'/>
10: <title>Blog Posts</title>
11: </head>
12: <body>
13: HEADER
14: 
15: STDIN.each { |line|
16:   STDOUT.puts line
17: }
18: 
19: print <<FOOTER
20: </body></html>
21: FOOTER

入力から流れてくるのは、HTML の OL 部分。ここでは、その先頭と末尾に HTML ファイルとして必要な部分を付け加えることで、全体を HTML ファイルとして仕上げている。

プログラムを小さく分割して作ることの意義は何か?

「小さく分割する」とは、プログラムを互いに独立した要素として作るという意味で、つまりはモジュール化だ。すぐにわかる利点は以下のようなものだ。

  • 理解しやすい
  • テストしやすい
  • デバッグしやすい

何よりテストしやすいことは重要だ。端的に言ってしまえば、今回のように分割することで単体テストレベルのテストが可能になるからだ。テストしやすいからこそ、デバッグもしやすくなる。テストで動作の検証が簡単に行えるなら、リファクタリングも機能拡張もぐっとやりやすくなる。

関連リンク

関連記事

twitter より (2010-09-22)

Powered by twtr2src.

2010-09-22

入力デバイスは消耗品

iMac につないだキーボード(→ Realforce 86U)の掃除をしていて思った。キーボードは消耗品なのだ、と。同じことはマウスにも言える。直接、人の手に触れ、叩かれ、動かされる、これら入力用のデバイスは、いずれ汚れ、摩耗し、劣化し、時には壊れる。TrackPad も消耗していくだろうか?

消耗品であれば、補充について考えておかなければならない。直接手に触れる物だけに、必ずしも新しい製品が馴染むとは限らない。いま使っている物に愛着があるなら生産終了となる前に同じ製品を余分に買っておくべきだ。

もう手に入らない名品

Magic TrackPad を使い始めるまではずっと Apple Pro Mouse を使っていた。2003 年 3 月にわたしにとっての最初の Mac (チタニウム PowerBook G4)を手に入れて以来の付き合いだから、7 年以上使い続けたことになる。

Mac ユーザ以外には決して受け入れられることのなかった 1 ボタンマウスだ。いや、Mac ユーザの多くも(余程、古くからのユーザでない限り)マウスに関しては多ボタンが良いと言うに違いない。Mac OS X 以降は、PC 用の USB マウスをつなげば右クリックもフツーに使えるから。ただ、この Apple Pro Mouse はクセになる。一度、こいつをクリックしてしまうと、他のマウスをクリックするのがイヤになる。

手に吸い付くように思える材質、クリック時にマウス全体が沈み込む感覚、独特の深いクリック音。どれをとっても、他のマウスでは決して味わえない体験ばかりだ。いくら言葉を連ねても、使ったことにない人には伝わらないだろうけどね。

Apple の販売するマウスが他のタイプに変わってしまってからは、これが壊れたらどうしようと不安になった。もう他のマウスに戻るつもりはなかったからだ。白でも良かったから買っておくべきだったと悔やんだ。

結局、このお気に入りのマウスは壊れることなくその役割を終えた。光学式のマウスは可動部分がないから故障の確率は低いのだろう。また、このマウスの材質は汚れをキレイに拭き取れるので劣化が目立たない。これも長く使えた要因だろう。

いくら消耗品といっても、マウスやキーボードは頻繁に交換が必要になる物ではない。けれど、だからこそ補充品を買っておくべきなのだ。安心していると同じ物が手に入らなくなるよ。

Realforce 86U の使用感

このキーボードを使い始めて 10 ヶ月。最初の頃に感じた不満はいまも解消されていない。打鍵音はやはり安っぽく聞こえるし、不要なキーもずっと不要なキーのままだ。

ただ、現行の Apple 製キーボードに戻ろうとも思わない。その理由はストロークの深さにある。キーを叩いたときにしっかりと沈み込む感覚はやはり良い。現行の Apple 製キーボードのようにストロークの浅いものは、キーを「叩く」というより「押す」感じが強い。キーというよりもボタンに思える。

他に、気になるキーボードもあるんだけどね(↓)。これのベースになっているという「Apple Extended Keyboard」は使ったことがないんだよ。叩いていて気持ちの良いキーボードだったのかな (・д・)?

テンキーが余分。それがなければ飛び付いていたかも。

ノート型の場合はどうする?

ノート型のパソコンではメーカーに修理を依頼しない限り、キーボードを交換することは難しい。メーカーによっては、(個人ユーザでも)部品としてキーボードを発注できることもあるけれど、交換にかかる手間はケーブルの抜き差しだけですむデスクトップ型とは比べ物にならない。

(昔とくらべれば)本体の価格が下がった現在、ユーザは数年使った後でキーボードが壊れたとして修理に出すだろうか? 修理の問い合わせをして「修理費は○万円になります」と言われたら、修理をためらうに違いない。新しい製品を買う方が良いと思うんじゃないか?

関連リンク

関連記事

twitter より (2010-09-21)

  • 08:32  英語を忘れそうなのでメモ。proof of concept 。プロトタイプの前段階として位置付けられているようだ。→ http://ja.wikipedia.org/wiki/概念実証
  • 08:34  ふむふむ。proof of concept は proof of technology や pilot project とは別のプロセスなのね。この辺りの言葉はきちんと定義して使わないと誤解のもとだね。→ http://ja.wikipedia.org/wiki/概念実証
  • 22:22  ドルだての買い物をするときには円高がうれしいね。RTM の pro アカウントを更新したんだが、$25 ≒ ¥2,200.- だった。
Powered by twtr2src.