2年目の営業マンが3ヶ月プログラミングを勉強して、RubyでAmazonのランキングを表示してくれるSlack botを作ってみた


自己紹介

普段は渋谷のフィンテックベンチャーでセールスマネージャーをしており、プログラミングとは無縁の仕事をしています。ビジネスサイドが主戦場なので、物を作れる人に対する大きな憧れがあったのと、日々の業務の中でも、自分自身のシステムに対する理解の浅さから、エンジニアに迷惑をかけてしまうことがありました。また、技術的知識不足から、競合とのコンペで負けてしまい、悔しい思いをしたことがあったので、プログラミングの勉強をすることにしました。

はじめに

2月からRubyの勉強を始め、今回の10連休で簡易的なサービスを作れればと思い、UIの設計が不要なSlack botを作ることにしました。

写真のように、botにメンションを飛ばすと、botが反応し、カテゴリを選ぶと、AmazonのTOP10を表示してくれます。

詳細のコードはGithubにて共有しております。rubyのバージョンは2.3.0 で作っています。

開発の大まかな流れ

  • 必要なデータをAmazonから取得する
  • Slackとの連携
  • Slack内でのUIの作り込み

Amazonから必要なデータをスクレイピング

今回はAmazonが提供しているAPIではなく、スクレイピングでデータを取得しています。


require 'mechanize'

今回は mechanize というgemを使ってスクレイピングを進めていきます。


# Amazon.comの対象カテゴリのランキングをスクレイピングで取得する
def get_amazon_ranking(category)
    agent = Mechanize.new
    agent.user_agent_alias = "Windows Mozilla" #書かないとエラーが起こるおまじない的なもの。
    page = agent.get("#{get_amazon_ranking_url(category)}")

ここでは、変数categoryのデータを引っ張ってくる指示を飛ばしています。


# カテゴリー指定
def get_amazon_ranking_url(category)
    if category.include?('ビジネス・経済')
        url = 'https://www.amazon.co.jp/gp/new-releases/books/466282'
    else
        exit
    end

    return url
end

カテゴリー指定は上記の様な形で行い、今後、入力したいカテゴリーを増やす際は、上記にカテゴリー名とURLを入力し、if文を作るだけで追加出来ます。

データをタイトルをkeyに、URLをvalueにしてハッシュへ変換していきます。


# XMLを配列にする(Titles)
def convert_array_from_xml_titles(xml_titles)
    titles = []
    xml_titles.each_with_index do |xml_title, i|
        titles <<  "#{i + 1}#{xml_title.inner_text.gsub(/\r\n|\r|\n|\s|\t/, "")}"
        break 1 if i == 9
    end
    return titles
end

# XMLを配列にする(URLs)
def convert_array_from_xml_urls(xml_urls)
    urls = []
    xml_urls.each_with_index do |xml_url, i|
        # レビューのURLもClass名が同じなので、取得しない
        # 不要なクエリパラメータを削除する
        url = xml_url.get_attribute('href').match(/dp\/[a-zA-Z0-9]+/).to_s

        # レビューを除いた際に、空白が入ってしまうので削除する
        if url != ''
            urls << 'https://www.amazon.co.jp/' + url
        end
        break 1 if i == 9
    end
    return urls
end

それぞれ、convert_array_from_xml という形で変数を定義し、配列を用意します。Urlsの部分では、本に対するレビューのURLも同じClass名の中に入っていたため、不要なクエリパラメータを削除するための処理を行っています。


(タイトルのURLと本のレビューのURLが a-link-normal という同一名義のClassに囲われてる)

タイトル、URLともに、10個取り出せた時点でbreakする設定をしています。ここで回したデータを、TitlesとUrlsに返し、指定したクラスからデータを抜き出します。


# 必要な項目だけを抜き出す&XMLを配列にする
    titles = convert_array_from_xml_titles(page.search('.p13n-sc-line-clamp-2'))
    urls   = convert_array_from_xml_urls(page.search('.a-col-left .a-link-normal'))

    rankings = {}
    titles.zip(urls) do |title, url| 
        rankings[title] = url
    end 

    return rankings
end

ここで更にrankingsという連想配列を用意し、.zipメソッド を活用して、TitleとURLがSlackに交互に出力される指示を出します。

Slackとの連携

まず、slack-ruby-client というgemをインストールし、ひな形をコピペします。

require 'slack-ruby-client'

  Slack.configure do |conf|
    conf.token = 'xoxb-*****************' # トークンは後ほど取得します。
  end

  # RTM Clientのインスタンス生成
  client = Slack::RealTime::Client.new

  # Slackに接続できたときの処理
  client.on :hello do
    puts 'connected!'
    client.message channel: 'your_channel_id', text: 'connected!'
  end

  # ユーザからのメッセージを検知したときの処理
  client.on :message do |data|
    if data['text'].include?('こんにちは')
      client.message channel: data['channel'], text: "Hi!"
    end
    if data['text'].include?('かしこい') || data['text'].include?('えらい')
      client.message channel: data['channel'], text: "Thank you!"
    end
    if data['text'].include?('おやすみ')
      client.message channel: data['channel'], text: "Good night"
    end
  end

  # Bot start
  client.start!

次に、ブラウザ上のSlackで Bots から、botを作成します。
https://xxxxxxxxxxx.slack.com/apps/search?q=bots

client.message channel: 'your_channel_id', text: 'connected!'

上記の部分を今回表示したい自分のSlackのチャンネルに変える作業を行います。

xoxb-***************** の部分に、bot作成後のアクセストークンを貼ると、繋込みがされます。

Slack内でのUIの作り込み

Slack内での挙動に関しては、@レイワーくん とスラックで呼び出すと、「僕は現在のAmazonランキング10選を教えることができるよ!★をつけてカテゴリを選んでください!」→「カテゴリー一覧」という流れで、反応するインターフェイスにしました。カテゴリー名をユーザーが入力したかどうかを検出するため、各カテゴリーの頭に★マークを入れ、★があるかどうかで、カテゴリー名と紐づけたデータを引っ張ってくる処理をif文で書いています。


# ユーザからのメッセージを検知したときの処理
client.on :message do |data|
    if data['text'].include?('レイワーくん') || data['text'].include?('<@UJA1HUXEG>')
        client.message channel: data['channel'], 
        # 追加していく!
        text: "僕は現在のAmazonランキング10選を教えることができるよ!\n★をつけてカテゴリを選んでください!\n```★ビジネス・経済 ★コンピュータ・IT ★科学・テクノロジー  ★エンターテイメント ★歴史・地理  ★教育・学参・受験 \n★文学・評論 ★社会・政治 ★家電&カメラ ★ホーム&キッチン ★ホビー ★パソコン・周辺機器 ★ゲーム ★おもちゃ```"
    end

    # FIXME
    # メッセージの中に★があれば、カテゴリとしてみな
    if data['text'].include?('★')
        rankings = get_amazon_ranking(data['text'])

        rankings.each{|title, url|
            client.message channel: data['channel'], text: "#{title} \n#{url}"
        }
    end
end

完成!

Herokuへのアップロードの過程はこちらの記事で紹介しております。

作り終えての感想

簡単に作れるかと思いきや、XMLの変換や、スクレイピングでノイズを除去する作業で、思った以上に苦戦しました。一方、エラーを自力で解決出来たときの快感や、作ったものを人に見せて褒めてもらえたときの喜びは、これまでにあまり経験したことのない感動でした。未知の世界で自分の非力さを実感しながら格闘出来たことも非常に価値のある経験でした。

制作過程で死にそうな自分を何度も助けてくださった@IZUMIRU0313さんには感謝しかないです。早くエンジニアの先輩方とも肩を並べて、共闘できるくらいの戦闘力を身に着けていけるよう精進していきます。

サービスに関して、質問があれば、@matsukazu1995g1までご連絡ください!

参考記事