LoginSignup
4
16

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-06-01

はじめに

前回の「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
もう少しキレイに書けると思っています。

実装

今回は変更分のみです。
前回と違って、仕様ごとの解説です。

お店検索

slackbot_restapi.py
"""
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で入力された地名に合致する最初のエリアコードを返します。
エリアコードが取得できない場合は、これまで通り住所に対して検索することにします。
あとは前回と一緒です。

gnavapi.py
"""
ぐるなび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クラスに追加しました。

雨雲検索

slackbot_restapi.py
"""
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')
    })

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

4
16
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
16