Monthly Hacker's Blog

プログラミングや機械学習の記事を中心に書きます。

はてなブログのアクセス解析情報をSlackbotで共有する

はじめに

こんにちは、さかぱ(@zacapa_23)です。最近はPokemon GOが大流行ですね。私のキャンパスにもポケストップとジムが設置されていて、常に桜が舞い散り、ジムが建て替えられています。(追記: たった1週間の幻でした。
矢上キャンパスからポケストップとジムが消えた - 三度の飯よりラム酒が好き
)

今月のMonthly HackのテーマはBot。これはSNSやオンラインゲームを中心に馴染み深い、決められたタスクを自動的に行う仕組みです*1

私は当ブログのアクセス解析情報*2をslackに投稿するbotを作成しました。このアクセス解析情報は、読者ターゲットや今後のテーマを考えるとき、参考にするものです。そもそも当ブログは研究室の学生達で執筆しているので、個々人が解析ページを閲覧するよりも、誰かが報告するほうが効率的。そこで誰かの仕事をslackbotにやってもらうという運びになりました。

イメージとしては下のアクセス解析情報を、
f:id:ron_zacapa:20160723124851p:plain
slackbotが投稿します。
f:id:ron_zacapa:20160723124900p:plain

本記事では、以下のことについて書きます。

  • SeleniumでログインHTML取得
  • PandasでTableを抽出
  • SlackerでSlackbotの作成
  • Crontabでプログラムを定期的に実行

ログイン

アクセス解析情報ページには、'はてな'にログインした状態でないとセキュリティに阻まれます。そこで前回のScrapingと同様にSeleniumを使います。Seleniumは実際にブラウザを立ち上げて操作することができるモジュールです。詳しい説明は以下の記事をお読みください。
www.monthly-hack.com

まずは予め、はてなIDとパスワードを書いたファイルを用意します*3

# hatena_idpw.py
username = '*******'
password = '*******'

SeleniumでChromeを立ち上げて、はてなにログインします。実際にブラウザを操作しているので、ロード時間に余裕を持たせるといいでしょう*4

# Chromeの起動
driver = webdriver.Chrome('./chromedriver')
driver.get("http://www.hatena.ne.jp/")

# ログインページに移動
elem = driver.find_element_by_link_text('ログイン')
elem.send_keys(Keys.RETURN)
time.sleep(3) # ロードを待つ

# はてなIDの入力
elem = driver.find_element_by_name('name')
elem.send_keys(hatena_idpw.username)

# パスワードの入力
elem = driver.find_element_by_name('password')
elem.send_keys(hatena_idpw.password)

# ログイン
elem = driver.find_element_by_class_name('submit-button')
elem.send_keys(Keys.RETURN)
time.sleep(3) # ロードを待つ

スクレイピング

ログインが完了したので必要な情報をスクレイピングします。今回は以下の項目を対象にします。

  • トータルアクセス数
  • トータルユニークアクセス数
  • アクセス先URL(タブ:URL)
  • アクセス元URL(タブ:リンク元)

スクレイピング対象のページにはクエリを含んだURLを使って直接飛びます。アクセス数はXPathで取得しますが、各URLはテーブル(表)形式なので、HTMLソースをそのままPandasを渡します。詳しくは以下の記事をお読みください。
qiita.com

# アクセス数の取得
driver.get('http://counter.hatena.ne.jp/ron_zacapa/report?cid=102&date={}&mode=access&type=daily'.format(date))
elem = driver.find_element_by_xpath('//*[@id="totalreport"]/table/tbody/tr[1]/td/strong')
ta = elem.get_attribute('innerHTML')
elem = driver.find_element_by_xpath('//*[@id="totalreport"]/table/tbody/tr[2]/td/strong')
tua = elem.get_attribute('innerHTML')

# アクセス先URLの取得
driver.get('http://counter.hatena.ne.jp/ron_zacapa/report?cid=102&date={}&mode=summary&target=url&type=daily'.format(date))
html = driver.page_source
df1 = pandas.read_html(html)[0]

# アクセス元URLの取得
driver.get('http://counter.hatena.ne.jp/ron_zacapa/report?cid=102&date={}&mode=summary&target=link&type=daily'.format(date))
html = driver.page_source
df2 = pandas.read_html(html)[0]


次に取得したデータを整形します。またURLのままでは、何の記事のことか判断しにくいのでページのタイトルと入れ替えます。

# 余計な行と列を削除
df2 = df2.drop(0)
df2 = df2.drop(3, axis=1)
df1 = df1.drop(0)
df1 = df1.drop(3, axis=1)

# URLをページタイトルに変換
def get_title(df):
    for i, row in df.iterrows():
        try:
            driver.get(row[0])
            time.sleep(1)
            title = driver.title
            if title != '':
              df[0][i] = title
        except:
          pass
    return df

df1 = get_title(df1)
df2 = get_title(df2)

Slackbotの作成

Slackbotをpythonで作成するには、slackbot*5slackerを導入するのがおすすめ。今回は後者のSlackerを使います。

まずはこちらからslackbotを作成してAPI Tokenを取得してください。そして別ファイルで保存しておきます*6

# slackbot_api.py
api = '****************'

そしてslackbotが投稿するメッセージを作成していきます。改行を反映するために'>>>'(引用コマンド)を文頭にします。メッセージを作成し終わったら、slackerを使って'bot-test'というチャンネルに投稿です。

# slackbotのメッセージ作成
date = date.split('-')
message = '>>>'
message += '{}年{}月{}日のアクセス状況です\n'.format(date[0], date[1], date[2])
message += '----------------------------------------\n'
message += 'アクセス数 (ユニーク): {}({})\n'.format(ta, tua)
message += '\n読まれている記事:\n'

for i, row in df1.iterrows():
    message += '{}: {}件({})\n'.format(row[0], row[1], row[2])

message += '\nリンク元: \n'

for i, row in df1.iterrows():
    message += '{}: {}件({})\n'.format(row[0], row[1], row[2])

# slackに投稿
slack = Slacker(slackbot_api.api)
slack.chat.post_message('bot-test', message, as_user=True)

定期的に実行

前章まででプログラムコードの作成は終了です。ここからは毎日自動でプログラムを実行するためにcrontabを使います。これは予め設定した時間に指定されたコマンドを実行してくれます。

$ crontab -e

で設定ファイルを開いて、時間とコマンドを[分 時 日 月 曜日 実行コマンド]の順で書き込みます。

# 例1: 毎日23時55分に'python hatenabot.py 2016-07-30'を実行する
# *は'全て'を表す
55 23 * * * python hatenabot.py 2016-07-30

# 例2: 平日の11時55分と23時55分に実行する
# -を使うことで連続した数値を表す
55 23 * * 1-5 python hatenabot.py 2016-07-30
55 11 * * 1-5 python hatenabot.py 2016-07-30

# 上記の2つの文は,を使って統合できます。
55 11,23 * * 1-5 python hatenabot.py 2016-07-30

実際には以下の設定を書きました。注意点は2つあり、1つ目はpythonや実行ファイルは絶対パスで指定することです*7。2つ目に引数の日付はbashのdate関数を使うことです*8

55 23 * * * //anaconda/bin/python ~/Google/Monthly_Hack/muto/07bot/hatenabot.py $(date +%Y-%m-%d)

以上で毎日23時55分にアクセス解析情報を投稿するslackbotの完成です。

おわりに

先月のスクレイピングで学んだことを活かしたbotを作成しました。プログラムを自動実行するcrontabは、今後のMonthly Hackで活かせると思いました。来月のテーマはTwitterです。

ソースコード

今回作成したプログラムです。

$ python hatenabot.py 2016-07-30

のように日付(YYYY-MM-DD)を引数にして実行してください。

# -*- coding: utf-8 -*-
import sys
import time
import pandas
import hatena_idpw
import slackbot_api
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from slacker import Slacker

# 日付を取得
date = sys.argv[1]

# Chromeの起動
driver = webdriver.Chrome('./chromedriver')
driver.get("http://www.hatena.ne.jp/")

# ログインページに移動
elem = driver.find_element_by_link_text('ログイン')
elem.send_keys(Keys.RETURN)
time.sleep(3) # ロードを待つ

# はてなIDの入力
elem = driver.find_element_by_name('name')
elem.send_keys(hatena_idpw.username)

# パスワードの入力
elem = driver.find_element_by_name('password')
elem.send_keys(hatena_idpw.password)

# ログイン
elem = driver.find_element_by_class_name('submit-button')
elem.send_keys(Keys.RETURN)
time.sleep(3) # ロードを待つ

# アクセス数の取得
driver.get('http://counter.hatena.ne.jp/ron_zacapa/report?cid=102&date={}&mode=access&type=daily'.format(date))
elem = driver.find_element_by_xpath('//*[@id="totalreport"]/table/tbody/tr[1]/td/strong')
ta = elem.get_attribute('innerHTML')
elem = driver.find_element_by_xpath('//*[@id="totalreport"]/table/tbody/tr[2]/td/strong')
tua = elem.get_attribute('innerHTML')

# アクセス先URLの取得
driver.get('http://counter.hatena.ne.jp/ron_zacapa/report?cid=102&date={}&mode=summary&target=url&type=daily'.format(date))
html = driver.page_source
df1 = pandas.read_html(html)[0]

# アクセス元URLの取得
driver.get('http://counter.hatena.ne.jp/ron_zacapa/report?cid=102&date={}&mode=summary&target=link&type=daily'.format(date))
html = driver.page_source
df2 = pandas.read_html(html)[0]

# 余計な行と列を削除
df2 = df2.drop(0)
df2 = df2.drop(3, axis=1)
df1 = df1.drop(0)
df1 = df1.drop(3, axis=1)

# URLをページタイトルに変換
def get_title(df):
    for i, row in df.iterrows():
        try:
            driver.get(row[0])
            time.sleep(1)
            title = driver.title
            if title != '':
              df[0][i] = title
        except:
          pass
    return df

df1 = get_title(df1)
df2 = get_title(df2)

# slackbotのメッセージ作成
date = date.split('-')
message = '>>>'
message += '{}年{}月{}日のアクセス状況です\n'.format(date[0], date[1], date[2])
message += '----------------------------------------\n'
message += 'アクセス数 (ユニーク): {}({})\n'.format(ta, tua)
message += '\n読まれている記事:\n'

for i, row in df1.iterrows():
    message += '{}: {}件({})\n'.format(row[0], row[1], row[2])

message += '\nリンク元: \n'

for i, row in df1.iterrows():
    message += '{}: {}件({})\n'.format(row[0], row[1], row[2])

# slackに投稿
slack = Slacker(slackbot_api.api)
slack.chat.post_message('bot-test', message, as_user=True)

*1:正確な'bot'の定義はわからないので、あくまでも個人の考えです。

*2:はてなブログProが利用できる、はてなカウンターを対象にします。

*3:別ファイルを用意せず、elem.send_keys('*******')と直接書いても大丈夫です。

*4:今回はtime.sleep()を使っていますが、seleniumのexpected_conditionsでwaitさせる方法もあります。

*5:紛らわしいですね。

*6:はてなログインと同様に、省略しても大丈夫です。

*7:crontabは編集したディレクトリで実行するわけではないので。

*8:動的にしないと同じ日のデータが投稿されてしまいます