「恋するプログラム」の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を用いる部分の書き換えは、こちらを参考にさせてもらった。

    1. Ogaの導入
      Google Web APIが使えないので、本と同等のことを実現するには、以下の手順が必要になる。

      1. Ruby標準のopen-uriで検索を実行(HTML文書を取得)
      2. 検索結果(HTML文書)を構文解析
      3. 解析結果から、タイトルとURLを取得(Webスクレイピング)

      RubyでWebスクレイピングを行うライブラリとしてはNokogiriが有名だが、Ruby2.1以上でないと導入できない。macOS標準添付のRuby2.0でも使えるのはOgaである。よってこれをインストールする。

                          sudo gem install oga
                      
    2. 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
      
                                  (以下省略)    
                              
    3. 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
                              
    4. 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
                              
    5. 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!(/&nbsp;/, ' ')
      
                                      # 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
                              
    6. 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
                              
    7. 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を利用した人工無脳の応答作成について説明している。本からのコード変更はわずかだが、変更点も含め、コード理解のためのポイントをメモしておく。

    1. responder.rb 読み込みライブラリの追加
      Google検索と形態素解析ライブラリをロードしている。

      読み込みライブラリの追加
                                  require_relative 'morph'
                                  require_relative 'gugulu'
                              
    2. 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
                              
    3. 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