madokaのブログ

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

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

golangでファイルパスを扱う際に気をつけたいこと

 きっかけ

golangの標準ライブラリを使って、ファイルパスを扱うコードを書きました。自分はMacを使っていたので問題はなかったのですが、同じコードをWindowsに持っていったら、ファイルパスがうまく取れない事態が発生。あまりにもびっくりだったので、ここでまとめます。

 

path (os.path)のファイルセパレーターは/だった

以下はpath.Splitのコードです。

func Split(path string) (dir, file string) {
    i := strings.LastIndex(path, "/")
    return path[:i+1], path[i+1:]
}

まさかのファイルセパレーターが/で指定されていました。Windowsの場合、ファイルセパレーターは\なのでここで見つからずに.になってしまっていました。

path.filepathを使おう

goの標準ライブラリにはpathで実装されているmethodを新たに実装してるものがあります。path.filepathです。 さきほど例に挙げたSplitはfilepathでは以下のようになっています。

func Split(path string) (dir, file string) {
    vol := VolumeName(path)
    i := len(path) - 1
    for i >= len(vol) && !os.IsPathSeparator(path[i]) {
        i--
    }
    return path[:i+1], path[i+1:]
}

こちらはセパレーターはosによって変化するようになっています。ファイルを扱う際にはこちらを使いましょう。

ちなみに

pathはもしや、urlの処理に使えるのではと頭によぎったので、入れてみました。

func main(){
  url := path.Join("http://localhost:8080","app")

  fmt.Println(url)
 // http:/localhost:8080/app

}

まさかのhttp://からスラッシュが1つ削られて使えなくなりした。

path.Cleanというmethodによって不要なセパレーターは排除されてしまうようです。

まとめ

まさか標準ライブラリに翻弄されるとは思いませんでした。 path - The Go Programming Languageを見に行ってみるとurlで記述するようなスラッシュを使っていて、Windowsには対応してません。と書いてありました。Macのファイル操作にしか使わないものにしか使えなさそうですね。

.gitignoreを変更せずに、ファイルを無視したい

動機

この度、intelliJのUltimate版を購入したのがきっかけで、.gitignoreファイルに.ideaを追記したくなったのが事の発端。 しかし、自分の事情だけでファイルを変更するのは、あまり好ましくありません。 ということで、他の方法で対象から外す方法を調査しました。

あった

git cloneや、git initをしたときにできる .git ディレクトリの中にありました。 .git/info/excludeファイル(repositoryからの相対path) です。ここに、.gitignoreと同じように記述することで、そのファイルらを対象から外すことができました。

before

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    .idea/
    my_test.iml

nothing added to commit but untracked files present (use "git add" to track)

さっそく追記します

vimで開いて編集しました。

# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
.idea  #追記
*.iml   #追記

after

$ git status
On branch master
nothing to commit, working directory clean

みごと、gitから.idea/と.imlファイルが無視されるようになりました。

参考させていただいたページ

https://qiita.com/kiyodori/items/a29f7be2013803250141

pythonのプロジェクトごとにパッケージを管理する

動機

Pythonをお仕事でも使うことになり、自分の環境だけでなく、他の環境でもすぐ動くようにする必要が出てきたので、pyenv-vertualenvを利用して環境構築をしました。その際の覚え書きです。  

まずはインストール(Mac)

Homebrewを使って、pyenvとpyenv-virtualenvをインストールします。

$ brew install pyenv

$ brew install pyenv-virtualenv

必要なpythonのバージョンをインストール

まずはpyenvでインストールできるPythonのバージョンを確認してみます。

$ pyenv install --list

先ほどの一覧からインストールしたいバージョンを選びインストールを行います。今回は3.6.5をインストールすることにしました。

$ pyenv install 3.6.5

インストールができたら、実際にpyenvに入ったかどうか確認します。

$ pyenv versions

ディレクトリにpythonの仮想環境を作る

これからpythonの開発を行おうとしているディレクトリに移動し、そのディレクトリで利用するpythonのversionを指定します。

$ cd projectのディレクトリ
$ pyenv local 3.6.5

#指定できたことの確認
$ python --version
Python 3.6.5

pythonの環境を現在のディレクトリ配下のvenv(2つ目のvenvがディレクトリ指定になっています)に作成します。

$ python -m venv venv --copies

現在のディレクトリにて先ほど作成した環境を適用する。

$ source venv/bin/activate
# 上記コマンド実行の結果、下記のようにvenvが現れれば成功です。
(venv) $

パッケージの管理

パッケージ管理に必要なツールのインストールを行います。

$ pip install pip-tools

requirements.inに利用しようと思っているパッケージを記入します。

Django

requirements.inに書かれたパッケージと依存関係のあるパッケージをrequirements.txtに書き出します。

(venv) $ pip-compile requirements.in
#
# This file is autogenerated by pip-compile
# To update, run:
#
#    pip-compile --output-file requirements.txt requirements.in
#
django==2.1.2
pytz==2018.5              # via django

djangoと依存関係があるらしいpytzも記入してくれました。

最後に書き出したパッケージのインストールを行います。

(venv) $ pip-sync

   

思ったこと

自分のPCでプロジェクトディレクトリにpythonコピーして、最終的にpip-syncをしたら、Uninstallが恐ろしいほど出てきました。今までこれだけのパッケージを統一的に扱っていたと思うと恐怖でしかありません。。