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 ret
getHTTP
#!/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 ret
extract
#!/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 ret
printEach
#!/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