恋するプログラムをSinatraでWebアプリにするPart.12[CHAPTER9 ノビィ、ネットワークにつながる]
記事の概要
『恋するプログラム』の[CHAPTER9 ノビィ、ネットワークにつながる]を参考に、Google検索を利用して応答時の文を生成するチャットボットを作成します。
目次
開発環境
- OSX 10.11.2 El Capitan
- テキストエディタ: MacVim
- ターミナルエミュレータ: Macターミナル
- シェル: zsh
- パッケージマネージャ: Homebrew
- ブラウザ: Firefox - Ruby 2.0.0p645
- バージョンマネージャ: rbenv
- Webフレームワーク: Sinatra - MeCab 0.996
- 日本語辞書: IPA辞書
参考記事
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.rb
のGuguluResponder
クラスも戻り値に合わせて変更しました。
その他の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!(/ /, ' ') 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
場合によっては、起伏の烈しい会話が楽しめます。
参考書籍
参考記事
Sinatraについては、こちらの記事を参考にさせていただきました。
- Rubyの入門や書き捨てアプリを作る場合は sinatraがオススメ! - むかぁ~ どっと こむ
- SinatraとjQueryでおよそ100行で作るAjax掲示板アプリケーション - gaaamiiのブログ
- Sinatra: README (Japanese)
関連記事
- 恋するプログラムを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[おわりに]