madokaのブログ

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

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以来初!すごいうれしい!!

pyqueryとBeautifulSoupの比較

lambdaにてpyqueryを使ったものをアップロードして利用しようとしたところ、エラーが発生してしまいました。pyquery中のetreeというパッケージが存在しないとのこと。おそらくetreeのデータをsite-packagesの中に組み込めば動くようになると考えられるのですが、普通の行動してエラー吐かれた事実に面倒くささを感じ、スクレイピングを行うapiをBeautifulSoupに変更することにしました。その時に発生した変更分などから、どんな違いがあるのか簡単まとめてみました。

スクレイピングの準備

pyquery

from pyquery import PyQuery

url = 'https://…'
query = PyQuery(url, parser='html')

BeautifulSoup

from urllib.request import urlopen
from bs4 import BeautifulSoup

url = 'https://…'
soup = BeautifulSoup(urlopen(url), 'html.parser')

タグによる指定

pyquery

query(‘header’)

BeautifulSoup

soup.find_all('header')

idによる指定

pyquery

query('#a')

BeautifulSoup

soup.find_all(id='a')

class名による指定

pyquery

query('.clazz')

BeautifulSoup

soup.find_all(class_='clazz')

属性による指定

pyquery

query('[href=\'https://….\']')

BeautifulSoup

soup.find_all(attrs={'href':'https://….'})
soup.find_all(href='https://….')

中身を取得する

pyquery

element.attr('href')
element.attr('class')

BeautifulSoup

element.get('href')
element.get('class')

要素の中の文章を取得する

pyquery

element.text()

BeautifulSoup

element.text

少しコメント

pyqueryの指定方法は、cssセレクタのようです。一方、BeautifulSoupは使えないのかというとそういうわけではありません。selectというmethodを使えば、cssセレクタによる指定も可能です。

まとめ

スクレイピングで書くコードはどのapiを使っても書き方に大きな変化はないなという印象を受けました。

YOLPを使って1時間以内の雨予報を取得する

休日なので外に洗濯物干してひきこもってたのですが、知らないうちにあめがふってたぽい?なことがありました。 そこで1日の天気予報とかではなく、もっと1時間とかの近い時間帯について雨の情報をおしらせしてくれるものがほしいなと思いました。 まずは、お手軽に小さい単位での気象情報を取得できるAPIがないか調べてみました。すると、Yahoo! Open Local Platform (YOLP)に気象情報APIというAPIを発見!2時間前から1時間後までの降水量を10分間隔で取得できるらしいことがわかったので、こちらを利用させてもらうことにしました。 developer.yahoo.co.jp

YOLPとは

地図・地域情報などのAPISDKです。地図を扱ったサービスがメインなようですが、今回利用した気象情報やその他にも郵便番号検索や経路探索などもできたりと幅は広そうです。

YOLPを利用するために

まずはapplication id を取得します。下記のサイトから手順を追えば、取得できます。

developer.yahoo.co.jp

https://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/weather.html

1時間以内の雨情報を取得してみた

parameters

key value 必須
appid さっき取得したapplication id *
coordinates 経度、緯度のリスト *
output 出力方法 json or xml(デフォルト)
past 1時間前の降水量を取得する デフォルトは0になってる。2時間前まで指定できる
interval 10分毎 or 5分毎で指定できる

コード

下記が取得するためのコードです。

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


class WeatherClient:
    BASE_URL = "http://weather.olp.yahooapis.jp/v1/place"

    def __init__(self, app_id, coordinates):
        """

        :param app_id: your YOLP app_id
        :param coordinates: tuple(longitude, latitude)
        :type app_id: str
        :type coordinates: tuple[float]
        """
        self.app_id = app_id
        self.coordinates = coordinates

    def get(self):
        url = "{}?{}".format(self.BASE_URL, urlencode({
            "appid": self.app_id,
            "coordinates": self.coordinates,
            "output": "json",
            "past": 1
        }))
        with urlopen(url) as res:
            return json.loads(res.read().decode('utf-8'))
if __name__ == '__main__':
    print(WeatherClient("xxxxxxxxxxxxxxxxxxxxxxxxxx", (139.767125, 35.681236)).get())

取得できたjson

{
    "ResultInfo": {
        "Latency": 0.006118,
        "Total": 1,
        "Status": 200,
        "Description": "",
        "Count": 1,
        "Copyright": "(C) Yahoo Japan Corporation.",
        "Start": 1
    },
    "Feature": [
        {
            "Name": "\u5730\u70b9(139.76712,35.681236)\u306e2018\u5e7412\u670822\u65e5 20\u664230\u5206\u304b\u3089120\u5206\u9593\u306e\u5929\u6c17\u60c5\u5831",
            "Geometry": {
                "Coordinates": "139.76712,35.681236",
                "Type": "point"
            },
            "Id": "201812222030_139.76712_35.681236",
            "Property": {
                "WeatherList": {
                    "Weather": [
                        {
                            "Date": "201812222030",
                            "Type": "observation",
                            "Rainfall": 0.0
                        },
                        {
                            "Date": "201812222040",
                            "Type": "observation",
                            "Rainfall": 0.0
                        },
                        {
                            "Date": "201812222050",
                            "Type": "observation",
                            "Rainfall": 0.0
                        },
                        {
                            "Date": "201812222100",
                            "Type": "observation",
                            "Rainfall": 0.0
                        },
                        {
                            "Date": "201812222110",
                            "Type": "observation",
                            "Rainfall": 0.0
                        },
                        {
                            "Date": "201812222120",
                            "Type": "observation",
                            "Rainfall": 0.0
                        },
                        {
                            "Date": "201812222130",
                            "Type": "observation",
                            "Rainfall": 0.0
                        },
                        {
                            "Date": "201812222140",
                            "Type": "forecast",
                            "Rainfall": 0.0
                        },
                        {
                            "Date": "201812222150",
                            "Type": "forecast",
                            "Rainfall": 0.0
                        },
                        {
                            "Date": "201812222200",
                            "Type": "forecast",
                            "Rainfall": 0.0
                        },
                        {
                            "Date": "201812222210",
                            "Type": "forecast",
                            "Rainfall": 0.0
                        },
                        {
                            "Date": "201812222220",
                            "Type": "forecast",
                            "Rainfall": 0.0
                        },
                        {
                            "Date": "201812222230",
                            "Type": "forecast",
                            "Rainfall": 0.0
                        }
                    ]
                },
                "WeatherAreaCode": 4410
            }
        }
    ]
}

今回取得したいのは降水量のところなので、response["Feature"][0]["Property"]["WeatherList"]["Weather"]で取れそうです。

今回の制作物

雨情報が取得できたら、slackに投げるものをつくってみました。

github.com

まとめ

記事書いててはっとしたんですが、もしかすると、降水量は実測値しか入ってないかもなので予報のところはすべて0.0で入って返ってきてるかもです。5分以内の情報ならギリ使える。。頻度多めにapiを叩かないとですね。

pythonの参照型をデフォルト引数にすることとは

javaをメインにお仕事してたので、なかなかお目にかかることのなかったデフォルト引数 (javaにはない) 。最近pythonを書いていて、たまたまデフォルト引数にlistをいれてみようかなと思って書いてみたら、intelliJさんに黄色くされたので気になって調べてみることに。

デフォルト引数について思うこと

そもそもデフォルト引数とは引数の初期値。

javaだと引数減らした同じ名前のmethodをもう一つ作ってoverloadというかたちをとったり、場合によってはnull判定をしてnullだったらこれ入れるみたいなことをしてたのが、デフォルト引数で全て片付くのでコードがすっきりするので存在に感謝している。

デフォルト引数は使い回す。

print_sampleの引数としてもらったsのidを出力するというコードを書きました。

def print_sample(s=1):
    print(id(s))


if __name__ == '__main__':
    print_sample() #1
    print_sample(2) #2
    print_sample() #3

出力

4353093824 #1
4353093856 #2
4353093824 #3

出力を見ていただけるとわかるようにデフォルト引数を使うことになった#1,#3でidが同じことがわかります。このことから、デフォルト引数が毎度生成されているわけではないということが言えそうです。

デフォルト引数にlistを入れるということは。。。

デフォルト引数にlistをいれるなんて、なんて恐ろしいことなんだと気がつきました。

さきほどデフォルト引数が毎度生成されていないということがわかったので、つまるはなし参照型のものをデフォルト引数にしてしまうとlistに要素を追加するなどのデータが書き換えがあった場合に、次にデフォルト引数を使うときには中身が想定のもとは異なってしまうということです。

またこのmethodを実際に使うことを考えると、以前にどんな呼び出され方をしたのか意識して使わなければいけないということです。ああ、なんておそろしいんだ。。

恐ろしさを実感するコードが以下になります。

def print_list(n, l=[]):
    print(l)
    l.append(n)


if __name__ == '__main__':
    print_list(2)
    print_list(3)
    print_list(0)

出力

[]
[2]
[2, 3]

デフォルト引数のlistに要素が増えていることがわかります。

まとめ

intelliJのwarningは聞いておくものだなとしみじみと感じました。これからはもうちょっとintelliJさんの声に耳を傾けようと思います。

macでcron的なことをする

macで定期実行はcronではなく、launchdがおすすめ

macでもcronは使えるらしいが、launchdを利用することが推奨されているらしいので、launchdで書きます。

今回動かすコマンド

$ /bin/sh path/to/test1.sh

のコマンドを動かしてもらいます。 念のためfull pathで/bin/sh指定しました。

コード

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>test1</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/sh</string>
        <string>/path/to/test1.sh</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
         <key>Hour</key>
         <integer>7</integer>
         <key>Minute</key>
         <integer>10</integer>
    </dict>
    <key>StandardOutPath</key>
    <string>/path/to/out.log</string>
    <key>StandardErrorPath</key>
    <string>/path/to/err.log</string>
</dict>
</plist>

ファイルの置き場所

用途 Directory
ユーザーごとの設定 ~/Library/LaunchAgents/<Label>.plist
全てのユーザーの設定 /Library/LaunchAgents/<Label>.plist
システム共通設定 /Library/LaunchDaemons/<Label>.plist
  • LaunchAgents : ログインしているときに動くもの
  • LaunchDaemons : ログインしてなくても動くもの

ファイル名の<Label>とファイル中の<Label>を一致させる。今回の場合はtest1

コマンドの指定

今回動かしてもらおうと思っていたコマンドを空白区切で配列としてProgramArgumentsをkeyにして記入する。

<key>ProgramArguments</key>
<array>
    <string>/bin/sh</string>
    <string>/path/to/test1.sh</string>
</array>

イベントのタイミング

今回は毎日同じ時間にプログラムを動かして欲しいので、時間指定しました。

<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>7</integer>
    <key>Minute</key>
    <integer>10</integer>
</dict>

数秒ごとにプログラム実行も可能。その場合はStartCalendarIntervalの代わりにStartIntervalを利用する。この時の単位は秒です。

<key>StartInterval</key>
<integer>300</integer>

その他にもファイル監視をすることもできるようです。 下記urlに詳しく書いてあります。

Creating Launch Daemons and Agents

登録と解除

ユーザーがログインしている時に動かしたいので、pathは~/Library/LaunchAgents/test1.plistにしました。

登録する

$  launchctl load ~/Library/LaunchAgents/test1.plist

解除する

$  launchctl unload ~/Library/LaunchAgents/test1.plist