職場でSlackを利用している。なかなか使いやすいし便利だ。しかし、技術者としては与えられた機能を利用するだけではやはり満足できない。自分好みの道具を作りたい。
というわけで、単なるチャットツールとして使うSlackでは物足りなくなったので、Slackのボット(bot)をPythonで作ってみることにした。Hubotのようなボット用フレームワークも利用可能だが、SlackのAPIをガシガシ制御する感じにしたかったので、Slack Real Time Messaging APIをPythonから制御してSlackと連携した。今回は、書き込んだ数字を素因数分解するボットを作ってみた。
まずは、Slack用のボットを利用する準備をする。Slack APIにアクセスして、Bot usersをクリックし、Custom bot usersにあるcreating a new bot userのリンクからBotsのページを開く。 名前(ここでは @prime_decomposition)を入力したらAdd bot integrationボタンをクリックする。
Integration Settingsの画面ではAPI Tokenが表示されており、ボットのスクリプトでこれを利用する。あとは好みに応じてアイコンの変更やボットの説明を記述しておく。今回のボットでは、デフォルトのフェイスマークに用意してあるロボットのアイコンを使い、説明文は「素因数分解をするよ!」とした。あとは、Save Integrationのボタンをクリックして設定を保存する。
ボットの準備ができたら、それをSlackへメンバーとして加える。ボットを参加させたいチャンネル(ここの画像では #general だが #random に参加させたい場合は #random を選択すること)のChannel Settings(歯車のアイコン)から、Invite team members to join...を選択する。先ほど設定したボットがメンバーとして表示されているのでそれを選ぶ。これで下準備は完了だ。
次にPythonを使ってボット用スクリプトを作成する。目的は、Slack上で誰かが数字を書き込んだ際に、それを取得して素因数分解をすること。そのためには、Slackに書き込まれた内容を取得しなければならないが、そのためにslackclientというPythonのモジュールを利用する。
# pip install slackclient
pipであれば上記のコマンドでインストールが終わる。手動で導入する場合は、python-slackclientから入れる。
また、素因数分解プログラムをスクラッチから作るのも面倒なので、数論用ライブラリNZMATH(ニジマス)を利用する。これもpipでインストールできる。
# pip install nzmath
さて、ここまでできたらあとはコードを書くだけだ。Slackからデータを取得する場合は SlackClient.rtm_read() を、書き込みには SlackClient.rtm_send_message([channel, message]) を使う。これらを使うためには SlackClient.rtm_connect() で通信を確立するだけで良い。
以下に簡単な例を書いておく。
sc = SlackClient(TOKEN) if sc.rtm_connect(): r = sc.rtm_read() print r sc.rtm_send_message(u"random", u"こんにちは")
こんな感じ。簡単。
今回のボットの場合、数字を取得してそれを素因数分解する。数字を取得すること自体は上に書いたとおり簡単なのだが、連続して数字が書かれた場合はそれを漏らさないためにキューに保存しておく必要がある。また、一定時間で解けない分解が難しい合成数などを取得した場合、タイムアウトを設定しなければならない。これらを実現するためにはスレッドを使って並列処理をする必要がある。
数字を取得したらキューに入れ、そのキューから別スレッドのworkerスレッドが処理を行う。workerスレッドはキューから数字を一つ取り出し、それの素因数分解を試みる。この時にタイムアウトを考慮するので、この素因数分解を行う関数についてもスレッドを利用する。そのスレッド内で時間を測り、計算が終了しないまま一定時間が過ぎたら処理を強制終了する。
実行方法はネットにつながっているPCから以下のコマンドを実行するだけ。
$ ./prime_decomposition_bot.py
そして、実際のSlack上の画面は以下のようになる。
今回書いたコードは以下の通り。今回は素因数分解をするボットだったが、これをベースに作り変えれば大抵のことはできると思う。
というわけで、単なるチャットツールとして使うSlackでは物足りなくなったので、Slackのボット(bot)をPythonで作ってみることにした。Hubotのようなボット用フレームワークも利用可能だが、SlackのAPIをガシガシ制御する感じにしたかったので、Slack Real Time Messaging APIをPythonから制御してSlackと連携した。今回は、書き込んだ数字を素因数分解するボットを作ってみた。
まずは、Slack用のボットを利用する準備をする。Slack APIにアクセスして、Bot usersをクリックし、Custom bot usersにあるcreating a new bot userのリンクからBotsのページを開く。 名前(ここでは @prime_decomposition)を入力したらAdd bot integrationボタンをクリックする。
Integration Settingsの画面ではAPI Tokenが表示されており、ボットのスクリプトでこれを利用する。あとは好みに応じてアイコンの変更やボットの説明を記述しておく。今回のボットでは、デフォルトのフェイスマークに用意してあるロボットのアイコンを使い、説明文は「素因数分解をするよ!」とした。あとは、Save Integrationのボタンをクリックして設定を保存する。
ボットの準備ができたら、それをSlackへメンバーとして加える。ボットを参加させたいチャンネル(ここの画像では #general だが #random に参加させたい場合は #random を選択すること)のChannel Settings(歯車のアイコン)から、Invite team members to join...を選択する。先ほど設定したボットがメンバーとして表示されているのでそれを選ぶ。これで下準備は完了だ。
次にPythonを使ってボット用スクリプトを作成する。目的は、Slack上で誰かが数字を書き込んだ際に、それを取得して素因数分解をすること。そのためには、Slackに書き込まれた内容を取得しなければならないが、そのためにslackclientというPythonのモジュールを利用する。
# pip install slackclient
pipであれば上記のコマンドでインストールが終わる。手動で導入する場合は、python-slackclientから入れる。
また、素因数分解プログラムをスクラッチから作るのも面倒なので、数論用ライブラリNZMATH(ニジマス)を利用する。これもpipでインストールできる。
# pip install nzmath
さて、ここまでできたらあとはコードを書くだけだ。Slackからデータを取得する場合は SlackClient.rtm_read() を、書き込みには SlackClient.rtm_send_message([channel, message]) を使う。これらを使うためには SlackClient.rtm_connect() で通信を確立するだけで良い。
以下に簡単な例を書いておく。
sc = SlackClient(TOKEN) if sc.rtm_connect(): r = sc.rtm_read() print r sc.rtm_send_message(u"random", u"こんにちは")
こんな感じ。簡単。
今回のボットの場合、数字を取得してそれを素因数分解する。数字を取得すること自体は上に書いたとおり簡単なのだが、連続して数字が書かれた場合はそれを漏らさないためにキューに保存しておく必要がある。また、一定時間で解けない分解が難しい合成数などを取得した場合、タイムアウトを設定しなければならない。これらを実現するためにはスレッドを使って並列処理をする必要がある。
数字を取得したらキューに入れ、そのキューから別スレッドのworkerスレッドが処理を行う。workerスレッドはキューから数字を一つ取り出し、それの素因数分解を試みる。この時にタイムアウトを考慮するので、この素因数分解を行う関数についてもスレッドを利用する。そのスレッド内で時間を測り、計算が終了しないまま一定時間が過ぎたら処理を強制終了する。
実行方法はネットにつながっているPCから以下のコマンドを実行するだけ。
$ ./prime_decomposition_bot.py
そして、実際のSlack上の画面は以下のようになる。
今回書いたコードは以下の通り。今回は素因数分解をするボットだったが、これをベースに作り変えれば大抵のことはできると思う。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
import sys | |
import time | |
import json | |
import time | |
import threading | |
import datetime | |
import Queue | |
import nzmath.factor.methods as methods | |
from slackclient import SlackClient | |
TOKEN = "xoxb-00000000000-XXXXXXXXXXXXXXXXXXXXXXXX" # Your API token | |
CHANNEL = "random" | |
CALC_TIMEOUT = 60 | |
MAX_NUMBER = 10**60 | |
WAIT_TIME = 1 | |
NUM_WORKERS = 4 | |
NUM_TRY = 10 | |
sc = None | |
q = Queue.Queue() | |
def factor(number): | |
result = Queue.Queue() | |
def target(number, result): | |
result.put(methods.factor(number)) | |
t = threading.Thread(target=target, args=(number, result)) | |
t.start() | |
t.join(CALC_TIMEOUT) | |
return [] if result.empty() else result.get() | |
def worker(): | |
global sc | |
while True: | |
number = q.get() | |
if number < 2 or not sc: | |
continue | |
if number > MAX_NUMBER: | |
text = u"%d は大きすぎるので、もう少し小さい数にしてもらえませんか?" % number | |
sc.rtm_send_message(CHANNEL, text) | |
continue | |
factors = [n for (n, m) in factor(number) for i in range(m)] | |
text = None | |
if len(factors) == 0: | |
text = u"頑張って計算したんですけど %d を素因数分解できませんでした… :pensive:" % number | |
elif len(factors) == 1: | |
text = u"%d は素数です!" % number | |
else: | |
text = u"%d は %s に素因数分解されます!" % (number, u" と ".join(map(str, factors))) | |
if text: | |
sc.rtm_send_message(CHANNEL, text) | |
def main(args): | |
global sc | |
for i in range(NUM_WORKERS): | |
t = threading.Thread(target=worker) | |
t.daemon = True | |
t.start() | |
for n in range(NUM_TRY): | |
sc = SlackClient(TOKEN) | |
if sc.rtm_connect(): | |
while True: | |
try: | |
records = sc.rtm_read() | |
except: | |
print "接続が切断されました。再接続します。試行回数: %d" % (n + 1) | |
break | |
for record in records: | |
if "text" in record: | |
text = record["text"] | |
### for log | |
print datetime.datetime.fromtimestamp(float(record["ts"])).strftime("%Y-%m-%dT%H:%M:%S") if "ts" in record else "_" * 17, | |
print record["user"] if "user" in record else "_________", | |
print text.encode("utf-8") | |
sys.stdout.flush() | |
if text.isdigit(): | |
try: | |
q.put(int(text)) | |
except: | |
pass | |
time.sleep(WAIT_TIME) | |
else: | |
print "接続に失敗しました。TOKENが間違っていませんか?" | |
time.sleep(60) | |
if __name__ == "__main__": | |
main(sys.argv) |
コメント
the single > that makes a quote line, and the >>> that makes all lines as single quote :)