怒Mは思いつきでモノを言う

やったことメモなどなど

続・Python3の勉強がてらSlackbotを作ってみた

オリジナル

続・Python3の勉強がてらSlackbotを作ってみた - Qiita

Qiitaからの移行記事です。

はじめに

前回の「Python3の勉強がてらSlackbotを使ってみた」から、 機能を改善したり追加したりしました。

仕様

  1. ぐるなびAPIを利用して、slackで検索ワードを入力してヒットしたURLを返します。 「ご飯 品川 焼き鳥」と打つと、品川の焼き鳥屋っぽい店のURLを返します。 場所のキーワードで検索する際に住所で検索していたため、実際のイメージと違う場所の結果が返ってくるので、エリアマスタを使った検索に変更。

  2. ぐるなびAPIを利用した、店名検索です。 「お店 品川 笑笑」と打つと、品川の笑笑のURLを返します。 単純に店名検索です。ただ、間に空白が入るケースには対応できていませんw

  3. YahooのジオコーダAPIとスタティックマップAPIを利用して、雨雲レーダーの画像を返します。 「雨 品川」と打つと、品川付近の雨雲付き地図画像を返します。地図画像を返す方法はSlackのAPI files.uploadを使います。

環境などなど

画像ファイルを扱うために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_objJpgImageFileオブジェクトだったので、fileオブジェクトと同じだろう、思い込んだのが敗因でしょうか。PngImageFileにしたり、image_obj = BytesIO(staticmap_api.response_data.content)としてから、getvalue()getbuffer()を使ってみたりして3日くらい悩みましたw

スクリーンショット 2017-06-01 23.27.30.png

終わりに

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')
    })

と修正して動かしたところ、動きました! なぜ、今ままで動かなかったのか。 何はともあれ、ありがとうございました!