書評:艶っぽさなら「吉原手引草」
教えながら学ぶRuby:「Rangeの積を求める」をやってみた

オブジェクトを次々に渡す「Ruby Filter」ってどうだろう

 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

Comments

nati

おもしろいです
オブジェクトをxmlで表現すれば
複数言語間でつかえますね

ujihisa

わざわざグローバル変数を使わずともいいと思うのですが、どうでしょうか。

あと、
eval("p $val[:" + key + "]");
のところは
p $val[key.to_sym]
でOKです。

satoshi

>わざわざグローバル変数を使わずともいいと思うのですが、どうでしょうか。

 最初は$_を使おうと思ったのですがうまくできなくて、$valにしました。ローカル変数でもスコープの問題さえなければかまいませんね、確かに。

>p $val[key.to_sym]でOKです。

 ご指摘ありがとうございます。修正します。

anonymous

考え方としては、Microsoft の Windows PowerShell ととても似ていますね。

name-3333

面白いです。

ややこしさとのトレードオフですがprint用のコマンドとprintの文字列出力部分を分離しておくと処理の途中と最終を出力するってこともできますね。普通のフィルターは人間の向けの文字列が渡されればそれを後に順々に渡すだけってことにしておいて。

例えばappendPrintは引数があればその変数を「人間の向けの文字列」に追加、引数がなければ標準入力を全て「人間の向けの文字列」に追加って感じにしすると。

parseURI | getHTTP | appendPrint port | parseHTML | extract rss10 | parseURI | getHTTP | parseRSS | eachPrint title | appendPrint | printToUser
って感じで最初のport番号と結果を出力することができます。

Verify your Comment

Previewing your Comment

This is only a preview. Your comment has not yet been posted.

Working...
Your comment could not be posted. Error type:
Your comment has been posted. Post another comment

The letters and numbers you entered did not match the image. Please try again.

As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

Having trouble reading this image? View an alternate.

Working...

Post a comment

Your Information

(Name is required. Email address will not be displayed with the comment.)