Rubyで内部DSLっぽいのやってみる
少し前に仕事中、一瞬の隙も見逃さずに情報収集できるRubyワンライナーとスクリプトというRubyスクリプトを書いたのだが、スクレイピング対象サイトをハッシュで持ってるあたりが使いづらい。良い機会なので内部DSLってのに挑戦してみた。
しかし、内部DSLについてざっと調べたものの、まだ租借しきれていない。とりあえず、Sinatraっぽい構文でスクレイピング対象サイトを指定できるようにした。
sites.rb(ユーザー側)
fetch 'yahoo' do url 'http://www.yahoo.co.jp' regexp /topics.+?>([^<]+?)</ end fetch 'hatena' do url 'http://b.hatena.ne.jp/hotentry' regexp /entry-link.+>(.+?)</ end fetch 'naver' do url 'http://matome.naver.jp/' regexp /matomename.+?-->(.+?)<!--/m end fetch '2ch' do url 'http://uni.2ch.net/newsplus/' regexp /<a.+?>\d+?:\s(.+?)</ end
news(定義側)
#!/usr/bin/env ruby require 'open-uri' require 'nkf' class Site attr_accessor :name, :url, :regexp def initialize(n) @name = n end def url(u) @url = u end def regexp(r) @regexp = r end def get open(@url) do |f| f.read.scan(@regexp) {|m| puts NKF.nkf('-w', m.join)} end end end @defined_sites = [] def fetch(name, &block) s = Site.new(name) s.instance_eval &block @defined_sites << s end load File.expand_path('../sites.rb', __FILE__) defined_site_names = @defined_sites.map(&:name) site_names = ARGV.empty? ? defined_site_names : ARGV sites = [] site_names.each do |site_name| raise 'not defined' unless defined_site_names.include? site_name @defined_sites.map {|s| sites << s if s.name === site_name} end sites.map(&:get)
実行
上記2ファイルを同じディレクトリに配置し、newsに実行権限を与え、パスを通せば前回と同様に動く。
Rubyワンライナー入門
先日仕事中、一瞬の隙も見逃さずに情報収集できるRubyワンライナーとスクリプトというネタエントリを書いたのだが、その際Rubyのワンライナーをもう少しまともに理解したいと思ったので入門してみた。手元のRubyは1.9.2
はじめに
Rubyのワンライナーは、rubyコマンドに色々なオプション(-eや-nなど)をつけて実行する。各オプションの網羅的な解説が見たければ、コマンドラインでは$ man rubyで閲覧できるし、webではるりまサーチで参照できる。ただ、これらは網羅的すぎるので、以下よく使うオプションとその周辺情報についてまとめた。
-n
プログラム全体がwhile gets ... endというループで囲まれているように動作する。ちょっとややこしい。
例: hoge.txtとfuge.txtの内容を表示
$ ruby -ne 'puts $_' hoge.txt fuge.txt
理解するには
Kernel.#gets、ARGF、$_について理解する必要がある
Kernel.#gets
ARGFから一行読み込んで、それを返します。...読み込んだ文字列は組み込み変数 $_ にもセットされます。...
http://rurema.clear-code.com/1.9.2/method/Kernel/m/gets.html
つまり、以下は同義
$ ruby -ne 'puts $_' hoge.txt fuge.txt
$ ruby -e 'while gets; puts $_; end' hoge.txt fuge.txt
ざっくりいうと、以下のような流れになる
hoge.txtとfuge.txtを連結した仮想ファイル(ARGF)をセット
↓
ARGFから1行読み込み、$_にセット。読み込めた場合は$_を出力してループ
ちなみに
上記では出力の部分をputs $_としているが、これはKernel.#printで代替できる。
Kernel.#print
引数を順に標準出力 $stdout に出力します。引数が与えられない時には変数 $_ の値を出力します。...
http://rurema.clear-code.com/1.9.2/method/Kernel/m/print.html
つまり、以下は全て同義
$ ruby -ne 'print' hoge.txt fuge.txt
$ ruby -ne 'puts $_' hoge.txt fuge.txt
$ ruby -e 'while gets; puts $_; end' hoge.txt fuge.txt
-p
nオプションとほぼ同じ。nオプションと同じくプログラム全体をwhile gets...endループするが、さらに各ループの最後にprintする。つまり、printが省略できるということ。なので、以下は全て同義。
$ ruby -pe '' hoge.txt fuge.txt
$ ruby -ne 'print' hoge.txt fuge.txt
$ ruby -ne 'puts $_' hoge.txt fuge.txt
$ ruby -e 'while gets; puts $_; end' hoge.txt fuge.txt
$_を破壊的メソッドで変換して出力するような場合に便利
例: hoge.txtとfuge.txtの内容を全て大文字にして表示
$ ruby -pe '$_.upcase!' hoge.txt fuge.txt
-a
オートスプリット。nかpと一緒に使う。各ループの先頭で$F = $_.splitをを実行する。
例: カレントディレクトリの情報を配列に変換して表示
$ ls -laF | ruby -ane 'p $F'
-F
$;(入力フィールドセパレータ。splitのデフォルト区切り文字)を設定
例: 環境変数PATHを:で分割して改行表示
$ echo $PATH | ruby -F: -ane 'puts $F'
-r
ライブラリ読み込み。
例: yahooのhtmlを取得
$ ruby -r open-uri -e 'open("http://www.yahoo.co.jp") {|f| puts f.read}'
ちなみに、複数のライブラリを読み込みたい場合は-r を複数書く。
例: facebookのapiからマークザッカーバーグのJSONを取得しハッシュに変換
$ ruby -r open-uri -r json -e 'open("http://graph.facebook.com/zuck") {|f| p JSON.parse(f.read)}'
BEGIN, END
オプションでは無いのだが、普段あまり使わないRubyの構文BEGINとEND(begin...endとは全くの別物)がワンライナーだと結構使われるようだ。
例: 全行が整数のnumber.txtの合計値を表示
$ ruby -ne 'BEGIN{$sum = 0}; $sum += $_.to_i; END{puts $sum}' number.txt
BEGINとENDをつけないと、毎行読み込むごとに$sum = 0; と puts $sumが実行されてしまうので意図した結果にならない
BEGIN
初期化ルーチンを登録します。BEGINブロックで指定した文は当該ファイルのどの文が実行されるより前に実行されます。...
http://doc.ruby-lang.org/ja/1.9.2/doc/spec=2fcontrol.html#BEGIN
END
「後始末」ルーチンを登録します。END ブロックで指定した文はインタプリタが終了する時に実行されます。Ruby の終了時処理について詳しくは 終了処理を参照してください。...
http://doc.ruby-lang.org/ja/1.9.2/doc/spec=2fcontrol.html#END
仕事中、一瞬の隙も見逃さずに情報収集できるRubyワンライナーとスクリプト
情報収集はビジネスマンとしての基本である。しかし普段シェルで作業する者としては、毎回ブラウザを立ち上げる時間すら不毛である。よって、シェルから一瞬で情報収集できるRubyのワンライナーを書いた。
上からYahoo, はてな, Naverまとめ, 2ちゃん
$ ruby -r open-uri -e 'open("http://www.yahoo.co.jp").read.scan(/topics.+?>([^<]+?)</) {|m| puts m}'
$ ruby -r open-uri -e 'open("http://b.hatena.ne.jp/hotentry").read.scan(/entry-link.+>(.+?)</) {|m| puts m}'
$ ruby -r open-uri -e 'open("http://matome.naver.jp/").read.scan(%r{matomename.+?-->(.+?)<!-}m) {|m| puts m}'
$ ruby -r open-uri -e 'open("http://uni.2ch.net/newsplus/").read.scan(/<a.+?>\d+?:\s(.+?)</) {|m| puts m}' | nkf -w
あれ?
普通にブラウザ開いた方が早いw
なので
コマンド一発で情報収集できるスクリプトを書いた
news
#!/usr/bin/env ruby require 'open-uri' require 'nkf' defined_sites = { 'yahoo' => { 'url' => 'http://www.yahoo.co.jp', 'regexp' => /topics.+?>([^<]+?)</ }, 'hatena' => { 'url' => 'http://b.hatena.ne.jp/hotentry', 'regexp' => /entry-link.+>(.+?)</ }, 'naver' => { 'url' => 'http://matome.naver.jp/', 'regexp' => /matomename.+?-->(.+?)<!--/m }, '2ch' => { 'url' => 'http://uni.2ch.net/newsplus/', 'regexp' => /<a.+?>\d+?:\s(.+?)</ } } class Site attr_accessor :url, :regexp def initialize yield self end def fetch open(@url) do |f| f.read.scan(@regexp) {|m| puts NKF.nkf('-w', m.join)} end end end site_names = ARGV.empty? ? defined_sites.keys : ARGV sites = [] site_names.each do |site_name| raise 'not defined' unless defined_sites.keys.include? site_name sites << Site.new do |s| s.url = defined_sites[site_name]['url'] s.regexp = defined_sites[site_name]['regexp'] end end sites.map(&:fetch)
こいつに実行権限を与えて、pathを通して、newsコマンド
$ news 東電社長 時効主張せずと明言 鳩山元首相、招かれ訪中へ 高2自殺 前日に平手30-40発? JTB 企業の体力測定事業参入 魔女狩りで禁固20年 ネパール 本田にミランがオファー準備 安藤美姫がトヨタ自動車退社 金爆・鬼龍院のNSC芸人時代 今日の話題(27件) 一番福へ全速力 ...
キタ!
yahooとhatenaだけ表示したければ
$ news yahoo hatena
対象サイトを追加したければ、ハッシュ(defined_sites )に追加すればOK!
これで一瞬の隙も見逃さずに情報収集する「できるビジネスマン」になれる!
※注 ネタです
※追記 Rubyワンライナーについてまとめた
Rubyワンライナー入門
RackとERBを使って、超シンプルなRubyのWebアプリを作る
Railsで使われているRackとERB。いつもRailsにお任せだから、たまにはRailsの皮をはいで生で触ってみたい!ということで、超基本的な部分(通常表示, リダイレクト, not found)だけいじったのでメモ。
・/にアクセスすると、/hogeにリダイレクトされる
・/hogeは変数nameとsexに格納した値をerbで表示する
・それ以外のURLにアクセスされたら404エラーを返す
という代物を作る
config.ru
#coding: utf-8 run lambda {|env| request = Rack::Request.new(env) case request.path when '/' Rack::Response.new {|r| r.redirect("/hoge")} when '/hoge' name, sex = 'maeharin', 'man' html = ERB.new(<<-EOF).result(binding) <html> <head><meta charset="utf-8"></head> <body> 私の名前は<%= name %>。性別は<%= sex %>です。 </body> </html> EOF Rack::Response.new(html) else Rack::Response.new("not found", 404) end }
サーバー起動
config.ruがあるディレクトリで
$ rackup
localhost:9292にアクセス
動いた!シンプルで素敵すなあ。色々いじってみよう。
※RackとERBの詳細は以下の記事などを参考。
第23回 Rackとは何か(1)Rackの生まれた背景:Ruby Freaks Lounge|gihyo.jp … 技術評論社
Rubyist Magazine - 標準添付ライブラリ紹介 【第 10 回】 ERB
Rubyの多重代入におけるto_aとto_aryの挙動
先日のエントリでRubyの多重代入についてふれたのだが、教科書によってto_aryが呼ばれると書いてあったりto_aが呼ばれると書いてあったりで、なにが本当なのか分からなくなった。
回答らしきもの
調べてみたら、こちらに回答らしきものがあった。
OK, to_a means an explicit array conversion, so it should not be used
for implicit conversion a in "*y = x". On the other hand, I consider
"*x" as a form of explicit conversion, i.e. shorthand for "*(x.to_a)",
so that the current 1.9 behavior is intentional.
matz.
どうやら、Ruby1.9においては、暗黙的なコンテキストではto_aryが呼ばれ、明示的なコンテキストではto_aが呼ばれるようだ。
1.9.2で試してみた
class C def to_ary; [1,2]; end def to_a; [3,4]; end end c = C.new a, b = c # 暗黙的 d, e = *c # 明示的 puts a, b # 1,2 puts d, e # 3,4
確かに暗黙的なコンテキスト(a,b = c)ではto_aryが呼ばれており、明示的なコンテキスト(d,e = *c)ではto_aが呼ばれていることが分かる。ただ、これはto_aryとto_aの両方が定義されている場合の挙動であり、片方の場合には挙動が違った。上記においてto_aryのみを定義した場合、明示的コンテキストにおいてもto_aryが呼ばれる(to_aの代わりに)。しかしto_aのみを定義した場合は、暗黙的コンテキストにおいては通常の多重代入が行われる(to_aryの代わりにto_aが呼ばれることはない)。結構ややこしい。
1.8.7でも試してみた。
class C def to_ary; [1,2]; end def to_a; [3,4]; end end c = C.new a, b = c # 暗黙的 d, e = *c # 明示的 puts a, b # 1,2 puts d, e # 1,2
さらにややこしいことに、1.8.7の場合、1.9.2とは挙動が違った。to_aryとto_a両方が定義されていると、暗黙的でも明示的でもto_aryが呼ばれる。片方だけ定義された場合でいうと、to_aryのみが定義されている場合、暗黙的でも明示的でもto_aryが呼ばれる。一方to_aのみが定義されている場合は、暗黙的コンテキストでは通常の多重代入が行われ、明示的コンテキストにおいてはto_aが呼ばれる。
まとめ
すごく混乱したので、まとめておく。あまりこの違いで悩むことは無いだろうけど。。。
Ruby バージョン | 暗黙的コンテキスト | 明示的コンテキスト |
---|---|---|
1.9.2 | to_ary | to_a(なければto_ary) |
1.8.7 | to_ary | to_ary(なければto_a) |
Rack::Responseオブジェクトを多重代入すると、暗黙的にfinishが呼ばれる
環境:ruby-1.9.2、rack-1.4.1
RAILSCASTのRackの章を見ていたら、以下のような構文が出てきて、不思議に思った。
def call(env) Rack::Response.new("hoge") end
Rackアプリの呼び出し側(例えば、lintミドルウェア)では、このように書かれている
status, headers, @body = @app.call(env)
上記の場合、@app.call(env)を評価した結果はRack::Responseオブジェクトであるはず。なのに、それを多重代入できるのはなぜか?配列じゃないといけないのでは?
多重代入の仕様が怪しいので、調べる。
4.5.5.3 複数の左辺、1 個の配列の右辺
左辺は複数あるが、右辺は 1 つしかない場合、Ruby はその右辺を代入すべき値のリストに展開しようとす
る。右辺が配列なら、Rubyは配列を展開し、個々の要素が独自の右辺になるようにする。右辺が配列でなくても、to_aryメソッドを実装する場合は、to_aryを呼び出して配列を作ってから、その配列を展開して代入する。
どうやら、上記のような状況下で多重代入を行うと、右辺のオブジェクトにto_aryメソッドがある場合、その実行が行われるようだ。ということは、おそらくRack::Responseクラスのインスタンスメソッドにto_aryが定義されているはず。Rackのソースコードを見てみる。
# rack-1.4.1/lib/rack/response.rb def finish(&block) @block = block if [204, 205, 304].include?(status.to_i) header.delete "Content-Type" header.delete "Content-Length" [status.to_i, header, []] else [status.to_i, header, self] end end alias to_a finish # For *response alias to_ary finish # For implicit-splat on Ruby 1.9.2
to_aryメソッドはfinishのエイリアスになっていた!なので、Rack::Responseオブジェクトが暗黙的に多重代入されると、finishメソッドの実行結果が代入されるということになる。なるほどー!
※追記
Rubyの多重代入におけるto_aとto_aryの挙動について書いた
RubyのFile.expand_path('相対パス', __FILE__)の意味
RailsなどのRubyライブラリのソースコードを見ていると、よく
File.expand_path('相対パス', __FILE__)
という一文を目にする。ちょっと調べてみた。
File.expand_pathとは
riコマンドで調べてみる
$ ri File.expand_path (from ruby core) ------------------------------------------------------------------------------ File.expand_path(file_name [, dir_string] ) -> abs_file_name ------------------------------------------------------------------------------ Converts a pathname to an absolute pathname. Relative paths are referenced from the current working directory of the process unless dir_string is given, in which case it will be used as the starting point. The given pathname may start with a ``~'', which expands to the process owner's home directory (the environment variable HOME must be set correctly). ``~user'' expands to the named user's home directory. File.expand_path("~oracle/bin") #=> "/home/oracle/bin" File.expand_path("../../bin", "/tmp/x") #=> "/bin"
・相対パスを絶対パスに変換した文字列を返す
・第2引数を指定しない場合「プロセスのカレントワーキングディレクトリ」を相対パスの基準にする
・第2引数を指定した場合「第2引数で指定したディレクトリ」を相対パスの基準にする
検証用ディレクトリ構造
検証用に、以下のディレクトリを作った。a.rbからlib/を参照することを想定して、検証する。
/ └── Users/ └── hidenorimaehara/ └── xxx/ └── yyy/ └── a.rb └── lib/
File.expand_path('./lib')
まずは第2引数を指定しないでやってみる。
a.rb
プロセスのカレントワーキングディレクトリを確認するため、Dir.pwdの結果も表示することにする。
puts Dir.pwd puts File.expand_path('./lib')
yyyディレクトリで実行
$ ruby a.rb /Users/hidenorimaehara/xxx/yyy /Users/hidenorimaehara/xxx/yyy/lib
File.expand_path('./lib', '/Users/hidenorimaehara/xxx/yyy/')
次に第2引数を与えてみる。yyyディレクトリの絶対パスを第2引数に指定してみる。
a.rb
さきほどのa.rbを以下のように変更。
puts Dir.pwd puts File.expand_path('./lib', '/Users/hidenorimaehara/xxx/yyy/')
yyyディレクトリで実行
$ ruby a.rb /Users/hidenorimaehara/xxx/yyy /Users/hidenorimaehara/xxx/yyy/lib
xxxディレクトリで実行
$ ruby ./yyy/a.rb /Users/hidenorimaehara/xxx /Users/hidenorimaehara/xxx/yyy/lib
第2引数に指定したディレクトリを基準として相対パスを解析していることがわかる。
__FILE__
ここで一旦脇道にそれて、__FILE__について調べる。__FILE__は、現在のソースファイル名を返す変数。実際にどのような値を返しているのか調べてみる。
a.rb
puts __FILE__
yyyディレクトリで
$ ruby a.rb a.rb
File.expand_path('./lib', __FILE__)
となるとFile.expand_pathの第2引数に__FILE__を指定すれば、現在のソースファイルを基点とできる。第1引数に'./lib'、第2引数に__FILE__を指定してみる。
a.rb
puts Dir.pwd puts File.expand_path('./lib', __FILE__)
yyyディレクトリで
$ ruby a.rb /Users/hidenorimaehara/xxx/yyy /Users/hidenorimaehara/xxx/yyy/a.rb/lib
xxxディレクトリで
$ ruby ./yyy/a.rb /Users/hidenorimaehara/xxx /Users/hidenorimaehara/xxx/yyy/a.rb/lib
ありゃ
./では、a.rb(ファイル名)自体もパスに入ってしまっている。
File.expand_path('../lib', __FILE__)
ということで、第1引数に../libにしてみる。
a.rb
puts Dir.pwd puts File.expand_path('../lib', __FILE__)
yyyディレクトリで
$ ruby a.rb /Users/hidenorimaehara/xxx/yyy /Users/hidenorimaehara/xxx/yyy/lib
xxxディレクトリで
$ ruby ./yyy/a.rb /Users/hidenorimaehara/xxx /Users/hidenorimaehara/xxx/yyy/lib
できた!
以下のように使う
こうすれば、プロセスのカレントワーキングディレクトリがどこであっても、相対的な位置関係を参照できる
# このソースファイルと同ディレクトリにあるb.rbをrequire require File.expand_path('../b', __FILE__) # このソースファイルと同ディレクトリにあるlibディレクトリをrequireのロードパスに追加 $: << File.expand_path('../lib', __FILE__)