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

#はじめに

前回の「[Python3の勉強がてらSlackbotを使ってみた](http://qiita.com/do_m_gatoru/items/d6dbb5e5dbca8d96a1cd)」から、
機能を改善したり追加したりしました。

#仕様

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

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

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

#環境などなど

* [Slack](https://slack.com)
* [Heroku](https://www.heroku.com)
* Python3.6.0
* [lins05/slackbot](https://github.com/lins05/slackbot)
* [python-pillow/Pillow](https://github.com/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
もう少しキレイに書けると思っています。

#実装

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

##お店検索

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

```py3: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クラスに追加しました。


##雨雲検索

```py3: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')`を送れば行くだろうと思っていたのですが、全くダメ。
色々試した結果、一度実ファイルを保存してからk`open()`で読み込んで、そのデータを送ることで成功しました。
Herokuでは`/tmp`配下であればファイルの保存ができるようなので、`image_obj.save('/tmp/weather.jpg')`と保存してから読み込み直しました。

`image_obj`は`JpgImageFile`オブジェクトだったので、`file`オブジェクトと同じだろう、思い込んだのが敗因でしょうか。`PngImageFile`にしたり、`image_obj = BytesIO(staticmap_api.response_data.content)`としてから、`getvalue()`や`getbuffer()`を使ってみたりして３日くらい悩みましたw

<img width="614" alt="スクリーンショット 2017-06-01 23.27.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/99926/5d71bc04-aefd-02f5-c4f6-8ee1ebb077fd.png">


#終わりに

`requests.post()`の仕様を調べていますが、なぜ`JpgImageFile`が送れないのかは原因がわかっていません。引き続き調べますが、どなたか知っている方がいれば情報ください。

#追記(20170701)

コメントをいただきまして、

```py3:変更前
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')})
```

この箇所を、

```py3:変更後
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')
    })
```

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