続・Python3の勉強がてらSlackbotを作ってみた
オリジナル
続・Python3の勉強がてらSlackbotを作ってみた - Qiita
Qiitaからの移行記事です。
はじめに
前回の「Python3の勉強がてらSlackbotを使ってみた」から、 機能を改善したり追加したりしました。
仕様
ぐるなびAPIを利用して、slackで検索ワードを入力してヒットしたURLを返します。 「ご飯 品川 焼き鳥」と打つと、品川の焼き鳥屋っぽい店のURLを返します。 場所のキーワードで検索する際に住所で検索していたため、実際のイメージと違う場所の結果が返ってくるので、エリアマスタを使った検索に変更。
ぐるなびAPIを利用した、店名検索です。 「お店 品川 笑笑」と打つと、品川の笑笑のURLを返します。 単純に店名検索です。ただ、間に空白が入るケースには対応できていませんw
YahooのジオコーダAPIとスタティックマップAPIを利用して、雨雲レーダーの画像を返します。 「雨 品川」と打つと、品川付近の雨雲付き地図画像を返します。地図画像を返す方法はSlackのAPI
files.upload
を使います。
環境などなど
- Slack
- Heroku
- Python3.6.0
- lins05/slackbot
- python-pillow/Pillow
画像ファイルを扱うためにPillowを使いました。
構成
slackbot/ ├ plugins/ │ └ slackbot_restapi.py │ └ restapi.py │ └ gnaviapi.py │ └ run.py └ slackbot_settings.py └ Procfile(Heroku用ファイル) └ runtime.txt(Heroku用ファイル)
特に変わっていません。 gnaviapi.pyとslackbot_restapi.pyを変更しています。 ただし、クラス化とかなんとか整理は仕切れていませんw もう少しキレイに書けると思っています。
実装
今回は変更分のみです。 前回と違って、仕様ごとの解説です。
お店検索
""" Plugin Program """ from io import BytesIO import requests from requests.exceptions import RequestException from PIL import Image from slackbot.bot import listen_to from plugins.restapi import RestApi from plugins.gnaviapi import GnaviApi import slackbot_settings @listen_to('ご飯') @listen_to('お店') def search_restraunt(message): """ 受信メッセージを元にぐるなびを検索してURLを返す。 場所:エリアMマスタコード(areacode_m) or 住所(address) キーワード:フリーワード(freeword) or 店舗名(name) """ url = 'https://api.gnavi.co.jp/RestSearchAPI/20150630/' key = 'YOUR_GNAVI_API_TOKEN' gnavi = GnaviApi(url, key) search_word = message.body['text'].split() if len(search_word) >= 3: try: params = gnavi.create_params(search_word) gnavi.garea_middle_fech() search_area = gnavi.garea_middle_search(search_word[1]) if len(search_area) == 0: search_area = {'address': search_word[1]} params.update(search_area) gnavi.api_request(params) for rest_url in gnavi.url_list(): message.send(rest_url) except RequestException: message.send('ぐるなびに繋がんなかったから、後でまた探してくれ・・・( ´Д`)y━・~~') return except Exception as other: message.send(''.join(other.args)) return else: message.send('↓こんな感じで検索してほしい・・・( ̄Д ̄)ノ') message.send('ご飯 場所 キーワード(文字はスペース区切り)') message.send('例)ご飯 品川 焼き鳥')
params = gnavi.create_params(search_word)
で、「ご飯」か「お店」を判定して、APIに投げるパラメータをフリーワードか店舗名に切り替えています。
garea_middle_fech()
でぐるなびのエリアMマスタを検索してエリアコードを取得します。
garea_middle_search(search_word[1])
では、Slackで入力された地名に合致する最初のエリアコードを返します。
エリアコードが取得できない場合は、これまで通り住所に対して検索することにします。
あとは前回と一緒です。
""" ぐるなびAPI """ # -*- coding: utf-8 -*- from requests.exceptions import RequestException from plugins.restapi import RestApi class GnaviApi(RestApi): """ ぐるなびAPI用クラス """ def __init__(self, url, key): super().__init__(url) self.key = key self.garea_s = None def create_params(self, search_word): """ Slackで入力されたキーワードにより、APIのパラメータを変える。 """ params = { 'format': 'json' } if search_word[0] == 'ご飯': params['freeword'] = search_word[2] elif search_word[0] == 'お店': params['name'] = search_word[2] return params def url_list(self): """ ResponseからレストランURLのリストを作って返す。 """ json_data = self.response_data.json() if 'error' in json_data: raise Exception('そのキーワードじゃ見つかんなかった・・・(´・ω・`)') if json_data['total_hit_count'] == '1': return [(json_data['rest'])['url']] else: return [rest_data['url'] for rest_data in json_data['rest']] def garea_middle_fech(self): """ ぐるなびAPIからエリアMマスタを取得する。 """ garea = RestApi('https://api.gnavi.co.jp/master/GAreaMiddleSearchAPI/20150630/') params = { 'keyid': self.key, 'format': 'json', 'lang': 'ja' } try: garea.api_request(params) self.garea_s = garea.response_data.json() if 'error' in self.garea_s: raise Exception('その場所知らない・・・(´・ω・`)') except RequestException: raise RequestException() def garea_middle_search(self, area_name): """ エリアMマスタ内から、area_nameに一致する値を取得する。 (完全一致だと厳しいので、部分一致。) """ result_dict = {} for area_s in self.garea_s['garea_middle']: if area_s['areaname_m'].find(area_name) >= 0: result_dict = {'areacode_m': area_s['areacode_m']} break return result_dict
↑エリアマスタを探すメソッドをぐるなびAPIクラスに追加しました。
雨雲検索
""" Plugin Program """ from io import BytesIO import requests from requests.exceptions import RequestException from PIL import Image from slackbot.bot import listen_to from plugins.restapi import RestApi from plugins.gnaviapi import GnaviApi import slackbot_settings def search_restraunt(message): """ 省略!!! """ @listen_to('雨') def search_weather(message): """ 受信メッセージを元にジオコーダAPIから緯度経度を取得する。 緯度経度を中心に元にスタティックマップAPIから雨雲レーダーの画像を返す。 場所:住所(query) """ url_geocoder = 'https://map.yahooapis.jp/geocode/V1/geoCoder' url_staticmap = 'https://map.yahooapis.jp/map/V1/static' key_yahoo = 'YOUR_YAHOO_API_TOKEN' url_slackapi = 'https://slack.com/api/files.upload' geocoder_api = RestApi(url_geocoder) staticmap_api = RestApi(url_staticmap) search_word = message.body['text'].split() try: geocoder_api_params = { 'appid': key_yahoo, 'query': search_word[1], 'output': 'json' } geocoder_api.api_request(geocoder_api_params) geocoder_json = geocoder_api.response_data.json() if 'Error' in geocoder_json: raise Exception('その場所知らない・・・(´・ω・`)') coordinates = (((geocoder_json['Feature'])[0])['Geometry'])['Coordinates'] staticmap_api_params = { 'appid': key_yahoo, 'lon': (coordinates.split(','))[0], 'lat': (coordinates.split(','))[1], 'overlay': 'type:rainfall', 'output': 'jpg', 'z': '13' } staticmap_api.api_request(staticmap_api_params) slackapi_params = { 'token': slackbot_settings.API_TOKEN, 'channels': 'C5CJE5YBA' } image_obj = Image.open(BytesIO(staticmap_api.response_data.content), 'r') image_obj.save('/tmp/weather.jpg') with open('/tmp/weather.jpg', 'rb') as weatherfile: requests.post(url_slackapi, data=slackapi_params, files={ 'file': ('weather.jpg', weatherfile, 'image/jpeg')}) except Exception as other: message.send(''.join(other.args)) return
ぐるなびAPIと違い、エリアマスタは存在しないようなので住所をベースに緯度経度を取得します。 緯度経度を入手したら、あとは簡単です。画像データの「入手」までは簡単でした。 ここまでは・・・
こっから、ドハマりしました。
どうしても画像データをSlackにアップロードできない、と悩みました。
最初はimage_obj = Image.open(BytesIO(staticmap_api.response_data.content), 'r')
を送れば行くだろうと思っていたのですが、全くダメ。
色々試した結果、一度実ファイルを保存してからkopen()
で読み込んで、そのデータを送ることで成功しました。
Herokuでは/tmp
配下であればファイルの保存ができるようなので、image_obj.save('/tmp/weather.jpg')
と保存してから読み込み直しました。
image_obj
はJpgImageFile
オブジェクトだったので、file
オブジェクトと同じだろう、思い込んだのが敗因でしょうか。PngImageFile
にしたり、image_obj = BytesIO(staticmap_api.response_data.content)
としてから、getvalue()
やgetbuffer()
を使ってみたりして3日くらい悩みましたw
終わりに
requests.post()
の仕様を調べていますが、なぜJpgImageFile
が送れないのかは原因がわかっていません。引き続き調べますが、どなたか知っている方がいれば情報ください。
追記(20170701)
コメントをいただきまして、
image_obj = Image.open(BytesIO(staticmap_api.response_data.content), 'r') image_obj.save('/tmp/weather.jpg') with open('/tmp/weather.jpg', 'rb') as weatherfile: requests.post(url_slackapi, data=slackapi_params, files={ 'file': ('weather.jpg', weatherfile, 'image/jpeg')})
この箇所を、
output = BytesIO() image_obj = Image.open(BytesIO(staticmap_api.response_data.content), 'r') image_obj.save(output, 'jpeg') requests.post(slackbot_settings.API_URL, data=slackapi_params, files={ 'file': ('weather.jpg', output.getvalue(), 'image/jpeg') })
と修正して動かしたところ、動きました! なぜ、今ままで動かなかったのか。 何はともあれ、ありがとうございました!