Rubyでスクレイピングした結果をdiscordで表示するbotの作成


同級生がdiscordのbotを作っているのを見て面白そうだと思い作成するに至った。
同級生がRubyで動かしていて、ネットで見かける情報もRubyのものが多かったためその路線で行くことに。
PC自体はWin8だったけども環境構築をめんどくさがったため[cloud9](https://c9.io/)を使用。Rubyのバージョンは後述の参考サイトに倣って2.3.3にアップデート。

#目的
大乱闘スマッシュブラザーズ for WiiU/3DSのレーティング戦を行っている[スマメイト](http://sumamate.com/)という有志が運営しているサービスがある。
そこからレート戦ランキングの情報（順位、ユーザ名、レート）とユーザごとの情報（ユーザ名、ニンテンドーネットワークIDあるいはフレンドコード、勝利数、敗北数、勝率、レート、プロフィール）を取得してdiscord上に表示することが今回の目的。

#参考サイト

1. [イチからDiscord Bot 。for Ruby](http://qiita.com/deneola213/items/efaeb0f5c20d44608a71)
1. [Discord Botの作り方 #1](http://s4wa.hatenablog.com/entry/2017/08/07/194844)
1. [ruby入門者がdiscordのbotで遊んだ話](http://qiita.com/butaman551/items/7710113c0771cff05bbe)
1. [RubyでHTML解析が超余裕なんです](http://inobo52.hatenablog.com/entry/2014/09/04/Ruby%E3%81%A7HTML%E8%A7%A3%E6%9E%90%E3%81%8C%E8%B6%85%E4%BD%99%E8%A3%95%E3%81%AA%E3%82%93%E3%81%A7%E3%81%99)
1. [Ruby製の構文解析ツール、Nokogiriの使い方 with Xpath](http://blog.takuros.net/entry/2014/04/15/070434)

#作成
discordのアカウントは作成済みと言う前提で、botの作成手順は[参考サイト2](http://qiita.com/deneola213/items/efaeb0f5c20d44608a71)が丁寧に書いてあるのでそちらを見ながらbotユーザを作成する。

今回作成したコードがこちら。

```bot.rb
require 'discordrb'
require 'net/http'
require 'uri'
require 'open-uri'
require 'nokogiri'

bot = Discordrb::Commands::CommandBot.new token: 'トークン', client_id: クライアントID, prefix: '?'

bot.command :sumamate do |event, *code|
  if ("#{code[0]}" == "wiiu") #ハードウェア判別
    url = 'http://sumamate.com/'
    hardwareName = "WiiU"
  elsif ("#{code[0]}" == "3ds")
    url = 'http://sumamate3ds.com/'
    hardwareName = "3DS"
  else
  end
  
  if ("#{code[1]}" == "ranking") #ランキング表示
    
    requestUrl = url + "ranking/"
    
    charset = nil
    html = open(requestUrl) do |f|
      charset = f.charset
      f.read
    end
    
    doc = Nokogiri::HTML.parse(html, nil, charset)
    ENV["TZ"]="Asia/Tokyo"
    
    nowDate = Time.now
    youbi = %w[日 月 火 水 木 金 土]
    
    output = "```"
    output += hardwareName + "スマメイトランキング\n["
    output += nowDate.year.to_s + "年" + nowDate.month.to_s + "月" + nowDate.day.to_s + "日（" + youbi[nowDate.wday] + "）"
    output += nowDate.hour.to_s + "時" + nowDate.min.to_s + "分" + nowDate.sec.to_s + "秒に取得]\n"
    
    doc.css('div.row-battle').each do |node|
      rankTmp = node.css('div.rank').inner_html.to_s.strip
      nameTmp = node.css('p.name').css('a').inner_html.to_s.strip
      rateTmp = node.css('div.rate').inner_html.to_s.strip
      output += rankTmp + "：" + nameTmp + "：" + rateTmp + "\n"
    end
    output += "```"
    
    event.send_message output
    
    
  elsif ("#{code[1]}" == "user") #ユーザ検索機能
    requestUrl = url + "search/?search=" + "#{code[2]}"
    
    charset = nil
    html = open(requestUrl) do |f|
      charset = f.charset
      f.read
    end
    doc = Nokogiri::HTML.parse(html, nil, charset)
    
    hitAmount = doc.css('div.col-xs-8').css('h2').inner_html.gsub("検索結果","").gsub("件","").to_s.strip
    
    if(hitAmount != "")
      userUrl = doc.css('p.name').css('a').attribute('href').value.to_s #twitterIDで検索した結果から特定のユーザIDを含んだURL取得する
    else
      event.send_message ("ユーザが見つかりません")
      break
    end
    
    nowDate = Time.now
    youbi = %w[日 月 火 水 木 金 土]
    
    html = open(userUrl) do |f|
      charset = f.charset
      f.read
    end
    userDoc = Nokogiri::HTML.parse(html, nil, charset)
    
    userName = userDoc.css('table.user-table').css('span.user-name').inner_html.to_s.strip #ユーザ名
    userNintendoNetworkId = userDoc.css('table.user-table').css('tr')[1].css('td').inner_html.to_s.strip #ニンテンドーネットワークIDあるいはフレンドコードを格納
    
    userRate = userDoc.css('div.side').css('div.col-xs-6')[1].inner_html.to_s.strip #レートを格納
    
    userVictoryOrDefeat = userDoc.css('div.side').css('div.col-xs-6')[3].inner_html.to_s.strip.gsub("勝 ","\n").gsub("敗","") #勝敗数（文字列）
    userVictoryOrDefeatArray = userVictoryOrDefeat.split("\n") #勝敗数（配列）
    userVictoryAmount = userVictoryOrDefeatArray[0].to_f
    userDefeatAmount = userVictoryOrDefeatArray[1].to_f
    userWinningPercentage = ((userVictoryAmount / (userVictoryAmount + userDefeatAmount)).round(4) * 100).to_s #勝率
    
    userSeriesOfVictories = userDoc.css('div.side').css('span.red').inner_html.to_s.strip.gsub("現在","").gsub("連勝","").gsub("！","") #連勝数
    
    userProfile = userDoc.css('div.col-xs-8').css('div.side').inner_html.to_s.strip.gsub("<br>","") #プロフィール
    
    output = "```"
    output += userName + "さんの" + hardwareName + "スマメイトデータ\n["
    output += nowDate.year.to_s + "年" + nowDate.month.to_s + "月" + nowDate.day.to_s + "日（" + youbi[nowDate.wday] + "）"
    output += nowDate.hour.to_s + "時" + nowDate.min.to_s + "分" + nowDate.sec.to_s + "秒に取得]\n\n"
    
    if (hardwareName == "WiiU") #ハードウェア判別
      output += "ニンテンドーネットワークID：" + userNintendoNetworkId + "\n\n"
    elsif (hardwareName == "3DS")
      output += "フレンドコード：" + userNintendoNetworkId + "\n\n"
    else
    end
    
    if(userVictoryOrDefeat == "")
      output += "今期はまだ対戦を行っていません\n"
    else
      output += "今期レート：" + userRate + "\n"
      output += "今期対戦成績：" + userVictoryOrDefeatArray[0] + "勝 " + userVictoryOrDefeatArray[1] + "敗（勝率 " + userWinningPercentage + "%）\n"
    end
    
    if(userSeriesOfVictories == "")
    else
      output += "現在の連勝数：" + userSeriesOfVictories + "\n"
    end
    output += "\nプロフィール：\n" + userProfile
    output += "```"
    
    event.send_message output
    
  else
  end
end

bot.run
```

だいぶゴリ押ししたところが見えるコードとなった。

動作させるにはcloud9のターミナルで

```
$ ruby bot.rb
```
でbotを起動させた後にdiscordのチャットで

```
?sumamate {機種} ranking
```
とすれば機種ごとのランキングが表示される。（機種のところには```3ds```か```wiiu```のどちらかが入る）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/179114/621600be-6612-ab4c-3d52-329476b4f7d0.png)


また、ユーザごとの情報を見たい場合はチャットで

```
?sumamate {機種} user {TwitterID}
```
と検索すればよい。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/179114/dbddf8d4-0f86-0ef3-e9f3-519219478af3.png)


#課題点

- コードが雑（変数名が雑）
- 取得した情報を一旦処理したくせに処理前と同じような文で出力している
- ユーザ検索がTwitterIDだけなため不便

特にユーザ検索が不便というのは重大な問題点である。
今回ユーザ検索に使用した方法がスマメイトの検索機能を利用したもので、TwitterIDで検索をした結果のページをスクレイピングし結果として出てきたユーザページへのURLからまたスクレイピングをし情報を取得する。といったものである。検索できるものとしてはTwitterIDの他にニンテンドーネットワークID、ユーザ名がある。
しかしユーザ名は短いものが多く、他のユーザ名の一部分である可能性が高い。その場合多数の検索結果が出てくるため求めるユーザにたどり着くことが難しくなる。（""で囲う完全一致検索は使用できなかった）
またニンテンドーネットワークIDで検索することもできるがサブアカウントを持つユーザは少なくない。そのためメインで使っているアカウントを出すことがいささか面倒である。（いっそのことサブも含めて検索結果に出すという手も）
ともかくユーザ検索が不便であるため利便性を高めたい。

#感想
1日使って勢いで完成させた機能であったがひとまず動くものができたので良かった。
この機能を作る前にAPIを叩いてキャラクターの情報を引っ張ってくるという機能も実装していたがそれについては気が向いたらまた別の機会に記事にしようと思う。