【cron】定期的に処理を行う【MacOS】【Linux】

# Pythonによるスクレイピング&機械学習
[Pythonによるスクレイピング&機械学習 開発テクニック BeautifulSoup,scikit-learn,TensorFlowを使ってみよう](https://www.amazon.co.jp/Python%E3%81%AB%E3%82%88%E3%82%8B%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0-%E9%96%8B%E7%99%BA%E3%83%86%E3%82%AF%E3%83%8B%E3%83%83%E3%82%AF-BeautifulSoup-scikit-learn-TensorFlow%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86/dp/4802610793)より
学習ログ。
# 前回
[2-4](http://qiita.com/shunyooo/items/b408b8d61f9f73b21da7)では

- WebAPIからのデータ取得

を行いました。
リクエスト、レスポンスJSON取得、pythonデータにデコードしてごにょごにょできるようになりました。

# この章で得られるもの
2-5では、**cronによる定期的な処理**について学んでいきます。
定期的な処理を行う方法はOSによって異なるそうですが、ここではmacOS/Linuxで扱えるcronについてやっていきます。

## コード
[こちら(Git)](https://github.com/shunyooo/scraping_ML_DL/tree/master/Section2)にて

# 環境
Python 3.6.0
macOS Sierra 10.12.4

# 定期的な処理について
Webで公開されるデータの中には、定期的にデータが更新されるものが多くあります。為替情報であったり、ニュース系であったり、、
これらに対応するため、**定期的な処理を行う必要があります。**
macOSやLinuxでは「cron」というデーモンプロセスが用意されており、スクリプトを自動で実行することができます。
Windowsでは「タスク スケジューラ」というものが用意されているようです。

定期的な処理は一般的に以下の用途で使われています。

1. データ収集などアプリ内で必要となる定期処理
2. ログやバックアップなどシステム関連の定期処理
3. システムが正しく動作しているか定期的に監視する処理

今回扱う定期的なクローリングは1に相当するでしょう。

# 為替情報を毎日保存
[為替情報の確認が行えるAPI](http://api.aoikujira.com/kawase/)から毎日一回為替情報を取得するAPIを作ってみます。
取得したデータは「2017-10-10.json」みたいに日付.jsonでkawaseフォルダに保存することにします。
実行する前にkawaseフォルダを作っておいてください。

```bash:terminal
$ mkdir kawase
```

## 為替情報の取得

実装は以下。

```py3:2-5everyday-kawase.py
import urllib.request as request
import datetime
import json

# Web APIにアクセス
API = "http://api.aoikujira.com/kawase/get.php?code=USD&format=json"
json_str = request.urlopen(API).read().decode("utf-8")
data = json.loads(json_str)
print("1USD="+data["JPY"]+"JPY")

# 保存ファイル名を決定
t = datetime.date.today()
fname = "./kawase/" + t.strftime("%Y-%m-%d") + ".json"
with open(fname, "w", encoding="utf-8") as f:
	f.write(json_str)
```

実行すると、現在のUSD/JPYが出力され、kawaseフォルダに以下のようなjsonファイルが追加されます。いい感じです。

```json:kawase/2017-06-07.json
{"result":"ok","basecode":"USD","update":"2017-06-07 11:32:13","source":"*","API_URL":"http:\/\/api.aoikujira.com\/kawase\/","ARS":"16.01599","UYU":"28.38073","ANG":"1.79011","CAD":"1.34427","CUP":"1.00000","GTQ":"7.35136","KYD":"0.82000","CRC":"569.13622","COP":"2895.17964","JMD":"129.35066","CLP":"669.85302","DOP":"47.46295","TTD":"6.76732","NIO":"29.95246","HTG":"62.56540","PAB":"1.00000","BSD":"1.00000","BMD":"1.00000","PYG":"5566.33649","BBD":"2.00000","BRL":"3.27742","VEF":"9.98505","BZD":"1.99875","PEN":"3.26720","BOB":"6.91000","HNL":"23.44206","MXN":"18.24170","XCD":"2.70000","XPF":"105.89094","INR":"64.45486","IDR":"13307.82476","AUD":"1.32590","KHR":"4066.40306","SGD":"1.38016","LKR":"152.43947","SCR":"13.66088","THB":"33.99208","NZD":"1.39179","NPR":"103.09840","PKR":"104.84148","BDT":"80.60001","FJD":"2.07458","PHP":"49.52515","BND":"1.37984","VND":"22723.91451","MOP":"8.02815","MYR":"4.26776","MMK":"1355.13455","LAK":"8192.40867","CNY":"6.79590","TWD":"30.06787","JPY":"109.49471","KRW":"1122.32400","HKD":"7.79411","ISK":"97.93109","ALL":"118.81244","GBP":"0.77530","UAH":"26.23663","HRK":"6.57614","CHF":"0.96285","SEK":"8.67258","RSD":"108.56676","CZK":"23.36005","DKK":"6.60112","NOK":"8.46523","HUF":"273.44055","BGN":"1.73492","BYN":"1.86462","PLN":"3.72342","MDL":"18.19114","EUR":"0.88756","RON":"4.05253","RUB":"56.51578","AZN":"1.68616","AED":"3.67313","AMD":"482.21997","YER":"250.26565","ILS":"3.54422","IQD":"1166.62229","IRR":"32451.01537","UZS":"3874.72042","OMR":"0.38507","KZT":"312.85440","QAR":"3.64203","KWD":"0.30317","GEL":"2.41515","SAR":"3.75049","TMT":"3.50022","TRY":"3.51320","BHD":"0.37737","JOD":"0.71004","LBP":"1512.09394"}

```

## cronの設定
**crontab**からcronの設定を行います。crontabを起動する際には「-e」オプションを付けて起動します。初めてcrontabを起動する時は、設定には何も記述されていないはずです。

```bash:terminal
$ crontab -e
```

<img width="682" alt="スクリーンショット 2017-06-07 14.31.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/180836/3e57231b-3911-f55c-e4d8-0cc77241d8e3.png">


では、毎朝7時に先ほどのコードを実行させるように設定してみます。
vimの勉強にもなります。[こちらvim参考です。](http://qiita.com/okamos/items/c97970ab34ff55ff3167)

crontabの記述方法は以下の通りです。

```text:crontab
(分)(時)(日)(月)(曜日) 実行するコマンドのパス
```
各要素の設定可能な数値は以下の通り。

|項目|数値|
|:---:|:---|
|分|0-59|
|時|0−23|
|日|1-31|
|月|1-12|
|曜日|0-7(0または7は日曜日)|

また、この数値の指定の仕方は以下の通り。

|名前|利用例|説明|
|:--|:--|:--|
|リスト|0,10,30|0,10,30という値をそれぞれ指定|
|範囲|1-5|0,10,30という値をそれぞれ指定|
|間隔|*/10|10,20,30と10間隔指定|
|ワイルドカード|*|ワイルドカードで指定|


なので、毎朝７時に実行となると以下のような記述になります。（パスは絶対パスで指定します）

```bash:crontab
0 7 * * * python3 /Users/syunyo/Desktop/MyDev/python_dev/scraping_ML_DL/Section2/2-5\ everyday-kawase.py
```

ちなみに全てワイルドカード「*」にすると、毎分実行になります。

### ちょっとチートシート
使いがちなやつまとめておきます

```bash:crontab
# 毎日24:00
0 0 * * * <Command>

# 毎時0分
0 * * * * <Command>

# 毎週月曜7:00
0 7 * * 1 <Command>

# 毎週金曜22:00
0 22 * * 5 <Command>

# 毎月1日の7:00
0 7 1 * * <Command>
```

### 環境変数に注意
cron実行時には、環境設定が最低限しか用意されておらず、基本的にコマンドが通りません。
確かめてみましょう。

```bash:crontab
* * * * * env >/tmp/cron_env
```

```bash:terminal
$ cat /tmp/cron_env
SHELL=/bin/sh
USER=syunyo
PATH=/usr/bin:/bin
PWD=/Users/syunyo
SHLVL=1
HOME=/Users/syunyo
LOGNAME=syunyo
_=/usr/bin/env
```

ああ全然設定されていません。。
解決方法として、

- コマンドにも絶対パスを使用する。
- ```bash -l -c 'コマンド'``` とする。-lでログインシェルとして動くようになる。
- ```source ~/.bash_profile``` を書いてから目的のコマンドを記載する。

などなど方法があるようです。[参考](http://progmemo.wp.xdomain.jp/archives/790)
今回は２つ目のやりかたで解決しました。

```bash:crontab
bash -l -c 'python3 /Users/syunyo/Desktop/MyDev/python_dev/scraping_ML_DL/Section2/2-5\ everyday-kawase.py'
```

### 文字コードに注意
さあ実行だ、と実行してみると以下のエラーが。。


```bash:terminal
UnicodeEncodeError: 'ascii' codec can't encode characters in position 2-3: ordinal not in range(128)
```

どうやら文字コード系のエラーなようです。環境変数をなんか設定するのかなと弄ってみてもうまくいきません。しかし、コードに先頭に```# coding: utf-8```と明示的に文字コードを宣言してあげると上手くいきました。

```py3:2-5everyday-kawase.py
# coding: utf-8
import urllib.request as request
import datetime
import json
import os.path

...
```

### カレントディレクトリに注意
漸く実行だ、と実行してみると、以下のエラーがああ。

```bash:terminal
python_dev/scraping_ML_DL/Section2/2-5 everyday-kawase.py", line 17, in <module>
    with open(fname, "w", encoding="utf-8") as f:
FileNotFoundError: [Errno 2] No such file or directory: './kawase/2017-06-07.json'
```
ホームディレクトリにkawaseフォルダがないぞ、と言っています。ホームディレクトリで実行したつもりはないんですが。。

どうやら、**cronでは実行する際のカレントディレクトリはホームディレクトリにセットされてしまうようです。**
解決策として

- コード内で絶対パスを指定する。
- crontabで実行する絶対パスを指定する。

などがあげられます。
自分はコードを汚したくなかったので、2つ目の方法を取ることにしました。

ちなみに1つ目の方法も試しました。動的に対応するため、os.path.abspath()メソッドで絶対パスがとれるかと思いきや、これもホームディレクトリが取れてしまいます。手打ちしかないんでしょうか。。

### 最終的なcrontab

若干環境変数なども触った結果、以下のようになりました。

```bash:crontab
PYTHONIOENCODING = 'utf-8'
LANG=ja_JP.UTF-8

0 7 * * * cd /Users/syunyo/Desktop/MyDev/python_dev/scraping_ML_DL/Section2/; bash -l -c 'python3 /Users/syunyo/Desktop/MyDev/python_dev/scraping_ML_DL/Section2/2-5\ everyday-kawase.py'
```

一行が長い。。。
一応これで毎朝7時に実行できます。
ディレクトリパスなどは違うと思うので、絶対パスを調整して実行してください。


## cronの通知 (Appendix)
cronで実行された時に、その標準出力をメールで通知したり、テキストで保存しておいたりの設定がcrontabで出来るようです。

僕の場合```/var/mail/syunyo```で保存されていたので、デフォルト値は```/var/mail/{ユーザ名}```のようです。

この指定には```MAILTO```という環境変数をいじります。ローカルディレクトリ以外にメールアドレスを指定することもできます。

```bash:crontab
PYTHONIOENCODING = 'utf-8'
LANG = ja_JP.UTF-8
MAILTO = "bokunomailadress@yahoo.co.jp"

0 7 * * * cd /Users/syunyo/Desktop/MyDev/python_dev/scraping_ML_DL/Section2/; bash -l -c 'python3 /Users/syunyo/Desktop/MyDev/python_dev/scraping_ML_DL/Section2/2-5\ everyday-kawase.py'
```

わくわくしながら待ちましたが、届きません。
どうやら、スパム対策として、ISP側で許可した特定のサーバ以外のSMTP（通常使用されるTCPポートの25番）の送信をブロックする処置(Outbound Port 25 Blocking)が行われているかららしいです。

そこでGmailをSMTPサーバとして利用して、メール送信をするように設定します。

[ここを参考に...](http://qiita.com/tmsanrinsha/items/75e06fbdd7e12409bb02)

届きました。嬉しい。これで1ドルいくらか悩む生活とはおさらばです。

<img src = "https://qiita-image-store.s3.amazonaws.com/0/180836/017cd9bd-d785-c8e6-c6ee-5b812653c5ce.png" width=500>