もう毎日の打刻めんどいからSlack botにやってもらおう ―― その2
Table of Contents
現在、セキュリティーが厳しい現場にいるため、外部サイトへのアクセスが制限されています。 そのため、今まではPCからやっていた打刻がスマートフォンでしかできなくなり、あの小さい画面でユーザーIDを入力して、パスワード、種別を選択して。。。。 めんどくさい ということでSlack botにやってもらいます。
という記事を前に書きました。 https://qiita.com/kentakozuka/items/eedebcc4275c894c45a3
今回はその改良版についての記事になります。 改良版といってもまるっとほぼ変えてしまったのですが。。。
コードはgithubに載せています。 https://github.com/kentakozuka/dakoku_v2
やったこと
打刻
Slackで「おは」を打つと出勤打刻をして、「おつ」を打つと退勤打刻して、「した」と打つと退勤打刻+残業申請をします。 slackbotで投稿された文字を判定し、「おは」と「おつ」と「した」ならSeleniumでWebサイトに打刻しに行き、結果をスクショで返します。
ユーザー情報登録・変更
Slackでボットに対してメンションを送るとCLIっぽくユーザー登録変更ができます。
ライブラリ
主要なライブラリはslackbot、Slacker、Selenium、parseです。
出勤時:「おは」と投稿された場合
メッセージリスナー
@listen_to('おは')
def start_work(message):
''' 出勤打刻をする '''
mh = MessageHandler( \
message, \
Ts.CrawlRunner(Ts.StartStampCrawler()), \
const.msg_start_stamp, \
const.msg_start_greeting)
mh.run_crawler()
メッセージハンドラー
class MessageHandler(Handler):
'''メッセージハンドリングクラス'''
(中略)
def run_crawler(self):
''' クロール共通処理 '''
# ユーザー名を取得
send_user = self.get_send_user()
user_info = super().get_user_info(send_user)
# チャンネル名を取得
channel_info = self.get_channel_info()
# ユーザー情報が存在すれば打刻処理を行う
if user_info:
if not super().is_all_fields_filled(send_user):
self.message.reply('設定されていない項目があるため打刻処理を実行できません。')
else:
# クロール
img_path = self.crawlrunner.run(user_info)
# スクショを投稿
self.post_img(img_path)
else:
self.message.reply(self.greeting_msg.format(send_user))
# disconnect
self.cursor.close()
self.connect.close()
メッセージが「おは」場合、メッセージハンドラーを実行します。ハンドラーにはクローラークラスのインスタンスを呼び出して、クロールを実行させます。ハンドラー内ではリスナーのコールバックの引数で受け取った情報からユーザー名を取りだし、自分(と同期)のユーザー名と一致し、かつDBに情報がある場合はクロールを実行します。別の人ならただ返信します。
打刻しにいく
class AbsCrawler:
'''
Strategyパターン
クロール実行抽象クラス
'''
def __init__(self):
self.user_info = None
# ドライバを生成
self.driver = self.init_driver()
# 最大の待機時間(秒)を設定
self.wait = WebDriverWait(self.driver, 20)
def set_user_info(self, user_info):
''' ユーザー情報のセッター '''
self.user_info = user_info
@abstractmethod
def run(self):
pass
#######################################
# ページ遷移
#######################################
def proc_with_page_move(self, func):
''' ページ読み込みを含む処理 '''
# 関数実行
func()
# ページが読み込まれるまで待機
self.wait.until(ec.presence_of_all_elements_located)
def go_page(self, page_url):
''' 指定した画面に遷移 '''
self.driver.get(page_url)
def go_login_page(self):
''' ログイン画面に遷移 '''
page_url = const.LOGIN_URL
self.go_page(page_url)
def go_time_stamp_page(self):
''' 打刻画面に遷移 '''
page_url = const.TIME_STAMP_URL
self.go_page(page_url)
(中略)
#######################################
# フォーム送信
#######################################
def login(self):
''' ログインフォーム '''
loginid = self.driver.find_element_by_id('id')
password = self.driver.find_element_by_id('pass')
loginid.send_keys(self.user_info['daim_id'])
password.send_keys(self.user_info['daim_password'])
self.driver.find_element_by_name("form01").submit()
def stamp_start(self):
''' 出勤打刻フォーム '''
self.set_option(const.FORM_NAME_11, self.user_info['office_id'])
self.set_option(const.FORM_NAME_16, self.user_info['department'])
self.set_option(const.FORM_NAME_12, '1')
self.driver.find_element_by_name("form01").submit()
class StartStampCrawler(AbsCrawler):
''' 出勤打刻クラス '''
def __init__(self):
super().__init__()
def run(self):
# ログインページ遷移
super().proc_with_page_move(super().go_login_page)
# ログイン実行
super().proc_with_page_move(super().login)
## 打刻画面遷移
super().proc_with_page_move(super().go_time_stamp_page)
## 出勤打刻実行
super().proc_with_page_move(super().stamp_start)
# スクショ
img_path = super().take_screen_shot()
# ドライバを閉じる
self.driver.quit()
# スクショのパスを返す
return img_path
クローラーの実行でやることは単純で、ブラウザでやっていることと同じことをSeleniumにやらせるだけです。 ポイントとしては以下で余裕をもった時間を設定しておかないとDOMをすべて解析する前にタイムアウトと判断されてエラーがでることです。
# 最大の待機時間(秒)を設定
self.wait = WebDriverWait(self.driver, 20)
また、ドライバは”普通の”ブラウザ(Chromeとか)を設定することもできますが、その場合ブラウザが立ち上がって処理が進むのでGUIがない環境ではPhantomjsを設定しないと動きません。
driver_path = <ドライバのパス>
user_agent = <ユーザーエージェント>
dcap = {
"phantomjs.page.settings.userAgent" : user_agent,
'marionette' : True
}
driver = webdriver.PhantomJS(
executable_path=driver_path,
desired_capabilities=dcap)
return driver
ちなみに、遷移した最後の画面のスクショをslackに送信するのですが、slackbotでは画像の送信ができないため、一度画像ファイルとして保存したスクショをSlackerで送信しています。
# スクショを投稿する
slacker = Slacker(<API-トークン>)
slacker.files.upload( \
<ファイルパス>, \
channels=[<投稿するチャンネル名>], \
title=self.finish_msg.format(<タイトル>)
ユーザー情報の登録・変更
コマンドのパーサについて、今回は簡単なコマンドのパースだったので、以下のライブラリを使ってパターンマッチで実装しました。
https://github.com/r1chardj0n3s/parse
この部分は自信がなかったのでpytestを使ってテストしながら実装しました。やっぱり単体テストすると安心しますね。
実際のコマンドの使いかたはこんな感じです。
使い方: @dakoku_bot コマンド
Slack上で動作する打刻ボット
コマンド:
show 各種情報を表示します。
サブコマンド:
user ユーザー情報を表示します。
office 部署情報を表示します。
入力例 --ユーザー情報を表示する:
@dakoku_bot show user
add ユーザー情報を新規登録します。
入力例:
@dakoku_bot add
set 各種情報を設定します。
オプション:
--uid=string stringにはユーザーID (user id) を入力します。
--pw=string stringにはパスワード (password) を入力します。
--dep=string stringには所属部署 (department) の番号を入力します。
--atime=HH:MM HH:MMには出勤時間 (attendance time) を入力します。
--ltime=HH:MM HH:MMには退勤時間 (leaving time) を入力します。
--cow=string stringには残業理由 (cause of overtime-work) を入力します。
入力例 1 --まとめて設定する:
@dakoku_bot set --uid=0123456789 --pw=mypassword --atime=09:00 --ltime=18:00 --cow=作業過多のため
入力例 2 --残業理由のみ変更する:
@dakoku_bot set --cow=作業過多のため
del ユーザー情報を削除します。
入力例:
@dakoku_bot del
やってみて思ったこと
前回の記事にも書いたのですが、毎日が便利になりました。やっぱり、ここが一番うれしいですね。 前回はScrapyを使っていたのですが、どうしてもある画面でセッションが取得できなくて、かなりハマってしまいました。試行錯誤を重ねたのですが結局できず、Seleniumにすべてを書き換えることになりました。実際、簡単なクローリングならSeleniumの方が格段に楽に書けると感じました。ScrapyでもPhantomjsは使えるみたいです。 パーサー部分はかなり汚いコードになってしまいました。やっぱりパーサーは宣言的に書くべきなんでしょうかね。
最後に便利なライブラリを作っているみなさま、ありがとうございます!