Edited at

python知識ゼロからポケモンの名前でしりとりするslackbotを作ったノウハウのすべて


こんなのです

ソースはgithubで公開してますhttps://github.com/wagase/pokeshiri

よかったら「いいね」してください。


環境

OS:windows

言語:python


筆者について

趣味でプログラム書いてるにわか。

htmlとcssとjavascriptくらいはかける。

python知識ゼロ

Twitterフォローされると喜びます

https://twitter.com/wagase

よろしくお願いします


なぜpython?

話題だから


なぜslackbot?

開発経験ゼロだから気軽に作れるのがよかった


開発環境構築


知識ゼロだからまずpythonという言語の仕様を学ぶ

参考にしたサイト

https://www.pythonweb.jp/tutorial/


斜め読みしながらわからないところはググる


pythonをwindowsにインストール

https://www.python.org/

Python 3.6.5にしました

Download for windowsからpython-3.6.5.exeを取得して実行

Add Python to PATHチェックする

コンソールでHello pythonくらいはprintできた


Visual Studio Codeでpythonの設定をする

拡張機能「python」で検索して

Python

Python for VSCode

をインストール

pylintがないとかいわれるのでインストール

pip install pylint

これでいい感じに開発できるようになった


slackbotの作り方をググる

参考にしたもの

Pythonを使ったSlackBotの作成方法

PythonのslackbotライブラリでSlackボットを作る

↑この記事をみながら

pip3 install slackbotを実行

slackbotの登録

https://my.slack.com/services/new/bot

にいって書いてある項目を埋めるだけ

API トークンをコピーしておく


いざ実行

上記の参考サイトを丸パクリして

python run.py

をしてみる

Appのところにあるbotがアクティブ表示になった!

リプライすると「何言ってんだこいつ」って返ってくる!!

実行できた!


あとはゴリゴリプログラミングする

githubで公開中

https://github.com/wagase/pokeshiri

わからないところはググる


ポケモンのデータはこちらからお借りしました

全ポケモンのJSONデータ

https://github.com/kotofurumiya/pokemon_data/

ありがとうございます


ログ出力で参考にしたもの

Pythonでお手軽にかっこよくlogging


機能とか


ポケモンじゃないときは


ンで終わるポケモンはペナルティ



しりとりだから怒られる


しりとりになってないとペナルティ



slackのシステム的に1対Nのしりとりを想定してるため多少まちがっても

ゲームオーバーにはならない


一度登場したポケモンを言うとペナルティ


困ったときはヒントで答えてない一覧をだせる



もちろん一度言うとリストから消える


知らないポケモンがでたときに詳細表示機能で教えてもらえる



全ポケモンのJSONデータ作者さんに感謝


ランキング機能



登場回数順にランキングを表示。

ル攻めすると勝てるのでルチャブルは受け攻め強い


リセット機能



ただのリセット。ゲームリスタートの意味。

リセットするとペナルティと今まで言ったポケモンを忘れる。

リセットしてもランキングは消さない


ログ表示機能



今までの記録を教えてくれる


ソース

ソースはgithubで公開してます

https://github.com/wagase/pokeshiri

<追記 date=20180705>

この記事に下記にコピペしているソースは

特定のポケモンやある条件で発生するバグがあります

github上では見つけ次第修正していますがこの記事でも同じように修正するのは記事の趣旨とは異なるため

あえて初期のままにすることにしました

(初学者はこういう書き方をしてしまうとか反面教師にもなるかと思います)

</追記>


my_mention.py

# -*- coding: utf-8 -*-

from slackbot.bot import respond_to # @botname: で反応するデコーダ
from slackbot.bot import listen_to # チャネル内発言で反応するデコーダ
from slackbot.bot import default_reply # 該当する応答がない場合に反応するデコーダ
from libs import my_functions # 自作関数の読み込み
from libs import log

# 何回呼ばれたかカウントしたい
maincount = 0
resetcount = 0
hintcount = 0
detailcount = 0
rankingcount = 0
nomalcount = 0
errorcount = 0
notpokecount = 0

@respond_to(r'.+')
def mention_func(message):
global maincount
global resetcount
global hintcount
global detailcount
global rankingcount
global nomalcount
global errorcount
global notpokecount
maincount = maincount +1
req=message.body['text']
log.logger.info("["+str(maincount)+"] :総実行回数【"+str(req)+"】:受け取ったメッセージ")
if req == "リセット" or req == "reset":
resetcount = resetcount +1
my_functions.reset()
message.send("リセットしました")
elif req == "log" or req == "ログ" or req == "記録" :
message.send("["+str(maincount)+"] :総実行回数")
message.send("["+str(resetcount)+"] :総リセット回数")
message.send("["+str(hintcount)+"] :総ヒント回数")
message.send("["+str(detailcount)+"] :総詳細表示回数")
message.send("["+str(rankingcount)+"] :総ランキング表示回数")
message.send("["+str(notpokecount)+"] :総ポケモンじゃなくね?回数")
message.send("["+str(nomalcount)+"] :総しりとり成立回数")
message.send("["+str(errorcount)+"] :総しりとり不成立回数")
elif req == "ランキング" or req == "ranking" :
rankingcount = rankingcount +1
message.send(my_functions.remarkRanking())
elif req[:4] == "ヒント|" or req[:4] == "ヒント|" or req[:4] == "hint":
hintcount = hintcount +1
log.logger.info("["+str(hintcount)+"] :ヒント回数")
hint = my_functions.hint(req[4:5])
message.send(str(hint))
elif req[:3] == "詳細|" or req[:3] == "詳細|" :
detailcount = detailcount +1
log.logger.info("["+str(detailcount)+"] :詳細表示回数")
if my_functions.checkExistenceAllPoke(req[3:len(req)]) :
message.send(my_functions.getpokedetail(req[3:len(req)]))
else:
message.send("よくわかりませんでした"+req[3:len(req)])
else:
if my_functions.checkExistencePoke(req) :
my_functions.memoryRemark(req)
IsShiritoriOK = True
# すでに言ったことがあるかどうか
if my_functions.checkExistencereq(req) :
IsShiritoriOK = False
message.send(my_functions.countreqstock(req))
# しりとりになってるかどうか
if not my_functions.checkTruelastword(req) :
IsShiritoriOK = False
message.send(my_functions.forgivelastword(req))
if IsShiritoriOK :
nomalcount = nomalcount +1
log.logger.info("["+str(nomalcount)+"] :しりとり成立回数")
else :
errorcount = errorcount +1
log.logger.info("["+str(errorcount)+"] :しりとり不成立回数")
my_functions.reqstockappend(req)
ret = my_functions.shiritori(req)
log.logger.info("【"+str(ret)+"】:返答")
else :
notpokecount = notpokecount +1
ret = "ポケモンじゃなくね?"
log.logger.info("["+str(notpokecount)+"] :ポケモンじゃなくね?回数")
message.send(ret)



my_functions.py

# -*- coding: utf-8 -*-

import json
import random
import collections

def mid(text,s,e):
return text[s-1:s+e-1]

def left(text,e):
return text[:e]

def right(text,s):
return text[-s:]

# pokemon_data.jsonを読み取ってポケモンの名前だけにする
def getpokenamelist():
dic = {}
for key in POKEDATA:
if not key["no"] in dic.keys() :
dic[key["no"]]=key["name"]
return dic

# 辞書{'ア':['アーボ','アーボック'....],'イ':['イシツブテ','イワーク'....].....} の形にするが最後にンがつくものは除外
def makekanalistNotnn():
kanalist = {}
for i in range(1,len(KATAKANA)+1):
kanas = []
j = 1
for key in POKENAMELIST:
if left(POKENAMELIST[key],1) == mid(KATAKANA,i,1) and right(POKENAMELIST[key],1) != "ン" :
kanas.append(POKENAMELIST[key])
j = j +1
kanalist[mid(KATAKANA,i,1)] = kanas
return kanalist

# 辞書{'ア':['アーボ','アーボック'....],'イ':['イシツブテ','イワーク'....].....} の形にするが「ン」で終わるやつを取得
def makekanalistGetnn():
kanalist = {}
for i in range(1,len(KATAKANA)+1):
kanas = []
j = 1
for key in POKENAMELIST:
if left(POKENAMELIST[key],1) == mid(KATAKANA,i,1) and right(POKENAMELIST[key],1) == "ン" :
kanas.append(POKENAMELIST[key])
j = j +1
kanalist[mid(KATAKANA,i,1)] = kanas
return kanalist

# makekanalistNotnnのリストから指定した文字で始まるポケモンを適当に選ぶ
def pokechoice(kana):
val =""
if len(stock[kana]) == 0 :
if len(nstock[kana]) != 0 :
val = random.choice(nstock[kana])
memoryRemark(val)
delnstock(kana,val)
val= val + "・・・もう【"+kana+"】から始まるポケモンは答えられないよ。負けました。リセットしてね"
if penalty !=0 :
val= val +" ペナルティ合計は(" +str(penalty) +"回)でした"
else :
val = random.choice(stock[kana])
memorylastword(val)
reqstockappend(val)
delstock(kana,val)
rest = len(stock[kana])
memoryRemark(val)
val = val + "・・・【"+kana+"】のこり【"+str(rest)+"】" + "次のことばは【"+getshiri(val)+"】です"
return val

# そのポケモンがしりとりで存在するかどうか
def checkExistencePoke(req):
if req in POKENAMELIST.values():
return True
else :
return False

# そのポケモンがそもそも存在するかどうか
def checkExistenceAllPoke(req):
for key in POKEDATA:
if req == key["name"] :
return True
return False

# しりとりメソッド
def shiritori(req):
atama = left(req,1)
shiri = getshiri(req)
if shiri == "ン":
global penalty
penalty = penalty +1
return "「ン」で終わるやつはだめだよ ペナルティ(" +str(penalty) +"回)"
else :
if req in stock[atama]:
delstock(atama,req)
return pokechoice(shiri)

# 末尾の文字を調整する
def getshiri(req):
shiri = right(req,1)
# ミミッキュ対策
if shiri in "ァィゥェォッャュョヮヵヶ" :
shiri = shiri.replace("ァ","ア")
shiri = shiri.replace("ィ","イ")
shiri = shiri.replace("ゥ","ウ")
shiri = shiri.replace("ェ","エ")
shiri = shiri.replace("ォ","オ")
shiri = shiri.replace("ッ","ツ")
shiri = shiri.replace("ャ","ヤ")
shiri = shiri.replace("ュ","ユ")
shiri = shiri.replace("ョ","ヨ")
shiri = shiri.replace("ヮ","ワ")
shiri = shiri.replace("ヵ","カ")
shiri = shiri.replace("ヶ","ケ")
# 長音対策
if shiri == "ー" :
shiri = mid(req,len(req)-1,1)
return shiri

# 一度いったやつはストックから消す
def delstock(kana,val):
stock[kana].remove(val)

# 一度いったやつはストックから消す
def delnstock(kana,val):
nstock[kana].remove(val)

# 一度言われたやつを覚える
def reqstockappend(req):
reqstock.append(req)

# 一度言われたことがあるかどうかしらべる
def checkExistencereq(req):
if req in reqstock:
return True
else:
return False

# 何回言われてるか調べて返す
def countreqstock(req):
global penalty
penalty = penalty +1
return req + "は【" + str(reqstock.count(req)+1) + "】回目だよ。できれば違うやつ言ってね ペナルティ(" +str(penalty) +"回)"

# リセット
def reset():
global stock
global nstock
global lastWord
global reqstock
global penalty
penalty = 0
lastWord =""
stock = makekanalistNotnn()
nstock = makekanalistGetnn()
reqstock.clear()

# ヒント
def hint(req):
global penalty
if req in stock:
penalty = penalty + 1
return stock[req]
else:
return "カタカナ一文字でお願いします"

# 詳細機能
def getpokedetail(req):
ret = ""
for key in POKEDATA:
if key["name"] == req :
ret = ret + str(key) + "\n"
return ret

# 最後の文字を覚える
def memorylastword(req):
global lastWord
lastWord = getshiri(req)

# しりとりになってるか調べる
def checkTruelastword(req):
if lastWord != left(req,1) and not lastWord=="":
return False
else :
return True

# しりとりになってないメッセージ
def forgivelastword(req):
global penalty
penalty = penalty + 1
return req + "はしりとりになってないよ。できれば【"+lastWord+"】から始まるやつ言ってほしかったな ペナルティ(" +str(penalty) +"回)"

# 呼ばれたものを記憶
def memoryRemark(req):
remarkstock.append(req)

# ランキングカウント
def remarkRanking():
ret =""
i = 0
c = collections.Counter(remarkstock)
for item in c.most_common() :
i=i+1
if i > 5 :
break
ret = ret + str(item[0]) + " " + str(item[1]) + "回"+ "\n"
return ret

# 定数群
KATAKANA = "アイウエオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモヤユヨラリルレロワヲンヴ"
POKEDATA = json.load(open("data/pokemon_data.json","r",encoding="utf-8"))
POKENAMELIST = getpokenamelist()

# 変数群
stock = makekanalistNotnn()
nstock = makekanalistGetnn()
remarkstock=[]
reqstock =[]
lastWord =""
penalty =0



log.py

import logging

logger = logging.getLogger(__name__)
_detail_formatting = '[%(asctime)s] %(module)s.%(funcName)s %(levelname)s -> %(message)s'

logging.basicConfig(
level=logging.DEBUG,
format=_detail_formatting, # 出力のformatも変えられる
filename="./pokeshiri.log", # logファイルのありか
)



その他


ニドラン♀とかニドラン♂が入力できない問題

<追記 date=20180705>

slackの仕様なのか♀が「:女性のマーク:」っていう絵文字になるんですけど(困惑)



だれか解決方法知ってたら教えてください(小声)

</追記>

<追記 date=20190819>

↑これslack公式に問い合わせたところ♀が勝手に:女性のマーク:になるのは仕様で

設定等で勝手に変わらないようには現状できないとのことでした。

要望は出しておきました。

</追記>


おまけ 


Amazon linux (EC2 t2.micro)での実行方法 無料

pythonとpipとslackbotをインストール

sudo git clone https://github.com/yyuu/pyenv.git /usr/bin/.pyenv

cd /usr/bin/.pyenv
pyenv install 3.6.5
pyenv global 3.6.5
sudo apt-get install python3-pip
sudo yum install -y python36u-pip
pip3 install slackbot

上記はpythonにわかが2018/07頃に実行したhistoryです。

正確な情報は自分で調べることをおすすめします。

pokeshiriのモジュールを適当なところにおいて

nohup python -u run.py >out.log

でサーバーで実行し続けてくれます。

止めたいときは

ps -C python

kill 番号

でいけます


Herokuで実行する方法 無料

Herokuのアカウントを作る

https://signup.heroku.com/login

HerokuCLIをインストールしてパスを通す

https://devcenter.heroku.com/articles/getting-started-with-python#set-up

パスはC:\Program Files\heroku\bin

ここでいいはず

herokuコマンドでherokuのgitに上げる

heroku login

heroku create {名前}
heroku git:clone -a {名前}
cd {名前}

作業フォルダ(名前つけたやつ)にpokeshiriのモジュールをおいて

git add .

git commit -am "init"
git push heroku master

これでpythonが動く環境ができて勝手にデプロイしてくれる

なお外部依存のモジュールは

requirements.txtに書く必要があるのでrootにコミットしておく


requirements.txt

slackbot==0.5.3


実行するには

heroku run nohup python -u run.py >out.log

止めるには

heroku ps

heroku kill {実行名}

でいけました


あとがき

python知識ゼロからポケモンの名前でしりとりするslackbotを作ったノウハウのすべてでした

pythonは本当に学習コストが少ないと思いました

JSONの読み込みとかリストの並び替えとか辞書の扱い方とかググればすぐにサンプルコードがでてきます。

またググればでてくるポケモンデータJSONのすごさにちょっと感動

ポケモンデータJSON作者様にこのうえない謝辞をおくります。

以上

ありがとうございました。

よかったら「いいね」してください。

Twitterフォローされると喜びます

https://twitter.com/wagase

よろしくお願いします