あかんわ

覚えたことをブログに書くようにすれば多少はやる気が出るかと思ったんです

恋するプログラムをSinatraでWebアプリにするPart.12[CHAPTER9 ノビィ、ネットワークにつながる]

記事の概要

『恋するプログラム』の[CHAPTER9 ノビィ、ネットワークにつながる]を参考に、Google検索を利用して応答時の文を生成するチャットボットを作成します。

目次

開発環境

参考記事
Rubyの開発環境構築は、こちらの記事を参考にさせていただきました。

ノビィがググる

入力した会話の内容をGoogleで検索し、検索結果の記事から応答文を作成します。

アプリケーションディレクトリの構成
~/programinlove
    |- proto.rb                 // CUIチャットボットのメインファイル
    |- unmo.rb                  // チャットボットオブジェクトのモデル
    |- responder.rb             // 応答オブジェクトのモデル
    |- dictionary.rb            // 辞書を読み込み管理するクラス
    |- mecab_natto.rb           // 形態素解析器MeCabを使うためのモジュール
    |- morph.rb                 // MeCabNattoモジュールをunmo.rbで使うためのモジュール
    |- morkov.rb                // マルコフ連鎖を利用して辞書を作成するクラス
    |- gugulu.rb                // Google検索を利用するためのクラス
    |- utils.rb                 // select_randomメソッドを定義しただけのファイル
    |- log.txt                  // ユーザの入力履歴を保存するログファイル
    |- /dics                    // 辞書ファイルを格納するディレクトリ
    |     |- random.txt         // ランダムな応答を返すための辞書
    |     |- pattern.txt        // パターンに合った応答を返すための辞書
    |     |- template.txt       // テンプレートを使った応答を返すための辞書
    |     |- markov.dat         // マルコフ連鎖を使った応答を返すための辞書
    |- noby.rb                  // Webアプリのメインファイル
    |- /views                   // ビューのテンプレートを配置するディレクトリ
    |     |- index.erb          // チャットのインターフェースを表示するビュー
    |- /public                  // 静的ファイルを配置するディレクトリ
          |- nobycanvas.js      // キャラクターの画像をアニメーションさせるJavascript
          |- styles.css         // チャットのインターフェースをレイアウトするcss
          |- /img               // 画像ファイルを格納するディレクトリ
               |- /normal       // 平常時の画像を格納するディレクトリ
               |- /blink        // 瞬きの画像を格納するディレクトリ
               |- /lookaround   // 周囲を見回す画像を格納するディレクトリ
               |- /talk         // 喋る画像を格納するディレクトリ
               ~ 以下に表情ごとの画像を格納するディレクトリを配置
ソースコード

gugulu.rbでは、XML/HTML parserのOgaを使ってHTMLを切り出し、検索結果のURLを取得しています。 書籍内に記載されているとおり、現在はGoogle Web APIが利用できないようなので、地道にスクレイピングしました。

また、サンプルプログラムと異なり、Guguluクラスのsearchメソッドの戻り値を、検索結果のリンクのタイトルとURLを格納した配列に変更したため、responder.rbGuguluResponderクラスも戻り値に合わせて変更しました。

その他のRubyファイルは、サンプルプログラムと同様の記述で、特に変更はありません。

gugulu.rb
require 'open-uri'        # URLを開く
require 'Oga'             # HTMLやXMLをパースする
require 'cgi'             # 日本語をURLエンコード
require_relative 'morph'

class Gugulu
  GOOGLE = "https://www.google.co.jp"

  def search(query)
    # 検索文字列をURLエンコードして取得
    search_word = CGI.escape(query)

    # 検索結果のURLを開いて要素を取得
    query_url = "#{GOOGLE}/search?q=#{search_word}"
    begin
      search_result = open(query_url) do |f|
        # 文字化け対策にutf-8を指定してhtmlを読み込む
        f.read.encode('utf-8')
      end
      get_elements(search_result)
    rescue => e
      # Google検索ができなかった場合はエラーを表示
      puts("Search Error: #{e.message}")
    end
  end

  def get_elements(html)
    element = []
    # HTMLをパースしてリンクとタイトルを取得
    doc = Oga.parse_html(html)
    doc.xpath('//h3/a').each do |node|
      link = node.get('href')
      # open-uriでhttpを開こうとするとエラーが出るので
      # httpsのurlのみ取得する
      if link =~ /^\/url\?q=https:/
        element.push({ title: node.text,
                        url: "#{GOOGLE}#{link}" })
      end
    end

    # 検索結果のリンクが開けるか確認
    element.each do |elem|
      begin
        open(elem[:url])
      rescue => e
        # リンクが開けない場合は配列から削除
        element.delete(elem)
      end
    end
  end

  def self::get_sentences(url)
    html = open(url){|f| f.read}
    return html2sentences(html)
  end

  def self::html2sentences(html)
    html.gsub!(/<!--.*?-->/im, '')
    html.gsub!(/<.*?>/im, '')
    html = CGI.unescapeHTML(html)
    html.gsub!(/&nbsp;/, ' ')
    html.gsub!(/^[\s ]+/, '')
    html.gsub!(/[\s ]+$/, '')
    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
end

if $0 == __FILE__
  Morph::init_analyzer
  ggl = Gugulu.new

  loop do
    print('Search: ')
    line = gets.chomp
    break if line.empty?

    begin
      elements = ggl.search(line)
      elements.each.with_index(1) do |elem, i|
        puts('%d %s'%[i, elem[:title]])
        puts('    ' + elem[:url])
      end
      puts

      loop do
        print('Get: ')
        line = gets.chomp
        break if line.empty?
        no = line.to_i - 1
        next unless elements[no]
        puts(Gugulu::get_sentences(elements[no][:url]))
      end
    rescue => e
      puts("error: " + e.message)
    end
  end
end
responder.rb/GuguluResponder
class GuguluResponder < Responder
  def initialize(name, dictionary)
    @ggl = Gugulu.new
    @query_opts = ''
    super
  end

  def response(input, parts, mood)
    keywords = []
    parts.each{|w, p| keywords.push(w) if Morph::keyword?(p)}
    query = (keywords.empty?) ? input : keywords.join(' ')
    query += ' ' + @query_opts

    begin
      result = @ggl.search(query)
      raise('no results') if result.empty?
      elem = select_random(result)
      sentences = Gugulu::get_sentences(elem[:url])

      markov = Markov.new
      sentences.each do |line|
        parts = Morph::analyze(line)
        markov.add_sentence(parts)
        @dictionary.study(line, parts)
      end

      resp = markov.generate(select_random(keywords))
      return resp unless resp.nil?
    rescue => e
      puts(e.message)
    end

    return select_random(@dictionary.random)
  end
end

このコードのコミットには、[chapter9-3]のタグが付いてます。

実行結果

ターミナルでnoby.rbを動かし、ブラウザにhttp://localhost:4567を入力します。

~/programinlove
$ruby noby.rb

f:id:b0npu:20160107142020p:plain

場合によっては、起伏の烈しい会話が楽しめます。

f:id:b0npu:20160327180849p:plain

参考書籍

参考記事

Sinatraについては、こちらの記事を参考にさせていただきました。

関連記事

- 恋するプログラムをSinatraでWebアプリにするPart.0[はじめに]
- 恋するプログラムをSinatraでWebアプリにするPart.1[CHAPTER3 ほんとに無能]
- 恋するプログラムをSinatraでWebアプリにするPart.2[CHAPTER4 あこがれのGUI]①
- 恋するプログラムをSinatraでWebアプリにするPart.3[CHAPTER4 あこがれのGUI]②
- 恋するプログラムをSinatraでWebアプリにするPart.4[CHAPTER4 あこがれのGUI]③
- 恋するプログラムをSinatraでWebアプリにするPart.5[CHAPTER5 辞書を片手に]
- 恋するプログラムをSinatraでWebアプリにするPart.6[CHAPTER6 感情コントロールの魔術師]①
- 恋するプログラムをSinatraでWebアプリにするPart.7[CHAPTER6 感情コントロールの魔術師]②
- 恋するプログラムをSinatraでWebアプリにするPart.8[CHAPTER7 学習のススメ]①
- 恋するプログラムをSinatraでWebアプリにするPart.9[CHAPTER7 学習のススメ]②
- 恋するプログラムをSinatraでWebアプリにするPart.10[CHAPTER7 学習のススメ]③
- 恋するプログラムをSinatraでWebアプリにするPart.11[CHAPTER8 文章を作り出す]
- 恋するプログラムをSinatraでWebアプリにするPart.13[おわりに]