Pythonでバッチ処理

2013.12.24

概要

見出しの「バッチ処理」とは、「ある目的を持ったプログラム群を順次実行して行くこと」であり、定められた時間や何らかの処理要求によって起動されます。しかし、この定義では広過ぎるので、「サーバマシン上で定時起動され、処理完了までに一定時間（数分〜数時間）を要し、データの収集、加工、出力を行うもの」と、限定的に考えます。

サーバ上で自動処理、と来たら、まずは「Bash」が候補に上がります。理屈の上では、大抵何でも出来てしまうのですが、どうしてもトリッキーと呼ばざるを得ないコードが混ざってくるという欠点があります。次に、筆者自身、最も使用経験が長い「Java」です。こちらはオブジェクト指向の言語なのでコードはとても奇麗に仕上がりますが、コンパイル＆配備、それから、運用開始後のソースコードのリビジョン管理の手間が意外に嵩みます。また、ベースのライブラリが巨大な割には、かなりの頻度でサードパーティ製のライブラリにお世話になることが多く、この管理も大変です。

という訳で、掲題の「Python」の出番です。Bashよりも遥かに読みやすいコードで、かつ、Javaのようにコンパイルする必要も無く、しかしながら高速に動作します。そして「Battery included」を標榜するだけのことがあって、ライブラリの充実ぶりも素晴らしいものがあります。…ただし、一つ大きな落とし穴があります。それは「日本語の処理」。RedHat、CentOS、MacOSに標準装備されているPythonは2.X系列で、ユニコードの扱いに多少の難があるバージョンです。ここだけは日本人のハンデとして多少の手間を覚悟しなければなりませんが、一旦コツを掴んでしまえば、致命的な問題になることは無く、それ以上に、Pythonで奇麗に記述することの長期的なメリットは大きいのではないかと思います。

基本的なシェルスクリプトの書き方や実行方法など、割愛して説明している部分も多くあります。UNIXコマンドラインに関する基礎的な知識があることを前提としている点はご了承下さい。

実装

まずはスクリプトの基本形です。1行目でPythonを処理系に指定していますが、「-u」オプションを付けておくと、標準出力がバッファされなくなり、処理内容をリアルタイムに確認したい場合に便利です。2,3行目ではこの後の説明で使用するモジュールをインポートしています。

#! /usr/bin/python -u

import codecs, os, sys

import argparse, csv, decimal, email, MySQLdb, smtplib

def main():

# ここにメインの処理を記述します

if __name__ == "__main__":

main()

「if __name__..」の後ろに直接メインの処理を書かずに「main」関数を定義しているのは、このプログラム自体をさらにモジュールとして呼び出す処理が記述される可能性を考慮しているためです。この後の節で解説するコードは、この「main」関数の中に記述されているものとし、これらの周辺の定義は省略します。

引数のパース

argparseモジュールを使用します。ArgumentParserのインスタンスに必要な引数を設定し、parse_argsメソッドを呼び出すとコマンドライン引数がパースされた結果が返されます。

parser = argparse.ArgumentParser(description='')

parser.add_argument('-date',help='Specify target date.')

parser.add_argument('-e',action='store_true',help='Execute.')

args = parser.parse_args()

print args

データベース接続

MySQLdbモジュールを使用します。基本的な使い方は、他の言語のDBドライバと大体同じです。ただし、Commitが自動ではないようで、更新処理を記述した場合はカーソル（cur）を閉じる前にコミット処理が必要になる点に注意が必要です。

db = MySQLdb.connect('localhost','root','','test')

cur = db.cursor()

cur.execute("SELECT id,v1,v2 FROM t ORDER BY id")

res = cur.fetchall()

cur.close()

db.close()

for row in res:

print row

ファイル操作

CSVを扱う場合には、csvモジュールを使用します。文字列の値にコンマ(,)や改行(\n)が混ざってくる可能性があるデータ（≒Excelが出力したデータ）を扱う場合は、str.splitなどで済ませず、このモジュールを使用するべきです。但し、このままでは日本語を扱えないという難点があります。

f = "test.csv"

for row in csv.reader(open(f)):

print row

メール通知

smtplibとemailパッケージを使用します。これもCSVの操作同様、日本語の扱いが少々複雑です。また下記の例は、localhost上でsendmail等のMTAが動作していることが前提です。

f = ‘foo@example.com'

t = 'bar@example.com'

msg = email.MIMEText.MIMEText('Hello!')

msg['Subject'] = ‘Test from Python'

msg['From'] = f

msg['To'] = t

msg['Date'] = email.Utils.formatdate()

smtp = smtplib.SMTP()

smtp.connect()

smtp.sendmail(f,[t],msg.as_string())

smtp.close()

LOGFILE=logs/main.py-$(date +%a).log

環境変数の利用

テスト環境と実稼動環境など、スクリプトの配備先によって動作を変えたい部分が出てくることがあります。このような場合に役に立つのが、シェルの提供する環境変数です。例えばcrontabの先頭では以下のように定義することができます。.bash_profileの中に記述するのであれば、exportを付けるのを忘れないようにして下さい。

RUNLEVEL=main

このようにそれぞれのホストに環境変数を定義しておけば、スクリプトの中で参照し、処理を分岐することができます。

os.environ["RUNLEVEL"]

通貨計算

インタプリタの実行例で説明します。Pythonでも実数の処理には浮動小数点演算が使われます。つまり、実数の計算は常に近似値で行われているということであり、例えば以下のように誤差が顕在化してくることがあります。通常の数値計算では問題のないオーダーですが、通貨計算の場合は、これが許容されなくなってきます。その場合に使えるのが、decimalパッケージです。こちらを使うと我々人間が計算するのと同じ精度で結果が得られます。

>>> 0.1 + 0.1 + 0.1

0.30000000000000004

>>> decimal.Decimal('0.1')+decimal.Decimal('0.1')+decimal.Decimal('0.1')

Decimal('0.3')

JavaやPHPと違って四則計算や丸め演算を通常の実数と同じように記述できるのが、Pythonの大きな強みです。

ロギング

loggingパッケージを使用します。モジュール毎に出力先を分岐したり、出力レベルを制御できたり、ファイルのローテションを自動化してくれたり、と中々に高機能ですが…実は、実際のシステムに組み込んだことがありません。

logging.basicConfig(filename='example.log',level=logging.INFO)

logging.info('I told you.')

logging.warning('Watch out!')

現在のところ、次の（簡素な）方法で運用しています。loggingパッケージの詳細を知りたい場合は以下のURLのチュートリアルが役に立つでしょう。

http://docs.python.jp/2/howto/logging.html#logging-basic-tutorial

Pythonのフレームワークではありませんが、厳格なログ管理が必要なく、とりあえず1週間分の日々の記録があれば事足りる、というような場面では以下のようなログファイルの定義が便利です。曜日付きのファイル名がLOGFILE変数に格納されますので、あとはこのファイルにスクリプトの出力をリダイレクトしてあげるだけです（リダイレクトについては後述します）。

# in .sh file (not in .py!)

LOGFILE=logs/main.py-$(date +%a).log

起動スクリプト（Bash）

Pythonの基本的な構文と、ここまでの説明を踏まえた時点で、恐らく、あなたが思っている以上に何でも出来るようになっていることに吃驚すると思います。あとは、出来上がったスクリプトをcrontabに登録したり、何かしらのインタフェースから呼び出して実行するばかりです。現在は以下のようなBashのスクリプトを実行パスの通ったディレクトリ（例えば${HOME}/binなど）に置いて起動するという方法を主に使っています。

/opt/examble.comディレクトリ下にbin,logsの2つのディレクトリがあって、binの下にmain.pyが格納されている、という状況を想定しています。最後の行のリダイレクトが少しトリッキーかもしれません。標準出力と標準エラー出力を1つにまとめ（2>&1）、標準出力とファイルに分岐（tee)しています。こうするとコンソールから実行した場合でもリアルタイムに結果を確認できつつ、最新の実行結果は常にファイルにも記録されている、という状態を実現できます。

#! /bin/bash

# RUNLEVEL=.. crontabや.bash_profileなどで定義

PROJECT=example

LOGFILE=logs/main.py-$(date +%a).log

cd /opt/example.com/{PROJECT}

python -u bin/main.py $@ 2>&1 | tee ${LOGFILE}

補足＆執筆予定

便利そう、必要そう、と感じつつもまだ組み込めていない機能たちです。

アサーション

前提条件の確認に便利です。最初の引数に指定した条件が成り立たなかった場合に、例外を送出してくれます。通常の条件判定との違うのは、判定が偽となった条件式をスタックトレース中に明示してくれる点と、インタプリタに「-O」オプションを付与することによって、実行コードから完全に除外することができる点です。

assert 1 < 2, "True."

assert 1 > 2, "False."

通常実行した場合、例外が送出されます。

$ python assert.py

Traceback (most recent call last):

File "assert.py", line 5, in <module>

assert 1 > 2, "False."

AssertionError: False.

「-O」（最適化オプション）を付けるとassertは評価されません。

python -O assert.py

テスト

バックアップ

日本語の処理