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 つだから、最終結果が複数のファイルになることはない(将来のリクエストを見越してあらかじめ他のページを生成しておくなら別だが)。ウェブアプリ版としては、最終結果を標準出力に書き出す前回のものの方が完成した姿に近いと言える。

関連記事

0 件のコメント:

コメントを投稿