Rubyチュートリアル 〜英文小説の最頻出ワードを見つけよう!(その14)
Version20
ここまで来たらもう一歩
小説データは元々ネットにあるんですから
いちいちファイルにダウンロードしないで
直接ネットから取れたらうれしいです
open-uriライブラリというのを使うと
httpに簡単にアクセスできるようになります
require "open-uri" class WordDictionary private def input_to_string(input) case input when /^http/ begin open(input) { |f| return f.read } rescue Exception => e puts e exit end when String begin File.open(input, "r") { |f| return f.read } rescue puts "Argument has assumed as a text string" input end when ARGF.class input.read else raise "Wrong argument. ARGF, file or string are acceptable." end end end wdic = WordDictionary.new('http://www.gutenberg.org/files/245/245.txt') p wdic.top_by_length(30) #> [["inconsequentialities", 1, 20], ["straightforwardly", 1, 17], ["unenforceability", 1, 16], ["acquaintanceship", 3, 16], ["reproachlessness", 1, 16], ["misunderstanding", 1, 16], ["stenographically", 1, 16], ["preposterousness", 1, 16], ["responsibilities", 1, 16], ["incomprehensible", 1, 16], ["charlottesville", 1, 15], ["acknowledgments", 1, 15], ["unrighteousness", 2, 15], ["multitudinously", 1, 15], ["unphilosophical", 1, 15], ["impossibilities", 2, 15], ["inconspicuously", 1, 15], ["inconsequential", 2, 15], ["conscientiously", 1, 15], ["notwithstanding", 3, 15], ["merchantibility", 1, 15], ["architecturally", 2, 15], ["daguerreotypist", 1, 15], ["representations", 1, 15], ["unhandkerchiefs", 1, 15], ["correspondingly", 3, 15], ["picturesqueness", 2, 15], ["proportionately", 1, 15], ["unconsciousness", 1, 15], ["exemplification", 1, 15]]
open-uriライブラリをrequireして
input_to_stringに新しい分岐条件を加えます
ネットアクセスがうまくいかない場合は
エラーメッセージを表示してスクリプトの実行を終了します
これで一層便利になりました
Version21
もう少し実用的なメソッドも追加しましょう
オブジェクトを読みやすいかたちで出力するto_sメソッドと
オブジェクトの部分オブジェクトを返すselectメソッドを定義します
class WordDictionary def to_s @freq_dic.to_s end def select(regexp) text = @words.select { |key, val| key =~ regexp }.join(" ") WordDictionary.new(text) end end
次の例はselectメソッドにより
先頭がxyzの何れかで始まる語の集合からなる
新しいWordDictionaryオブジェクトを生成し
これをto_sメソッドで出力しています
wdic = WordDictionary.new(ARGF) puts xyz_dic = wdic.select(/^[xyz]/) p xyz_dic.top_by_length(5) #> {"you"=>2071, "yes"=>90, "zealand"=>1, "your"=>597, "yourself"=>60, "yesterday"=>18, "yet"=>163, "young"=>144, "yer"=>4, "ye"=>90, "yelp"=>1, "youth"=>17, "yawned"=>3, "zigzag"=>1, "yours"=>26, "yards"=>2, "year"=>124, "yawning"=>3, "x"=>2, "yelled"=>1, "xi"=>1, "xii"=>3, "yard"=>1, "years"=>226, "zip"=>3, "youngest"=>15, "younger"=>30, "yielding"=>4, "yield"=>8, "yawn"=>2, "york"=>13, "yourselves"=>5, "younge"=>4, "youths"=>1, "yielded"=>5, "yale"=>4, "zeph"=>3, "zephaniah"=>1, "zech"=>2, "zion"=>4, "zealots"=>3, "zinzendorf"=>6, "xxxiii"=>1, "xxv"=>3, "xxvi"=>1, "y"=>8, "zama"=>1, "zealous"=>2, "xiii"=>8, "yea"=>6, "zinzendorfs"=>1, "xenophon"=>3, "youthful"=>1, "yearly"=>2, "xxix"=>1, "xh"=>1, "zoroaster"=>2, "xciii"=>1, "zeal"=>2, "zambezi"=>1, "xerxes"=>11, "xv"=>1, "yellow"=>1, "xxiii"=>1} [["zinzendorfs", 1, 11], ["zinzendorf", 6, 10], ["yourselves", 5, 10], ["zoroaster", 2, 9], ["zephaniah", 1, 9]]
さてずいぶんと長い道のりを来ました
スクリプトは一時僅か3行にまで短くできたのに
現在80行を超えるまでに肥大化しました
ワードエコではありません
ここで最初のコードと3行のコードと
現在のコードとを見比べてみましょうか
Version01
dic = Hash.new(0) while line = ARGF.gets line.downcase! while line.sub!(/[a-z]+/, "") word = $& dic[word] += 1 end end p dic.sort { |a, b| b[1] <=> a[1] }[0...30]
Version03
WORDS = ARGF.read.downcase.scan(/[a-z]+/) DICTIONARY = WORDS.inject(Hash.new(0)) { |dic, word| dic[word] += 1 ; dic } p DICTIONARY.sort { |a, b| b[1] <=> a[1] }[0...30]
Version21
require 'open-uri' module Enumerable def take_by(nth) sort_by { |elem| yield elem }.take(nth) end end class WordDictionary include Enumerable def initialize(input) input = input_to_string(input) @words = input.downcase.scan(/[a-z]+/) @freq_dic = @words.inject(Hash.new(0)) { |dic, word| dic[word] += 1 ; dic } end def each @freq_dic.each { |elem| yield elem } end def top_by_frequency(nth, &blk) take_by_value(nth, lambda { |v| -v }, &blk) end def bottom_by_frequency(nth, &blk) take_by_value(nth, lambda { |v| v }, &blk) end def top_by_length(nth, &blk) list = take_by_key(nth, lambda { |key| -key.length }, &blk) list.map { |word, freq| [word, freq, word.length] } end def to_s @freq_dic.to_s end def select(regexp) text = @words.select { |key, val| key =~ regexp }.join(" ") WordDictionary.new(text) end private def input_to_string(input) case input when /^http/ begin open(input) { |f| return f.read } rescue Exception => e puts e exit end when String begin File.open(input, "r") { |f| return f.read } rescue puts "Argument has assumed as a text string." input end when ARGF.class input.read else raise "Wrong argument. ARGF, file or string are acceptable." end end def take_by_value(nth, sort_opt, &blk) val = lambda { |key, val| val } take_by_key_or_val(nth, sort_opt, val, &blk) end def take_by_key(nth, sort_opt, &blk) key = lambda { |key, val| key } take_by_key_or_val(nth, sort_opt, key, &blk) end def take_by_key_or_val(nth, sort_opt, by) @freq_dic.select { |key, val| block_given? ? yield(val) : val }.take_by(nth) { |key, val| sort_opt[by[key, val]] } end end wdic = WordDictionary.new(ARGF) p wdic.top_by_frequency(20)
確かにスクリプトは肥大化しています
果たして今までの労力は無駄だったんでしょうか
ワードエコでなくなった分
よくなったことがあるんでしょうか
はいあります
それは単語辞書が
単なる制御構造からオブジェクトになったことです
オブジェクトになった利点の1つは
コードがポータブルになるということです
つまりそれが持つデータを維持しながら
他のオブジェクトに送って相互作用させたり
データベースに保存したりできます
同時に内容の異なる複数の辞書オブジェクトを生成し
これらを相互に連携して結果を得る(内容の比較とか)
といったこともできるようになります
これらはネットワーク越しであってもかまいません
他の利点は機能の追加が容易になる点です
クラスにメソッドを追加することで
単語辞書を対象にした新たな機能が容易に追加できます
既にいくつかの機能追加を見てきました
最初のヴァージョンのスクリプトに
機能を追加することを想像頂ければ
この利点は明らかでしょう
このようにオブジェクトは
機能追加のフレームワークになっているのです
今までの苦労も
未来に対する投資というかたちで報われそうです
そろそろ幕を閉じるときが来たようです
(次回に続く)