madokaのブログ

勉強したことのoutput先として使ってます。内容はpythonがらみが多いかもです。

Pythonをつかってドラゴンズの試合結果を取得する

ドラゴンズの試合状況をサクッと取得できるようにしたいということで、野球の試合状況について、試合の状況(1回表、試合前)や対戦相手、得点状況を取得するプログラムを組みました。

今回は下記のyahooスポーツから情報を取得することにしました。 baseball.yahoo.co.jp

必要なもの

  • python3.x
  • beautifulsoup4 (こちらは下記のコマンドで入れて下さい。)
$ pip install beautifulsoup4

BeatuifulSoupの使い方

from bs4 import BeautifulSoup


# web pageから情報を取ってくる
soup = BeautifulSoup(urlopen(url), "html.parser")

# fileから情報を取ってくる
with open("sample.html", "r") as f:
     soup = BeautifulSoup(f, "html.parser")

上記の処理にて対象のhtmlを操作する準備は整ったので、これを使って探索を行います。html.parserのところはparserを指定しています。lxmlの指定もできます。こちらの方が早いらしいです。

BeatuifulSoupのfind, find_all

soup.find('タグ名',id='id名',class_='クラス名',href='url',...)
# href='url'は属性の一例です

大体のものはこのfind、find_allで取って来れます。上記では引数を色々書きましたが、この中のどれかを引数にいれれば動きます。このfindで拾ってきたものについても、最初のBeatifulSoupで作ったもの同様にfind等を用いてその内部を取得することができます。


ここから下は実際にこのサイトのhtmlからどう情報を拾ってくるかの話になります。

yahoo スポーツ野球のurlについて

このsiteではhttps://baseball.yahoo.co.jp/npb/teams/team番号/scheduleというurlになっており、チーム番号を指定することで対象のチームに関する今月のスケジュールが見れるようになっています。

野球チームに関しては下記のようにナンバリングされてました。

{'巨人': 1, 
 'ヤクルト': 2,
 'ロッテ': 9,
 '日本ハム': 8,
 '阪神': 5,
 '中日': 4,
 '西武': 7,
 'オリックス': 11,
 '楽天': 376,
 '広島': 6,
 'ソフトバンク': 12,
 'DeNA': 3}

また、年月についての指定は、上記のurlにクエリをのせることで指定ができました。 https://baseball.yahoo.co.jp/npb/teams/team番号/schedule?date=201905 このように年月を西暦の4桁と月の2桁からなる6桁の数字をdateに入れるようになってました。

今回扱うhtmlについて

全体のhtmlについては長すぎるので、 スポーツナビ - プロ野球 -中日ドラゴンズ - 日程・結果 からDeveloperモードで見て下さい。Macだと option + command + iで出てきます。ここでは対象の日付の情報が書かれた要素だけ載せます。

<td class="gameday home active">
    <table>
        <tbody>
        <tr>
            <td class="game">
                <a href="https://baseball.yahoo.co.jp/npb/game/2019042703/" data-ylk="slk:result;pos:27"
                   data-rapid_p="46">
                    <em class="day">27</em>
                    <dl>
                        <dt>
                            vs.<span class="team pm T"><i>阪神</i></span>
                        </dt>
                        <dd class="yjMS">
                            <span class="score">
                            <strong>5</strong> - 4
                            </span>
                            <span class="status">
                            結果
                            </span>
                        </dd>
                    </dl>
                </a>
            </td>
        </tr>
        <tr>
            <td class="data"><p class="yjXS">ナゴヤドーム</p>
                <ul class="yjSt">
                    <li>勝:<a href="/npb/player/1300063/" data-ylk="slk:win_pit;pos:27" data-rapid_p="47">又吉</a></li>
                    <li>S:<a href="/npb/player/1700101/" data-ylk="slk:sav_pit;pos:27" data-rapid_p="48">鈴木博</a></li>
                </ul>
            </td>
        </tr>
        </tbody>
    </table>
</td>

対象の日付の情報が書かれた要素を取り出すために

<td>で一日ずつの内容が書かれているのでまずはtdの要素一覧を取得します。 そして今日の日付(27日)がかかれた要素<em>の内部のtextと一致するかどうかを見て、一致すればその要素が今日の要素であると判断します。なので、ここでは<td>をfind_allで全件取得をしそれぞれの要素の<em>をfindで取り出して、findの中身が対象の日付であるかどうかをcheckして取り出すようにしました。

from urllib.parse import urlencode
from bs4 import BeautifulSoup


def __get_date_elements(self, _date):
        tds = BeautifulSoup(urlopen(self.url), "html.parser").find_all("td")
        for td in tds:
            em = td.find("em")
            if em is not None:
                if em.text == _date.strftime("%d"):
                    return td

試合があるかどうか

tdのclassにgamedayとあれば試合がある日なようです。一方試合がない日はfillinとなっていました。

def __have_game(self, _date):
    element = self.__get_date_elements(_date)

    return element is not None and 'gameday' in element.get("class") \
            and not self.__get_status(_date) == GameStatus.CALLED_OFF

試合の進行状況

id=yjMSclassを持つ要素中の<span class="status">内に書いてありました。 試合前、中止、結果 の他に1回表、1回裏、、、が入るようです。

def __get_status(self, _date):
    element = self.__get_date_elements(_date)
    span = element.find("span", class_="status")
    if span:
       return span.text.strip()

対戦相手

webページから見ると球団名の記述がないように見えますが、ちょうど球団の画像がある部分に<span class="team">の部分に球団名が入っていたのでそこから取り出します。

def __get_battle_team(self, _date):
    element = self.__get_date_elements(_date)
    if element:
        span = element.find("span", class_="team")
        if span:
            return span.text

得点

得点はid=yjMS<span class=score>から取り出します。

def __get_scores(self, _date):
    pattern = "[0-9]+"

    element = self.__get_date_elements(_date)
    if element:
        span = element.find("span", class_="score")
        if span.text:
            score = re.findall(pattern, span.text)
            return int(score[0]), int(score[1])
    return 0, 0

ニュース

試合がある日にはその日の試合状況に応じたニュース記事がでてきます。あります。リンクがあればそれがそのニュース記事へリンクです。とりあえずaで取得します。

def __get_news_page(self, _date):
        element = self.__get_date_elements(_date)
        if element:
            a = element.find("a")
            if a:
                url = a.get("href")
                return url

このurlからsoupをつくって、ニュースの文章を取り出します。このidがyjSNLiveBattlereviewのところにあります。

def __get_news_text(self, _date):
        news_elem = BeautifulSoup(urlopen(self.__get_news_page(_date)), "html.parser")
        if news_elem:
            div = news_elem.find("div", id="yjSNLiveBattlereview")
            if div:
                return div.text

こんな感じで、それぞれの試合情報を取ってくることができます。 これらをまとめたものが下記になります。

import re
from urllib.parse import urlencode
from urllib.request import urlopen

from bs4 import BeautifulSoup


class BaseballNewsFactory:
    def __init__(self, ):
        self.url = "https://baseball.yahoo.co.jp/npb/teams/4/schedule"
        # self.__news_picker = news_picker

    def create_news(self, _date=None):
        _date = datetime.now() if _date is None else _date

        news = BaseballNews()
        news.my_team = "中日"
        news.date = _date
        news.status = self.__get_status(_date)

        if news.have_game():
            news.news_text = self.__get_news_text(_date)
            news.my_team_point, news.battle_team_point = self.__get_scores(_date)
            news.battle_team = self.__get_battle_team(_date)
        return news

    def __get_date_elements(self, _date):
        year_month = _date.strftime('%Y%m')
        elements = BeautifulSoup(urlopen("{}?{}".format(self.url, urlencode({"date": year_month}))))
        tds = elements.find_all("td")
        for td in tds:
            em = td.find("em")
            if em is not None:
                if em.text == _date.strftime("%d"):
                    return td

    def __get_status(self, _date):
        element = self.__get_date_elements(_date)
        if 'gameday' in element.get("class"):
            span = element.find("span", class_="status")
            if span:
                return span.text.strip()
        else:
            return "試合なし"

    def __get_scores(self, _date):
        pattern = "[0-9]+"

        element = self.__get_date_elements(_date)
        if element:
            span = element.find("span", class_="score")
            if span.text:
                score = re.findall(pattern, span.text)
                return int(score[0]), int(score[1])
        return 0, 0

    def __get_news_page(self, _date):
        element = self.__get_date_elements(_date)
        if element:
            a = element.find("a")
            if a:
                url = a.get("href")
                if url:
                    return BeautifulSoup(urlopen(url))

    def __get_news_text(self, _date):
        news_elem = self.__get_news_page(_date)
        if news_elem:
            div = news_elem.find("div", id="yjSNLiveBattlereview")
            if div:
                return div.text

    def __get_battle_team(self, _date):
        element = self.__get_date_elements(_date)
        if element:
            span = element.find("span", class_="team")
            if span:
                return span.text


from datetime import datetime


class BaseballNews(object):
    def __init__(self):
        self.__my_team = ""
        self.__battle_team = ""
        self.__my_team_point = 0
        self.__battle_team_point = 0
        self.__status = ""
        self.__news_text = ""
        self.__date = None

    @property
    def my_team(self):
        return self.__my_team

    @my_team.setter
    def my_team(self, value):
        self.__my_team = value

    @property
    def battle_team(self):
        return self.__battle_team

    @battle_team.setter
    def battle_team(self, value):
        self.__battle_team = value

    @property
    def my_team_point(self):
        return self.__my_team_point

    @my_team_point.setter
    def my_team_point(self, value):
        if isinstance(value, int):
            self.__my_team_point = value

    @property
    def battle_team_point(self):
        return self.__battle_team_point

    @battle_team_point.setter
    def battle_team_point(self, value):
        if isinstance(value, int):
            self.__battle_team_point = value

    @property
    def status(self):
        return self.__status

    @status.setter
    def status(self, value):
        if isinstance(value, str):
            self.__status = value

    @property
    def news_text(self):
        return self.__news_text

    @news_text.setter
    def news_text(self, value):
        if isinstance(value, str):
            self.__news_text = value

    @property
    def date(self):
        return self.__date

    @date.setter
    def date(self, value):
        if isinstance(value, datetime):
            self.__date = value

    def issue(self):
        return self.my_team_point > self.battle_team_point

    def have_game(self):
        return not (self.status == "試合なし"
                    or self.status == "中止")


if __name__ == '__main__':
    factory = BaseballNewsFactory()
    news = factory.create_news()
    print(news.date)
    print("{} vs {}".format(news.my_team, news.battle_team))
    print(news.status)
    print("{} - {}".format(news.my_team_point, news.battle_team_point))
    print(news.news_text)

これを実行すること、こんな出力が得られます。

2019-05-11 19:41:44.993285
中日 vs 阪神
結果
5 - 1

5月11日(土)阪神 vs. 中日 8回戦
中日は初回、高橋と阿部の連続適時打で3点を先制する。その後は8回表に、高橋の適時打で加点すると、9回には福田のソロが飛び出し、リードを広げた。投げては、先発・柳が8回5安打無失点の快投で今季3勝目。敗れた阪神は、打線が7安打1得点とつながりを欠いた。

勝ってる!!柳!!!

まとめ

今回のコードを使ってalexaちゃんでスキルを作ったりしました。ソースコードは下記になります。

github.com

対象の日付について書かれた要素を取り出せば一度の取得でいろいろ取り出せると思いましたが、一回読み出すとclassの情報が抜けてしまい取り出せませんでした。とりあえず毎度アクセスで対処しました。原因は不明です。。。

git rebaseが捗るオプション紹介

git rebaseでfixupやsquashをよくつかうわたしが出会って感動したオプションたちを紹介してこうと思います。

rebase.autostash

git rebase を行うとき、編集差分がある状態では実行できないため、stashして下さいてきなコメントが出てやり直しとなってしまうのが、なんとも面倒臭いと思うのです。

これを払拭するのがautostashです。

git rebase を実行した際に自動でgit stashを行い、rebaseの作業に移ります。そして、rebase作業が終わった際にはstashしたものを戻すgit stash popまでを自動でやってくれるというものです。ぜひ、おすすめしたい、、!

rebase.abbreviateCommands

git rebase -i で作業するとき、毎度出てくるこの画面。

pick 5f818fa edit file
pick cd4e4ab delete file

# Rebase 24029a2..cd4e4ab onto 24029a2 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.

わたしの場合、fixupをよく使いますが、これを行うためにはpickの文字を消してから、fを入力しなければなりません。これがちょっと面倒臭いと思うのです。

そこでこのabbreviateCommandsを使います。

これにより、pick が p となり編集がより楽になります。

pにカーソルを合わせたらrfを入力してpをfに変更して終了!とやるのが私流です。

p 5f818fa edit file
p cd4e4ab delete file

# Rebase 24029a2..cd4e4ab onto 24029a2 (2 commands)
・・・

rebase.autosquash

fixupやsquashをよく使う人におすすめなオプションです。 このオプションはコミットメッセージの文頭にfixup!またはsquash!がついたものコミットを対象にgit rebase -i画面においてあらかじめfixupやsquashにしてくれます。

$git rebase -i head~~ --autosquash

p 5f818fa edit file
f f540190 fixup! edit file

# Rebase 24029a2..f540190 onto 24029a2 (2 commands)
・・・

autosqaushのオプション追加によりこの画面におけるデフォルトのpick(p)ではなくfixup(f)の状態で画面がでてきたのがわかります。

ここまでautosqaushの説明をしてきましたが、コミットの際にわざわざfixup!とつけたコミットメッセージを書かなければ効果を発揮しないのが難点です。。。

ここでfixup!やsquash!を簡単につける方法があるのでぜひ紹介したいと思います。 git commit --fixup <コミットの指定>です。 コミットの指定についてはコミットidや、head~などが使えます。

$ git commit --fixup head
[master f540190] fixup! edit file
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git log --oneline
2285367 (HEAD -> master) fixup! edit file
ab0d701 edit file
24029a2 create file

デフォルトに設定しよう

このオプションたちはデフォルト設定にしておくととても便利。

$ git config --global rebase.autostash true
$ git config --global rebase.abbreviateCommands true
$ git config --global rebase.autosqaush true

これらを実行するとhome下の.gitconfigファイルの中身に下記が追加されます。

[rebase]
        autosqaush = true
        abbreviateCommands = true
        autostash = true

まとめ

gitはもどかしいと思うポイントは大体対策されてるんだなとrebaseのoptionたちから感じることができました。もどかしいと思ったらとりあえずオプションを探しにいく、これを続けてより効率の良い開発ができるようになりたいです。

もともとわたしはgit commit に関してはIDEIntelliJさんに頼ってるのですが、3番目に書いたgit commit --fixupに関してはサポートされてないなーと思ったら、今年でた2019.1で追加されたみたいです。うれしい。

slackアプリで送信したurlを展開する

f:id:xmadoka:20190227235654p:plain

slackにてurlを貼ってメッセージを送信すると自分のアカウントではurl展開されてページの概要のようなものが見れるのに、incoming webhooksから送られたurlは展開されないということに気がつきました。もちろんincoming webhooksでも展開して欲しいので調べてみました。

どうやらこの現象は仕様らしい

By default we unfurl all links in any messages posted by users. For messages posted via incoming webhooks, the chat.postMessage API method or chat.postEphemeral, we will unfurl links to media, but not other links.

ユーザーからの投稿にあるリンクはデフォルトでは全て展開する。chat.PostMessage もしくはchat.postEphemeralを使ったincoming webhooksのmediaへのリンクは展開しようとするが、他はしない。とのこと。

参照元: Unfurling links in messages | Slack

リンクを展開させる

slackにpostするときにtextに加え、unfurl_linkをtrueにして送るとリンクを展開するようになる。

{
    "text": "<https://api.slack.com>",
    "unfurl_links": true
}

これによりさきほどと同様のリンクが展開するように!

f:id:xmadoka:20190227235702p:plain

mediaについても試してみた

mediaはデフォルトで展開されるそうなので試してみた。(ここでいうmediaは動画や画像、ツイートなど)

{
    "text": "https://www.youtube.com/watch?v=ci7eOVfYBfA"
}

f:id:xmadoka:20190228222328p:plain 逆に消したいときは、unfurl_mediaをfalseにて送ることで、展開されなくなる。

{
    "text": "https://www.youtube.com/watch?v=ci7eOVfYBfA",
    "unfurl_media": false
}

まとめ

今回は展開するだけでしたが、調べてるうちにslackのメッセージにボタンつけることができたりなど、面白そうなことがいろいろできそうなことを知りました。今度機会があるときにでも遊んでみたいです。

「DNSをはじめよう」を読んでまとめてみた

mochikoAsTechさんの書いた「DNSをはじめよう」を読んで自分なりにまとめてみました。

ドメインの取得

ドメインの伝搬

レジストリ -> レジストラ -> リセラ

この関係性からレジストラからの登録が一番信頼できる。 この書籍に載ってた事業者種類とサイトとの対応づけは下記の表の通り。

事業者種類 事業者名一覧
レジストラ お名前.com、ゴンベイドメイン
リセラ 名付けてねっと、Yahoo!ドメインムームードメインVALUE DOMAIN

レジストリの役割

TLD(top level domain)* を管理すること。1つのTLDは1レジストリによって管理されている。 同じドメインが複数存在するとルーティングができなくなってしまうので、それを引き起こさないために一つのTLDは一つのレジストリに一元管理されている。

*: TLDとは「example.co.jp」、「yahoo.com」などの一番右側 jp、comを指す。

ドメイン取得のとき

  • 色々と買わせようとしてきたりするので根気強く
  • ドメインの自動更新は早めに切っておいた方がよい
  • Whois*の登録はしなければならない

*: Whoisとはドメインの所有者の氏名、連絡先などを公開しているサービス。 何らかのトラブルがあったときにドメイン所有者同士で解決してほしいためこういうサービスが存在する。(むしろそうしないとレジストリ側の処理数がやばいことになる)

DNS

フルリゾル

ドメインに紐づくIPアドレスを調べてくるもの。 「DNSキャッシュサーバ」、「フルサービスリゾルバ」とも呼ばれる。 TLDから順に辿ってIPアドレスを取得してくる。

個人的には電話番号を解釈するときの思考に似てるなと思った。

+81 03 xxxx xxxx とあったら、+81は日本 -> 日本の03は東京 ...みたいな。

TTL (time to live)

フルリゾルバにはキャッシュがある。ドメインに紐づくIPアドレスを一回取得したら、TTLとして登録されている時間分だけその情報を残しておく。そのため、ドメインに紐づくIPアドレスが変更してすぐにそれをすぐ自分が参照したいと思ったら、自分の利用しているフルリゾルバのTTLを一時的に短くするまたはクリアするなどの必要がある。

リソースレコード

リソースレコードのタイプ 値の意味
Aレコード ドメインに紐づくIPアドレス
NSレコード ドメインのゾーンを管理するネームサーバ
MXレコード ドメインに紐づくメール受信サーバ
TXT(SPF) このドメインのメール送信元サーバ
SOA ドメインのゾーンの管理情報
CNAME このドメインの別名でのリソースレコードの参照先

コマンドで調査

whoisというコマンドを使って、ドメインに紐づくwhoisの情報を取得することができる。

$ whois ドメイン名

【 whois 】コマンド――ドメイン情報を表示する:Linux基本コマンドTips(159) - @IT

digコマンドを使ってa,ns,mx,txt,soaなどの情報を指定して取得できる。指定しなくてもドメインに紐づくIPアドレスは取得できるが情報量多めになる。

$ dig ドメイン名 [クエリタイプ]
# クエリタイプにaやnsなどをいれる
$ dig ドメイン名 [クエリタイプ] +short
# +shortをいれるとドメインに紐づくIPアドレスの一覧だけが取得できる。

【 dig 】コマンド――ドメイン名からIPアドレスを調べる:Linux基本コマンドTips(158) - @IT

迷惑メール判定について

一つ目は必須だが、二つ目は受信サーバによってはこの条件があるという感じらしい。とりあえず上記2つを満たしていれば、いまのところ迷惑メール判定はされない。

まとめ

今回はこの本を読んでDNS周りで新しく得られた知識について書きました。ここにはまとめていませんが、実際のドメインの取得方法やDNS(Route53)への登録方法など、実践的な内容も細かに説明されているので、この本があれば自分のサイトをサクサク作れそうです。次はAWSをはじめようを読み進めていく予定です!

numpyをjson.dumpするときに気をつけたいこと

numpyの数値をほかのintやfloat型と同様にjson dumpしようとして、できたりできなかったりすることがあった。

どうやらfloat64はできるのにint64はできないらしい。

どの型ならそのままdumpできるのか

どの型ならそのままdumpできるのか、testしてみました。

testに使うコードは下記。

import numpy as np 
import json

def json_dump(array):
    with open("./sample.json", 'w') as f:
        json.dump({ i: e for i, e in enumerate(array)}, f, indent=4)
    return "success"

int32,int64,float43,float64でためしてみる。

>>> json_dump(np.array([0,1,1], dtype=np.int32))

TypeError: Object of type 'int32' is not JSON serializable

>>> json_dump(np.array([0,1,1], dtype=np.int64))

TypeError: Object of type 'int64' is not JSON serializable

>>> json_dump(np.array([0,1,1], dtype=np.float32))

TypeError: Object of type 'float32' is not JSON serializable

>>> json_dump(np.array([0,1,1], dtype=np.float64))

'success'

float64以外は全滅。。。

解決方法

json dumpにエンコーダーを入れ込む。今回はint,floatだけ扱ったが、ndarrayも同様にそのままではlistとして扱えず、シリアライズできないというエラーが発生する。numpyの数値をそのままjsonにしたいと思ったら下記のclassを作って入れ込むのが良さそうだ。

class NumpyEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, np.integer):
            return int(obj)
        elif isinstance(obj, np.floating):
            return float(obj)
        elif isinstance(obj, np.ndarray):
            return obj.tolist()

        return super(NumpyEncoder, self).encode(obj)

ちなみに

numpyの配列はpythonのlistとは違って、配列の要素を全て同じ型で扱う。配列作成時の特に指定がない場合は配列の要素のデータの型が一番大きいものに合わせるので、おもしろいことに下記のように要素の一つに小数点を入れるだけで型がガラッと変わるのだ。

>>> np.array([0,0,0]).dtype, np.array([0.0,0,0]).dtype
(dtype('int64'), dtype('float64'))

まとめ

むしろnumpyのfloat64が普通にdumpできてしまうことが素晴らしいと思った。数値計算などに使うことが想定されたライブラリだから、int64を扱うことの方がレアだから、対応がされてないのかなとか思ったり。

AWS Lambdaで時刻を扱う

Lambdaにてdatetime.now()を用いて、現在時刻との時間比較を行うApplicationを作っていたところ、どうも時間比較が想定と違う動きをしているぞと悩んでました。ふとLambdaのTimeZoneが違うのでは!と頭をよぎったので調べてみました。

LambdaのTimeZoneは、、

LambdaのTimeZoneはどうやらUTCのようです。比較対象の時刻はJSTだったので、それはうまくいかないわけです。

LambdaでのTimeZone確認

LambdaのTimeZone確認に使用したコードです。

from datetime import datetime 
import os
def lambda_handler(event, context):
    return {
        'time_zone': os.environ["TZ"],
        'now':datetime.now().strftime("%Y%m%d%H%M")
    }

出力

{
  "time_zone": ":UTC",
  "now": "201902181443"
}

TimeZoneを変更する

環境変数に TZ : Asia/Tokyoと追加しました。 環境変数の入力欄 その後先ほどのコードを動かしてみたところ、、、

{
  "time_zone": "Asia/Tokyo",
  "now": "201902182343"
}

時刻表示が東京基準になりました。めでたし!

雨の予報をslackに通知する

外に出かけると屋内にいることが多くて、雨が降ったことも気付かないことがしばしばある。雨が降っているのであれば雨が止むまで待てば良いし、降り止んでいるのならそのまま帰ればいいしで、とくに問題があることはない。しかし、そんな日に洗濯物を外に干しているとなれば話は別だ。外に出ていてもこれから雨が降るとなればダッシュで帰って洗濯物を取り込みたい。そんな理由から今回、雨が降るから帰らなきゃみたいなアクションのきっかけになればと、雨の予報を通知するアプリを作ることに。

要件定義

指定した地域で雨が降っている、もしくは近々1時間くらいに雨が降りそうだという情報の通知の通知

必要な機能

  • 気象情報と取ってくる
  • 通知を投げる
  • ある一定の間隔で繰り返しを行う

今回使ったものたち

  • 気象情報を取ってくる -> YOLP

  • 通知を投げる -> slack

    • 雨が継続してるのに連続で投げられると嫌 -> Amazon DynamoDB
  • ある一定の間隔で繰り返しを行う -> Amazon Cloud Watch

  • serverの代わり -> AWS lambda

大体の流れ

sourceをlambdaにあげて、Amazon Cloud Watchに実行してもうようにしました。

  1. Amazon Cloud WatchのEventをきっかけにlambda起動 (以下lambdaの動き)
  2. YOLPから1時間以内の降水量を5分ごとのデータとして取得。
  3. 現在(10分前)から1時間後までのデータで降水量が0ではないところがあるかないかを判断。
  4. DynamoDBに通知をなげる必要があるかの情報を取得 -> 更新の必要があれば更新。
  5. さきほど取得したデータから通知の必要があった場合にのみ、slackに通知を投げる。

f:id:xmadoka:20190104224951p:plain
連携イメージ

githubにコードをあげました。 github.com

AWSと連携しないオンプレバージョンの記事はこちら

xmadoka.hatenablog.com

追記

こちら製作したのが去年の12月23日とかで、それから2週間近く雨なんて無縁な晴れた日が続いたため、slackのこのチャンネルがまったく動かなかった。。そんなところにやっと!!!

slackに通知が来た様子
slackに通知が来た様子
通知が来ました!!!test以来初!すごいうれしい!!