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

やったことメモなどなど

Vagrant のインストール・設定

オリジナル

Vagrant のインストール・設定 - Qiita

Qiitaからの移行記事です。

はじめに

実践 Vagrantを読みつつ、忘れないようにメモ。 本ではバージョンが1.0系だが、2.0系に読み替えて進めている。

環境

macOS Mojave 10.14.3 Vagrant 2.2.3 VirtualBox 6.0.4

VirtualBoxのダウンロードとインストール

Download VirtualBoxからダウンロードしてインストールするか、Homebrewでダウンロード・インストールするかです。

brew cask install virtualbox

Vagrantのダウンロードとインストール

Download Vagrantからダウンロード。 ダウンロードしたdmgファイルをクリックしてインストール。

Vagrantで使うベースイメージを検索

Discover Vagrant Boxesで使いたいBoxを検索。

image.png

vagrant initのコマンドが記載してあるので、その内容をホストで実行。

image.png

host $ vagrant init centos/7
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.
host $ ls -l
total 8
drwxr-xr-x  3 hoge  staff    96  2 25 00:49 .
drwxr-xr-x  3 hoge  staff    96  2 25 00:48 ..
-rw-r--r--  1 hoge  staff  3015  2 25 00:49 Vagrantfile

よく使うVagrantコマンド

コマンド 内容
vagrant init [box name] [box name]で指定したイメージで初期のVagrantfileを生成する。
vagrant up 仮想マシンの起動。
vagrant halt 仮想マシンの停止。
vagrant ssh 仮想マシンへのssh接続。
vagrant reload 仮想マシンの再起動。Vagrantfileの変更反映によく使う。
vagrant destory 仮想マシンの破棄。
vagrant status 仮想マシンの状態を確認する。

Vagrantfile

vagrant initで作成されるVagrantfileの中身は以下のようになっている。 ※コメントは割愛 VagrantfileはRubyで書かれている。

Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"
end

このファイルに仮想マシンに対する設定を書き込むことでカスタマイズができる。

ネットワーク設定(簡単)

プライベートアドレスを設定する

Vagrantfileを編集する。

 # Create a private network, which allows host-only access to the machine  
 # using a specific IP.                                                    
 config.vm.network "private_network", ip: "192.168.33.10"                   

Vagrantfileを編集したら、vagrant reloadする。

ホストから仮想マシンへアクセスできるようになる。

host $ ping 192.168.33.10
PING 192.168.33.10 (192.168.33.10): 56 data bytes
64 bytes from 192.168.33.10: icmp_seq=0 ttl=64 time=0.529 ms
64 bytes from 192.168.33.10: icmp_seq=1 ttl=64 time=0.393 ms
64 bytes from 192.168.33.10: icmp_seq=2 ttl=64 time=0.478 ms
64 bytes from 192.168.33.10: icmp_seq=3 ttl=64 time=0.503 ms
64 bytes from 192.168.33.10: icmp_seq=4 ttl=64 time=0.408 ms
64 bytes from 192.168.33.10: icmp_seq=5 ttl=64 time=0.421 ms
--- 192.168.33.10 ping statistics ---

仮想マシンのポートをホストにフォワード

Vagrantfileを編集する。

 # accessing "localhost:8080" will access port 80 on the guest machine.    
 # NOTE: This will enable public access to the opened port                 
 config.vm.network "forwarded_port", guest: 80, host: 9090                

vagrant reload して仮想マシンでWebサーバを立ち上げてアクセスする。

host $ vagrant ssh
[vagrant@localhost ~]$ sudo python -m SimpleHTTPServer 80
Serving HTTP on 0.0.0.0 port 80 ...

ホストからアクセスして表示されれば成功。

image.png

共有ファイルシステム

設定

Vagrantfileでローカルと仮想マシンに共有ディレクトリのセットアップをすることができる。 以下の内容だと、ローカルディレクト"./test_dir"仮想マシン"/home/vagrant/test_dir_vagrant"を共有し、仮想マシン側に指定したディレクトリがなければ作成し、所有者、グループを"vagrant"に設定する。

  # Share an additional folder to the guest VM. The first argument is       
  # the path on the host to the actual folder. The second argument is       
  # the path on the guest to mount the folder. And the optional third       
  # argument is a set of non-required options.                              
  config.vm.synced_folder "./test_dir", "/home/vagrant/test_dir_vagrant",  
        create: true, owner: "vagrant", group: "vagrant"                     

詳細は公式ドキュメントを読んでいただきたく。 https://www.vagrantup.com/docs/synced-folders/basic_usage.html

config.vm.synced_folder "host_path", "guest_path", option ...

共有ディレクトリの設定をした際に発生した問題

Vagrant was unable to mount VirtualBox shared folders. This is usually
because the filesystem "vboxsf" is not available. This filesystem is
made available via the VirtualBox Guest Additions and kernel module.
Please verify that these guest additions are properly installed in the
guest. This is not a bug in Vagrant and is usually caused by a faulty
Vagrant box. For context, the command attempted was:

mount -t vboxsf -o uid=1000,gid=1000 home_vagrant_test_dir_vagrant /home/vagrant/test_dir_vagrant

The error output from the command was:

mount: unknown filesystem type 'vboxsf'

Vagrantのpluginをインストールすることで解決できる。

vagrant-vbguest

VirtualBox Guest Additionsを仮想マシンに自動的にインストールするVagrant plugin。

host $ vagrant plugin install vagrant-vbguest
Installing the 'vagrant-vbguest' plugin. This can take a few minutes...
Fetching: micromachine-2.0.0.gem (100%)
Fetching: vagrant-vbguest-0.17.2.gem (100%)
Installed the plugin 'vagrant-vbguest (0.17.2)'!

host $ vagrant vbguest
(色々動きます)
irtualBox Guest Additions: Starting.
Redirecting to /bin/systemctl start vboxadd.service
Redirecting to /bin/systemctl start vboxadd-service.service
Unmounting Virtualbox Guest Additions ISO from: /mnt

host $ vagrant vbguest --status
[default] GuestAdditions 6.0.4 running --- OK.

上記を実行したあと、vagrant reload すると解決した。

参考

Vagrant + VirtualBoxでWindows上に開発環境をサクッと構築する Vagrant | synced_folder でホストOSとゲストOSの任意のフォルダを同期する

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

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

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

オリジナル

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

Qiitaからの移行記事です。

はじめに

タイトル通りですが、Pythonの勉強がてらSlack用のBotを作ってみました。 他の方々が記事で書いているので、自分の備忘で残すレベルになります。 Slackの登録方法、Botを使い始めるまで、Herokuの使い方は記載していません。 ソースコードを載せているので、ソースコードの書き方などでお作法的に間違っていることがあれば、指摘してください。

仕様

ぐるなびAPIを利用して、slackで検索ワードを入力してヒットしたURLを返します。 「ご飯 品川 焼き鳥」と打つと、品川の焼き鳥屋っぽい店のURLを返します。

環境などなど

構成

slackbot/  ├ plugins/  │ └ slackbot_restapi.py  │ └ restapi.py  │ └ gnaviapi.py  │   └ run.py  └ slackbot_settings.py  └ Procfile(Heroku用ファイル)  └ runtime.txt(Heroku用ファイル)

実装

run.py と slackbot_settings.py

"""Slack Bot Program."""
# coding: utf-8

from slackbot.bot import Bot

def main():
    """
    Slackbot
    """
    bot = Bot()
    bot.run()

if __name__ == '__main__':
    main()
"""
Configuration file for slackbot
"""
API_TOKEN = 'YOUR_API_TOKEN'

DEFAULT_REPLY = '何言ってんの?'

PLUGINS = ['plugins']

ここに書いてある通りです。 run.pyを実行すれば、Slackbotは動き出します。

slackbot_restapi.py

"""
Plugin Program
"""
from requests.exceptions import RequestException
from slackbot.bot import listen_to
from plugins.gnaviapi import GnaviApi

@listen_to('ご飯')
def search_restraunt(message):
    """
        受信メッセージを元にぐるなびを検索してURLを返す
    """
    gnavi = GnaviApi('https://api.gnavi.co.jp/RestSearchAPI/20150630/')
    key = 'YOUR_API_KEY'

    search_word = message.body['text'].split()

    if len(search_word) == 3:
        params = {
            'keyid': key,
            'format': 'json',
            'address': search_word[1],
            'freeword': search_word[2]
        }
        try:
            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('例)ご飯 品川 焼き鳥')

Slackで打ち込まれた内容を拾って処理します。 ここで微妙にハマったのは、Slackで入力されたMessageの拾い方がわからなかったことです。 ちょいちょい調べたところ、

message.body['text']

で取得できることがわかりました。 拾ったメッセージをsplit()で分割して、場所とフリーワードをAPIのパラメータとして使います。

restapi.py と gnaviapi.py

Pythonのクラスと継承の勉強で作ってみました。 restapi.pyでは、Requestを投げてResponseを持っとくだけのクラスです。 gnaviapi.pyでは、ResponseからURLのみのリストを作成して返すメソッドを追加しています。 リスト内包表記って便利ですよねー。なんか新鮮でした。

"""
REST API CLASS
"""
# -*- coding: utf-8 -*-
import requests
from requests.exceptions import RequestException

class RestApi():
    """
    REST API CLASS
    """
    def __init__(self, url):
        self.url = url
        self.response_data = None

    def api_request(self, search_dict):
        """
        API呼び出し
        """
        try:
            self.response_data = requests.get(self.url, params=search_dict)
        except RequestException:
            raise Exception('APIアクセスに失敗しました')
"""
ぐるなびAPI
"""
# -*- coding: utf-8 -*-
from plugins.restapi import RestApi

class GnaviApi(RestApi):
    """
    ぐるなびAPI用クラス
    """
    def __init__(self, url):
        super().__init__(url)

    def url_list(self):
        """
        ResponseからレストランURLのリストを作って返す。
        """
        json_data = self.response_data.json()
        if 'error' in json_data:
            raise Exception('そのキーワードじゃ見つかんなかった・・・(´・ω・`)')

        return [rest_data['url'] for rest_data in json_data['rest']]

実行結果

こんな感じです。

スクリーンショット 2017-05-16 1.45.09.png

終わりに

ATNDやdots.など他のAPIで検索できるように、拡張は簡単にできそうです。 pythonで実装するよりも、Herokuの使い方に四苦八苦していた時間の方が長かった気がしますw