tanamonの稀に良く書く日記

KEEP CALM AND DRINK BEER

Pythonのloggingでエラーがあった場合だけ遡って出力する

クローラーみたいなものを作ってるとログがやたら出て困る。

今日動かしたやつは200MBを超えていた。そして開いたらVSCodeが不安定になった。

再起動したらこのメッセージ。いや、今まで散々Pythonでコード書いてたじゃない。何忘れてるの。

ログは見たいが出力量は減らしたい

ログが出すぎる問題への対処の基本はパッケージ単位で出力ログレベルを上げることだけど、クローラーのような、相手側のデータの都合でこちらの処理の成否が決まるようなプログラムは出力レベルを上げるとエラーを見てもわからないことが多い。必ず入る想定(例えばニュース記事のタイトル)のXPathの結果がNoneなのはなぜ?みたいなのは、UnitTestで先回りして品質を上げるのも難しい。

そうなると、ある程度ざっくりとした想定で作ったプログラムに処理の途中経過のログをDEBUGレベルなどで多めに出すようにしておいて、ひたすら動かして異常データを検出してから、それをテストケースにしながら修正する、という泥臭い作り方になる。当然ログ出力の量は増える。

とはいえ、ほぼ正常なログの中からエラーとなったデータを探すのは面倒なので、ログ出力のコードは書くが実際の出力はログレベルの以外の方法で抑制して欲しい、という要件が出てくる。

こういう、ある種のとんち的な要求に対する実装は検索で探しても見つからない(そもそも検索ワードがわからない)ので、最初から諦めてとりあえず自前で簡単なものを実装してみた。

コード

import logging.config

logger = logging.getLogger(__name__)


class MyMemoryHandler(logging.handlers.MemoryHandler):

    def emit(self, record):
        if self.capacity <= len(self.buffer):
            del self.buffer[0]
        super().emit(record)

    def shouldFlush(self, record):
        return (record.levelno >= self.flushLevel)

    def flush(self):
        # memo: logging.shutdown()からはshouldFlush()を経由せずに呼ばれるので再チェックがいる
        if self.buffer and self.shouldFlush(self.buffer[-1]):
            super().flush()


if __name__ == '__main__':
    if False:
        target_handler = logging.FileHandler(filename='./logtest.log', mode='w')
    else:
        target_handler = logging.StreamHandler()
        target_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] (%(filename)s) %(message)s"))

    handler = MyMemoryHandler(capacity=3, flushLevel=logging.ERROR, target=target_handler)
    logging.basicConfig(level=logging.DEBUG, handlers=[handler])

    for n in range(25):
        if n % 10 == 9:
            logger.error(n, stack_info=True)
        else:
            logger.debug(n)

    logger.debug('end')  # 出力されない

loggingパッケージのMemoryHandlerからの拡張。このクラスは外部などに非同期でログ出力する用途で、ログ出力を同期から非同期に変えるために一度バッファリングするという目的のものだけど、今回の用途に合ってたので使った。

エラーの時に遡ったログと合わせてSlackに投げるみたいなのもできるので、このクラスのベースでいいと思う。

引数はこれら。

  • capacity: 貯めておくログの行数。flushLevel以上となったログを含むので、この値-1の分だけ遡る
  • flushLevel: ログを出力するトリガーとするエラーレベル
  • target: flushLevel以上のログが出た時に渡すhandler

ちなみに、del self.buffer[0]のところが計算量O(n)と遅いので、capacityはあまり巨大な値にしないほうがいいと思います。

実行結果

2019-07-15 03:39:05,150 [DEBUG] (logtest.py) 7
2019-07-15 03:39:05,150 [DEBUG] (logtest.py) 8
2019-07-15 03:39:05,150 [ERROR] (logtest.py) 9
Stack (most recent call last):
  File "logtest.py", line 35, in <module>
    logger.error(n, stack_info=True)
2019-07-15 03:39:05,150 [DEBUG] (logtest.py) 17
2019-07-15 03:39:05,150 [DEBUG] (logtest.py) 18
2019-07-15 03:39:05,150 [ERROR] (logtest.py) 19
Stack (most recent call last):
  File "logtest.py", line 35, in <module>
    logger.error(n, stack_info=True)

ERROR出力の時に2行遡って出力されます。

参考

緯度経度の省略はlat/lng、lat/lon、lat/longのどれが多数派なのか

タイトルのとおりなのですが、気になったので調べてみました。

ちなみに、調べる前はlat/lng派でした。

Google Trendsで比較

省略しないlatitude/longitudeを加えてGoogle Trendsで比較してみました。

f:id:tanamon:20190710175931p:plain https://trends.google.co.jp/trends/explore?date=today%205-y&q=lat%2Flon,lat%2Flng,lat%2Flong,latitude%2Flongitude

結果はlat/long >>> latitude/longitude > lat/lon >>> lat/lngでした。

lat/lngが少ないのが意外でした。けっこう使われてた記憶があるんですが、日本限定なんでしょうかね。

longは予約語に入っている言語もあるのですが、地図を一番使う環境のJavaScriptでは予約語になってないので、気にしないのが主流ということなんでしょうか。

というわけで、これからはlat/longを使おうと思います。

緯度経度と経度緯度

緯度経度を経度緯度と逆順で書く流派もあるので、ついでにその比率も調べてみました。

f:id:tanamon:20190710175936p:plain

latitude/longitude, longitude/latitude - Google トレンド

意外と拮抗していますが、やはり緯度経度の順番が多いようです。

この順番違いがバグを生むので、緯度経度の順番で統一して欲しいです。

GCEのf1-micro環境でPythonが動く環境を構築する

Google Compute Engineの無料枠のf1-micro環境でPythonの実行環境を構築した記録です。

OSはUbuntu 19.04にしました。

% gcloud compute ssh [INSTANCE_NAME]

$ cat /etc/os-release | grep VER
VERSION="19.04 (Disco Dingo)"
VERSION_ID="19.04"
VERSION_CODENAME=disco

スワップ領域が無い

アップデートや必要ライブラリを入れる前にスワップ領域を作ります。

f1-microはRAMが600MBしか無いくせに、初期状態ではスワップ領域が未設定なのです。

$ cat /proc/swaps
Filename                Type        Size    Used    Priority

そのままだとpipenv installでOutOfMemory Killerというシリアルキラーがわりと現れる世紀末環境なので、設定は必須です。

スワップ領域を作る

スワップ領域のサイズは、RAMが2GBまでの環境ではRAMの2倍が一般的らしい。

access.redhat.com

慣習に従って、1.2GBで作ります。

$ sudo dd if=/dev/zero of=/var/swapfile bs=1M count=1200
1200+0 records in
1200+0 records out
1258291200 bytes (1.3 GB, 1.2 GiB) copied, 43.4791 s, 28.9 MB/s

$ sudo chmod 600 /var/swapfile

$ sudo mkswap -L swap /var/swapfile
Setting up swapspace version 1, size = 1.2 GiB (1258287104 bytes)
LABEL=swap, UUID=1ec17506-09a4-4517-911d-c3e3e0f45428

$ sudo swapon /var/swapfile

$ cat /proc/swaps
Filename                Type        Size    Used    Priority
/var/swapfile                           file        1228796 0   -2

再起動時にスワップ領域が自動的にmountするようにする

このままだと再起動時に有効にならないので、fstabに書きます。

$ echo '/var/swapfile swap swap defaults 0 0' | sudo tee -a /etc/fstab
/var/swapfile swap swap defaults 0 0

確認のため再起動してみる。

$ exit

いったんsshを終了させて、gcloudコマンドで再起動します。

% gcloud compute instances stop [INSTANCE_NAME]
% gcloud compute instances start [INSTANCE_NAME]
% gcloud compute ssh [INSTANCE_NAME]

ログインできたらマウントの確認。

$ cat /proc/swaps
Filename                Type        Size    Used    Priority
/var/swapfile                           file        1228796 0   -2

マウントされていた。

Ubuntuのアップデート

$ sudo apt update

$ sudo apt upgrade -y

anyenvのインストール

aptで入れてもいいけど、OS配布版は古いバージョンになったりするので、anyenv + pyenvを使う。

今回はPythonしか使わないのでpyenvを直接入れてもいいけど、別の言語の時でも環境構築を同一手順にしたいのでanyenvから入れる。

github.com

github.com

$ git clone https://github.com/anyenv/anyenv ~/.anyenv

$ cat << \EOS >> ~/.bashrc
if [[ -d $HOME/.anyenv ]]; then
  export PATH="$HOME/.anyenv/bin:$PATH"
  eval "$(anyenv init -)"
fi
EOS

$ exec $SHELL -l
ANYENV_DEFINITION_ROOT(/home/user/.config/anyenv/anyenv-install) doesn't exist. You can initialize it by:
> anyenv install --init

$ anyenv install --init
Manifest directory doesn't exist: /home/user/.config/anyenv/anyenv-install
Do you want to checkout ? [y/N]: y
...
Completed!

$ anyenv --version
anyenv 1.1.1

pyenvのインストール

依存ライブラリを入れる。

公式サイトによると、以下のライブラリが必要らしい。

$ sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev
xz-utils tk-dev libffi-dev liblzma-dev python-openssl git

ログを見るとbuild-essential, make, wget, curlはすでにあった。gitもすでに使ってるのである。

Minimal版を想定したライブラリ構成だろうか。

$ anyenv install pyenv

$ exec $SHELL -l

$ pyenv --version
pyenv 1.2.12-4-g525dac36

Pythonのインストール

現在の最新版3.7.3を入れる。

$ pyenv install 3.7.3

$ pyenv global 3.7.3

$ python --version
Python 3.7.3

Pipenvのインストール

pipenvの作業ディレクトリはプロジェクト配下のほうが好きなのでPIPENV_VENV_IN_PROJECTの設定をしておく。

$ pip install pipenv
...
You are using pip version 19.0.3, however version 19.1.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

$ pip install --upgrade pip

$ echo "export PIPENV_VENV_IN_PROJECT=true" >> ~/.bashrc

$ pipenv --version
pipenv, version 2018.11.26

pipが古いと言われたので、ついでに更新した。

(参考)aptでPythonを入れる場合

もし、aptで直接入れたい場合は、以下の手順で入れられる。

$ sudo apt install -y python3 python3-pip python3-venv

$ pip3 install pipenv

$ echo "export PIPENV_VENV_IN_PROJECT=true" >> ~/.bashrc

住所.jpのMySQLデータがインポートできない件

住所情報のデータベースを探したら住所.jpというのを見つけた。

jusyo.jp

お知らせが2010年からあるので、けっこう昔からデータ提供しているらしい。

しかし、試しに使ってみようと思ったらインポートエラー。

$ mysql -u root -p sandbox -p < zenkoku.sql
Enter password:
ERROR 1366 (HY000) at line 3: Incorrect integer value: '' for column 'new_id' at row 1

ファイル2行目にあるcreate tableの中で以下の部分が間違っていた。

`new_id` int(11) default NULL

型がintなのにinsert文では空文字を入れてる。

このnew_idというカラムは、

住所が廃止された場合(7)delete_flgを[ 1 ]とし、移行先の(1)idが判明していればこの項目に記載します。(現在未使用です)

住所データSQL - 仕様【住所.jp】

ドキュメントに未使用とあるので、手っ取り早く文字列型にしておく。

`new_id` varchar(11) default NULL

これでインポートできます。