「恋するプログラム」の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