「恋するプログラム」のMac向け読み替え その5
その4に続き、Macユーザーが恋するプログラム
を勉強する上で、読み替えや説明が必要だったことをメモして行く。
-
CHAPTER 9-1
現在は使えないGoogle Web APIの導入説明であり、コードは出てこない。 -
CHAPTER 9-2
この章ではサンプル(KOISURU_PROGRAM/sample/google)のうち、新規追加となるgugulu.rb
およびmorph.rb
への機能追加について説明している。gugulu.rb
はGoogle Web APIを用いる部分を書き換えることになる。なお、Google Web APIを用いる部分の書き換えは、こちらを参考にさせてもらった。
-
Ogaの導入
Google Web APIが使えないので、本と同等のことを実現するには、以下の手順が必要になる。- Ruby標準のopen-uriで検索を実行(HTML文書を取得)
- 検索結果(HTML文書)を構文解析
- 解析結果から、タイトルとURLを取得(Webスクレイピング)
RubyでWebスクレイピングを行うライブラリとしてはNokogiriが有名だが、Ruby2.1以上でないと導入できない。macOS標準添付のRuby2.0でも使えるのはOgaである。よってこれをインストールする。
sudo gem install oga
-
gugulu.rb Guguluクラス searchメソッド
Google検索を実行するメソッドである。- searchメソッド
-
class Gugulu GOOGLE = "https://www.google.co.jp" def search(query) # 検索文字列をURLエンコードして取得 search_word = CGI.escape(query) # クエリ文字列に変換(ブラウザのアドレス欄の表記にする) query_url = "#{GOOGLE}/search?q=#{search_word}" # UTF-8(未定義は置き換え)で読み込む search_result = open(query_url).read.encode("utf-8", :undef=>:replace) # 次項参照 get_resultElements(search_result) end (以下省略)
-
gugulu.rb Guguluクラス get_resultElementsメソッド
Google検索結果(HTML)を構文解析し、タイトルとURLを取得するメソッドである。- get_resultElementsメソッド
-
def get_resultElements(html) element = [] # OgaでHTMLをパース doc = Oga.parse_html(html) # Webスクレイピング # 「/」で文書のトップレベル # 「//」で先頭から途中までのパスを省略 # 「h3」と「a」要素(Google検索結果のタイトルとリンク)を取得 doc.xpath('//h3/a').each do |node| # 「a」要素からリンクを取得 link = node.get('href') # Wikipediaは開けないので除外 # SSL_connect returned=1 errno=0 state=SSLv3 read server key exchange B: unable to find ecdh parameters next if link =~ /wikipedia/ # 取得したlinkに「?q=」が含まれる場合、GOOGLEを付加 # 具体的にはリンクが「/url?q=」または「/search?q=」で始まるケース if link =~ /\?q=/ link = "#{GOOGLE}#{link}" end # シンボルをキーとするハッシュにタイトルとリンクを格納 # { :title => node.text, :url => link }と同じ意味 element.push({ title: node.text, url: link }) end return element end
-
gugulu.rb Guguluクラス get_sentencesメソッド
redirection forbiddenエラー対策を導入。また、検索結果を開く時もUTF-8(未定義は置き換え)で読み込む必要がある。- get_sentencesメソッド
-
# 「self::」はクラスメソッドであることを示す def self::get_sentences(uri) tries = 3 # redirection forbiddenエラー対策 begin html = open(uri, redirect: false) rescue OpenURI::HTTPRedirect => redirect uri = redirect.uri # assigned from the "Location" response header retry if (tries -= 1) > 0 raise end # 検索結果を開く時もUTF-8(未定義は置き換え)で読み込む必要がある。 html = html.read.encode("utf-8", :undef=>:replace) return html2sentences(html) end
-
gugulu.rb Guguluクラス html2sentencesメソッド
Google検索結果から選んで開いたURLのHTML文書を、通常の文章に変換するメソッドである。サンプルからの変更はないが、コード理解のためのポイントをメモしておく。
ここでWebスクレイピングが出来ない理由は、取得するHTML文書の形式が不定のためである。- html2sentencesメソッド
-
# 「self::」はクラスメソッドであることを示す def self::html2sentences(html) # HTML中のコメントタグの内容を除去。「.*?」で任意の文字0回以上。「/im」で大文字小文字無視 html.gsub!(/<!--.*?-->/im, '') # HTMLタグを除去 html.gsub!(/<.*?>/im, '') # HTML中の実体参照を元の文字列に置換 html = CGI.unescapeHTML(html) # HTML中のノンブレークスペースをスペースに置換 html.gsub!(/ /, ' ') # HTML中の先頭にある1回以上「+」のスペース、タブ等「\s」、全角スペースを除去 html.gsub!(/^[\s ]+/, '') # HTML中の末尾にある1回以上「+」のスペース、タブ等「\s」、全角スペースを除去 html.gsub!(/[\s ]+$/, '') # 否定先読みfoo(?!bar)はfooのうち直後にbarがないものを示す。 # [。??!!]は「。??!!」のいずれか。「[\r\n]」は改行コード。 # [。??!!](?![\r\n])で、直後が改行コードではない、文末の記号を意味する。 # ([。??!!](?![\r\n]))で、それをグループ化&キャプチャ # 「+」で、それの1回以上の繰り返し。 # (([。??!!](?![\r\n]))+)で、それをグループ化&キャプチャ。 # 「\1」で一番外側の()にマッチした記号を取得し、改行コードを付加している。 html.gsub!(/(([。??!!](?![\r\n]))+)/, "\\1\n") sentences = [] html.split(/\n/).each do |line| # 形態素解析 parts = Morph::analyze(line) # 文かどうかを名詞と記号の数で判定(次項参照) next unless Morph::sentence?(parts) sentences.push(line) end return sentences end
-
morph.rb Morphモジュール sentence?メソッド
開いたURLの内容が見出しか文章かを判定するメソッドである。サンプルからの変更はないが、コード理解のためのポイントをメモしておく。- sentence?メソッド
-
def sentence?(parts) num_noun = 0 # 名詞の数をカウントする変数 num_mark = 0 # 記号の数をカウントする変数 # 形態素解析結果配列[["形態素", "品詞"], ...]から多重代入 parts.each do |w, part| case part # 先頭が名詞であればnum_nounをカウントアップ when /^名詞/ num_noun += 1 # 先頭が # 否定先読みにより、後ろに読点や句点がつかない記号という意味になる # その場合num_markをカウントアップ when /^記号-(?!(読点)|(句点))/ num_mark += 1 end end # parts.size(品詞の数)が # 名詞と記号の数の合計の2倍より小さいかどうかを戻り値とする return parts.size > (num_noun + num_mark) * 2 end
-
gugulu.rb テストコードの変更点
searchメソッドと取得したelementがハッシュになったことによる変更が発生している。- テストコードの変更点
-
--- a/gugulu.rb 2017-01-25 11:58:32.000000000 +0900 +++ b/gugulu.rb 2017-01-25 13:12:07.000000000 +0900 @@ -8,11 +8,10 @@ break if line.empty? begin - result = ggl.search(line, 0, 10) - elements = result.resultElements + elements = ggl.search(line) elements.each_with_index do |elem, i| - puts('%d %s'%[i+1, elem.title]) - puts(' ' + elem.URL) + puts('%d %s'%[i+1, elem[:title]]) + puts(' ' + elem[:url]) end puts @@ -22,7 +21,7 @@ break if line.empty? no = line.to_i - 1 next unless elements[no] - puts(Gugulu::get_sentences(elements[no].URL)) + puts(Gugulu::get_sentences(elements[no][:url])) end rescue => e puts("error: " + e.message)
-
-
CHAPTER 9-3
上記で作成したgugulu.rbを利用した人工無脳の応答作成について説明している。本からのコード変更はわずかだが、変更点も含め、コード理解のためのポイントをメモしておく。-
responder.rb 読み込みライブラリの追加
Google検索と形態素解析ライブラリをロードしている。- 読み込みライブラリの追加
-
require_relative 'morph' require_relative 'gugulu'
-
responder.rb GuguluResponderクラス
Google検索を利用した応答作成クラスである。私見だが、マルコフ辞書への学習部分を変更している。(本のままだと全部の辞書で学習してしまうため)- GuguluResponderクラス
-
class GuguluResponder < Responder def initialize(name, dictionary) # Guguluクラスのインスタンス生成 @ggl = Gugulu.new # オプションの検索ワード(サイト指定など) @query_opts = '' # スーパークラスのinitializeメソッドを呼び出す super end def response(input, parts, mood) keywords = [] # each do〜endの省略形 # 品詞がキーワードならば、検索用キーワードに追加 parts.each{|w, p| keywords.push(w) if Morph::keyword?(p)} # 3項演算子 xx ? yy : zzは、if xx then yy else zz endと同じ # keywordsが空ならばinputを、そうでなければkeywordsをスペース区切りでテキストにする query = (keywords.empty?)? input : keywords.join(' ') query += ' ' + @query_opts begin result = @ggl.search(query) # 検索結果が存在しなければ、'no results'例外を発生させる # Google Web APIからの変更により下記変更 # result.resultElements → result raise('no results') if result.empty? # Google Web APIからの変更により下記変更 # result.resultElements → result elem = select_random(result) # Google Web APIからの変更により下記変更 # elem.URL → elem[:url] sentences = Gugulu::get_sentences(elem[:url]) # Markovクラスのインスタンス生成(使い捨て) # responder.rbをロードするunmo.rbで、markov.rbをロードするdictionary.rbが # ロードされているので、ここでもインスタンス化できる? temp_markov = Markov.new # 検索結果を文章化したものに対して sentences.each do |line| # 形態素解析 parts = Morph::analyze(line) # マルコフモデルによる学習 temp_markov.add_sentence(parts) # @dictionary.study(line, parts)ではすべての辞書に対して学習 # を指示することになる。 # マルコフ辞書のみ学習するならば、以下であるべきでは? # @dictionary.markovでマルコフ辞書を指定。それに対してadd_sentenceを実行 @dictionary.markov.add_sentence(parts) end # マルコフモデルによる文章生成 resp = temp_markov.generate(select_random(keywords)) # respがnilでなければrespを戻り値にする return resp unless resp.nil? rescue => e puts(e.message) end # respがnilのときランダム辞書から選択 return select_random(@dictionary.random) end end
-
unmo.rbの変更点
Google検索機能の追加に伴う変更(GuguluResponderのインスタンス生成、各レスポンダーの出現確率調整)だけである。- unmo.rbの変更点
-
--- a/unmo.rb 2017-01-23 16:50:46.000000000 +0900 +++ b/unmo.rb 2017-01-26 01:07:34.000000000 +0900 @@ -13,6 +13,7 @@ @resp_pattern = PatternResponder.new('Pattern', @dictionary) @resp_template = TemplateResponder.new('Template', @dictionary) @resp_markov = MarkovResponder.new('Markov', @dictionary) + @resp_gugulu = GuguluResponder.new('Google', @dictionary) @responder = @resp_pattern end @@ -21,14 +22,16 @@ parts = Morph::analyze(input) case rand(100) - when 0..29 + when 0..19 @responder = @resp_pattern - when 30..49 + when 20..39 @responder = @resp_template - when 50..69 + when 40..54 @responder = @resp_random - when 70..89 + when 55..74 @responder = @resp_markov + when 75..94 + @responder = @resp_gugulu else @responder = @resp_what end
-
-
CHAPTER 9-4
今後の人工無脳拡張の話題であり、コードは出てこない。
- *注意*
- 文字コード(SJIS → UTF8)変換、改行コード(CR/LF → LF)変換、およびrequireのrequire_relativeへの書き換えは説明していないが、サンプルの実行には必要である。
以上。
この投稿へのトラックバック
トラックバックはありません。
- トラックバック URL
この投稿へのコメント