オブジェクトを次々に渡す「Ruby Filter」ってどうだろう
2007.10.15
Rubyに慣れようと、コマンドライン・ツールなどを作ってみることにしたのだが、すでにUnixに存在しているgrepなどを作っても仕方がない。そこで、指定したブログのURLからHTMLページをHTTP GETで取得し、それをパースしてATOMやRSSフィードのURLを見つけて、それをさらにHTTP GETで取得してタイトルだけ表示する、というツールを作ってみることにした。
できるだけRubyらしい作り方をしようと思いついたのが「Ruby Filter」。Unixのフィルターのようにそれぞれは単一の機能を持ったプログラムをパイプでつなげて複雑なことをさせる。ただし、フィルターからフィルターに渡すものは単なるテキストではなく、オブジェクトのテキスト表現だ(次のフィルターはそのテキストをevalしてから入力として利用する)。
上のブログのURLからRSSフィードを取り出すケースだと、
parseURI | getHTTP | parseHTML | extract rss10 | parseURI | getHTTP | parseRSS | eachPrint title
という組み合わせで実現できる。
このブログのURL "http://satoshi.blogs.com/" を食わせた場合の処理は以下の通りになる。
ステップ1. parseURI
入力: "http://satoshi.blogs.com/"
出力: { :port=>80, :path=>"/", :scheme=>"http:, :host=>"satoshi.blogs.com" }
ステップ2. getHTTP
入力: { :port=>80, :path=>"/", :scheme=>"http:, :host=>"satoshi.blogs.com" }
出力: "<!DOCTYPE html ...(中略)...</html>"
ステップ3. parseHTML
入力: "<!DOCTYPE html ...(中略)...</html>"
出力: {:rss10=>"http://satoshi.blogs.com/life/index.rdf", rss20=>"http://satoshi.blogs.com/life/rss.xml", :atom=>"http://satoshi.blogs.com/life/atom.xml", :title=>"Life is beautiful"}
ステップ4. extract rss10 (注:パラメータが必要)
入力: {:rss10=>"http://satoshi.blogs.com/life/index.rdf", rss20=>"http://satoshi.blogs.com/life/rss.xml", :atom=>"http://satoshi.blogs.com/life/atom.xml", :title=>"Life is beautiful"}
出力: "http://satoshi.blogs.com/life/index.rdf"
ステップ5. parseURI
入力: "http://satoshi.blogs.com/life/index.rdf"
出力: { :port=>80, :path=>"/life/index.rdf", :scheme=>"http:, :host=>"satoshi.blogs.com" }
ステップ6. getHTTP
入力: { :port=>80, :path=>"/life/index.rdf", :scheme=>"http:, :host=>"satoshi.blogs.com" }
出力: "<?xml version=...(中略)...</rdf:RDF>"
ステップ7. parseRSS
入力: "<?xml version=...(中略)...</rdf:RDF>"
出力: [{:url=>"http://satoshi...", :title=>"..."}, {:url=>"...", :title=>"..."}, ...]
ステップ8. printEach title (注:これだけは出力が人間向け)
入力: [{:url=>"http://satoshi...", :title=>"..."}, {:url=>"...", :title=>"..."}, ...]
出力: (人間向けの)タイトル一覧
結構面白いと思うんだがどうだろう。
参考までにソースコードを乗せておく(注:Ruby初心者なので、あまり良いコードではないかも知れない)。
parseURI#!/usr/bin/ruby
require 'uri'
require 'filter'
Filter.evalStdin
uri = URI.parse($val)
ret = {:scheme=>uri.scheme, :host=>uri.host, :port=>uri.port, :path=>uri.path}
p retgetHTTP
#!/usr/bin/ruby
require 'net/http'
require 'filter'
Filter.evalStdin
Net::HTTP.version_1_2 # omajinai
Net::HTTP.start($val[:host], $val[:port]) { |http|
ret = http.get($val[:path])
p ret.body
}parseHTML
#!/usr/bin/ruby
require 'filter'
Filter.evalStdin
ret = { :title=>nil, :rss10=>nil, :rss20=>nil, :atom=>nil }
ret[:title]=$1 if /<title>(.*)<¥/title>/ =~ $val
ret[:rss20]=$1 if /<link.*title="RSS 2.0".*href="(.*)".*¥/>/ =~ $val
ret[:rss10]=$1 if /<link.*title="RSS".*href="(.*)".*¥/>/ =~ $val
ret[:rss10]=$1 if /<link.*title="RSS 1.0".*href="(.*)".*¥/>/ =~ $val
ret[:atom]=$1 if /<link.*title="Atom".*href="(.*)".*¥/>/ =~ $val
p retextract
#!/usr/bin/ruby
require 'filter'
key=ARGV.shift
Filter.evalStdin
p $val[key.to_sym]parseRSS
#!/usr/bin/ruby
require 'rexml/document'
require 'filter'
Filter.evalStdin
ret=[]
doc = REXML::Document.new $val
doc.elements.each('//item') { |e|
url=e.attributes['about']
item = { :url=>url, :title=>nil }
e.each_element('title') { |ee| item[:title]=ee.text }
ret.push(item)
}
p retprintEach
#!/usr/bin/ruby
require 'filter'
key=ARGV.shift.to_sym
Filter.evalStdin
$val.each { |e| printf "%s¥n", e[key] }filter.rb
class Filter
def self.evalStdin
input = "$val="
until $stdin.eof?
input += gets
end
eval(input)
end
end
おもしろいです
オブジェクトをxmlで表現すれば
複数言語間でつかえますね
Posted by: nati | 2007.10.16 at 04:13
わざわざグローバル変数を使わずともいいと思うのですが、どうでしょうか。
あと、
eval("p $val[:" + key + "]");
のところは
p $val[key.to_sym]
でOKです。
Posted by: ujihisa | 2007.10.16 at 08:48
>わざわざグローバル変数を使わずともいいと思うのですが、どうでしょうか。
最初は$_を使おうと思ったのですがうまくできなくて、$valにしました。ローカル変数でもスコープの問題さえなければかまいませんね、確かに。
>p $val[key.to_sym]でOKです。
ご指摘ありがとうございます。修正します。
Posted by: satoshi | 2007.10.16 at 10:06
考え方としては、Microsoft の Windows PowerShell ととても似ていますね。
Posted by: anonymous | 2007.10.16 at 11:53
面白いです。
ややこしさとのトレードオフですがprint用のコマンドとprintの文字列出力部分を分離しておくと処理の途中と最終を出力するってこともできますね。普通のフィルターは人間の向けの文字列が渡されればそれを後に順々に渡すだけってことにしておいて。
例えばappendPrintは引数があればその変数を「人間の向けの文字列」に追加、引数がなければ標準入力を全て「人間の向けの文字列」に追加って感じにしすると。
parseURI | getHTTP | appendPrint port | parseHTML | extract rss10 | parseURI | getHTTP | parseRSS | eachPrint title | appendPrint | printToUser
って感じで最初のport番号と結果を出力することができます。
Posted by: name-3333 | 2007.11.08 at 08:27