Help us understand the problem. What is going on with this article?

pythonでバッチスクリプトを書くときの雛形

仕事上Pythonでスクリプトをよく書くので、雛形コードを備忘録も兼ねて載せておきます。

python3系の雛形

概要

この雛形では以下のことをしています。

  • コマンドライン引数のパース(clickの利用)
  • 設定ファイル読み込み(configparserの利用)
  • ログ出力(loggingの利用)
  • ライブラリ読み込み
  • ライブラリの単体テスト

ファイルの配置

app_home/
       ├ bin/
       │   └  my_batch.py   #←実行するスクリプト
       ├ conf/
       │   └  my_batch.conf #←設定ファイル
       ├ lib/
       │   ├  __init__.py   #←モジュールをロードするのに必要
       │   └  my_lib.py     #←ライブラリ
       ├ tests/        
       │   └  test_my_lib.py#←単体テストコード
       └ log/               #←ログ出力先

内容

本体my_batch.pyの内容

import sys
import os
from configparser import ConfigParser
import click
import logging

# 親ディレクトリをアプリケーションのホーム(${app_home})に設定
app_home = os.path.abspath(os.path.join( os.path.dirname(os.path.abspath(__file__)) , ".." ))
# ${app_home}/libをライブラリロードパスに追加
sys.path.append(os.path.join(app_home,"lib"))

# 自前のライブラリをロード
from my_lib import MyLib

# コマンドライン引数のハンドリング. must_argは必須オプション、optional_argは任意オプション
@click.command()
@click.option('--must_arg','-m',required=True)
@click.option('--optional_arg','-o',default="DefaultValue")
def cmd(must_arg,optional_arg):
    # 自身の名前から拡張子を除いてプログラム名(${prog_name})にする
    prog_name = os.path.splitext(os.path.basename(__file__))[0]

    # 設定ファイルを読む
    config = ConfigParser()
    conf_path = os.path.join(app_home,"conf", prog_name + ".conf")
    config.read(conf_path)

    # ロガーの設定

    # フォーマット
    log_format = logging.Formatter("%(asctime)s [%(levelname)8s] %(message)s")
    # レベル
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    # 標準出力へのハンドラ
    stdout_handler = logging.StreamHandler(sys.stdout)
    stdout_handler.setFormatter(log_format)
    logger.addHandler(stdout_handler)
    # ログファイルへのハンドラ
    file_handler = logging.FileHandler(os.path.join(app_home,"log", prog_name + ".log"), "a+")
    file_handler.setFormatter(log_format)
    logger.addHandler(file_handler)

    # 処理開始
    try:
        # ログ出力
        logger.info("start")

        # コマンドライン引数の利用
        logger.error(f"must_arg = {must_arg}")
        logger.error(f"optional_arg = {optional_arg}")

        # ライブラリ呼び出し
        mylib = MyLib()
        logger.info(mylib.get_name())

        # 設定値読み込み
        logger.info(config.get("section1","key1"))
        logger.info(config.getboolean("section2","key2"))

        # 例外が発生しても・・・
        raise Exception("My Exception")

    except Exception as e:
        # キャッチして例外をログに記録
        logger.exception(e)
        sys.exit(1)

if __name__ == '__main__':
    cmd()

設定ファイルmy_batch.confの内容

[section1]
key1  = key1_value

[section2]
key2 = true

ライブラリmy_lib.pyの内容

class MyLib(object):
    def get_name(self):
        return "my_lib"

ライブラリの単体テストコードtest_my_lib.pyの内容

import sys,os
import unittest

# ../libをロードパスに入れる
app_home = os.path.abspath(os.path.join( os.path.dirname(os.path.abspath(__file__)) , ".." ))
sys.path.append(os.path.join(app_home,"lib"))

# ../テスト対象のライブラリのロード
from my_lib import MyLib

class TestMyLib(unittest.TestCase):

    def test_get_name(self):
        ml = MyLib()
        self.assertEqual("my_lib", ml.get_name())

if __name__ == '__main__':
    unittest.main()

実行

引数を指定しないで実行

$ python bin/my_batch.py

実行結果(clickの機能によりマニュアルが出る)

Usage: my_batch.py [OPTIONS]
Try "my_batch.py --help" for help.

Error: Missing option "--must_arg" / "-m".

引数を指定して実行

$ python bin/my_batch.py -m SpecifiedValue

実行結果

2019-06-28 16:42:53,335 [    INFO] start
2019-06-28 16:42:53,335 [   ERROR] must_arg = SpecifiedValue
2019-06-28 16:42:53,335 [   ERROR] optional_arg = DefaultValue
2019-06-28 16:42:53,335 [    INFO] my_lib
2019-06-28 16:42:53,336 [    INFO] key1_value
2019-06-28 16:42:53,336 [    INFO] True
2019-06-28 16:42:53,336 [   ERROR] My Exception
Traceback (most recent call last):
  File "bin/my_batch.py", line 62, in cmd
    raise Exception("My Exception")
Exception: My Exception

ログ(log/my_batch.log)にも同じ内容が出力される

2019-06-28 16:42:53,335 [    INFO] start
2019-06-28 16:42:53,335 [   ERROR] must_arg = SpecifiedValue
2019-06-28 16:42:53,335 [   ERROR] optional_arg = DefaultValue
2019-06-28 16:42:53,335 [    INFO] my_lib
2019-06-28 16:42:53,336 [    INFO] key1_value
2019-06-28 16:42:53,336 [    INFO] True
2019-06-28 16:42:53,336 [   ERROR] My Exception
Traceback (most recent call last):
  File "bin/my_batch.py", line 62, in cmd
    raise Exception("My Exception")
Exception: My Exception

単体テストの実行

bash-3.2$ python tests/test_my_lib.py

実行結果

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

tests以下の全テストコードtest_*.pyをまとめて実行する

$ python -m unittest  discover tests "test_*.py"

最後に

このコードはgithubでも公開しています→ https://github.com/fetaro/python-batch-template-for-v3

python2系の雛形

概要

この雛形では以下のことをしています。

  • オプションのパース
  • 設定ファイル読み込み
  • ログ出力
  • ライブラリ読み込み
  • ライブラリの単体テスト

python3系との違い

  • pyhon2系で動作
  • コマンドライン引数のパースにclickではなくoptparseを利用
  • ファイルの先頭に文字コーディングの宣言# -*- coding: utf-8 -*-が必要(ソースコードに日本語があるため)
  • 文字列のフォーマットに f式ではなく.format() を使わなければならない
  • ConfigParserのパッケージ名が大文字

ファイルの配置

app_home/
       ├ bin/
       │   └  my_batch.py   #←実行するスクリプト
       ├ conf/
       │   └  my_batch.conf #←設定ファイル
       ├ lib/
       │   ├  __init__.py   #←モジュールをロードするのに必要
       │   └  my_lib.py     #←ライブラリ
       ├ tests/        
       │   └  test_my_lib.py#←単体テストコード
       └ log/               #←ログ出力先

内容

my_batch.pyの内容

# -*- coding: utf-8 -*-
import sys
import os
from optparse     import OptionParser
from ConfigParser import ConfigParser
import logging

# 親ディレクトリをアプリケーションのホーム(${app_home})に設定
app_home = os.path.abspath(os.path.join( os.path.dirname(os.path.abspath(__file__)) , ".." ))

# ${app_home}/libをライブラリロードパスに追加
sys.path.append(os.path.join(app_home,"lib"))

# 自前のライブラリをロード
from my_lib import MyLib

if __name__ == "__main__" :
    # 自身の名前から拡張子を除いてプログラム名(${prog_name})にする
    prog_name = os.path.splitext(os.path.basename(__file__))[0]

    # オプションのパース
    usage = "usage: %prog (Argument-1) [options]"
    parser = OptionParser(usage=usage)
    parser.add_option("-d", "--debug",dest="debug", action="store_true", help="debug", default=False)

    # オプションと引数を格納し分ける
    (options, args) = parser.parse_args()

    # 引数のチェック
    if len(args) != 1:
        sys.stderr.write("argument error. use -h or --help option\n")
        sys.exit(1)

    # 設定ファイルを読む
    config = ConfigParser()
    conf_path = os.path.join(app_home,"conf", prog_name + ".conf")
    config.read(conf_path)

    # ロガーの設定

    # フォーマット
    log_format = logging.Formatter("%(asctime)s [%(levelname)8s] %(message)s") 
    # レベル
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    # 標準出力へのハンドラ
    stdout_handler = logging.StreamHandler(sys.stdout)
    stdout_handler.setFormatter(log_format)
    logger.addHandler(stdout_handler)
    # ログファイルへのハンドラ
    file_handler = logging.FileHandler(os.path.join(app_home,"log", prog_name + ".log"), "a+")
    file_handler.setFormatter(log_format)
    logger.addHandler(file_handler)


    # 処理開始
    try:
        # ログ出力
        logger.info("start")
        logger.error("arg1 = {0}".format(args[0]))

        # オプション取得
        logger.info(options.debug)

        # ライブラリ呼び出し
        mylib = MyLib()
        logger.info(mylib.get_name())

        # 設定値読み込み
        logger.info(config.get("section1","key1"))
        logger.info(config.getboolean("section2","key2"))

        # 例外が発生しても・・・
        raise Exception("My Exception")

    except Exception as e:
        # キャッチして例外をログに記録
        logger.exception(e)
        sys.exit(1)

my_batch.confの内容

[section1]
key1  = key1_value

[section2]
key2 = true

my_lib.pyの内容

class MyLib(object):
    def get_name(self):
        return "my_lib"

単体テストコードtest_my_lib.pyの内容

# -*- coding: utf-8 -*-
import sys,os
import unittest

# ../libをロードパスに入れる
app_home = os.path.abspath(os.path.join( os.path.dirname(os.path.abspath(__file__)) , ".." ))
sys.path.append(os.path.join(app_home,"lib"))

# ../テスト対象のライブラリのロード
from my_lib import MyLib

class TestMyLib(unittest.TestCase):

    def test_get_name(self):
        ml = MyLib()
        self.assertEqual("my_lib", ml.get_name())

if __name__ == '__main__':
    unittest.main()

実行

bash-3.2$ python bin/my_batch.py argument1
2016-08-16 23:25:03,492 [    INFO] start
2016-08-16 23:25:03,492 [   ERROR] arg1 = argument1
2016-08-16 23:25:03,492 [    INFO] False
2016-08-16 23:25:03,492 [    INFO] my_lib
2016-08-16 23:25:03,492 [    INFO] key1_value
2016-08-16 23:25:03,492 [    INFO] True
2016-08-16 23:25:03,492 [   ERROR] My Exception
Traceback (most recent call last):
  File "bin/my_batch.py", line 73, in <module>
    raise Exception("My Exception")
Exception: My Exception

ログ(log/my_batch.log)の内容

2016-08-16 23:25:03,492 [    INFO] start
2016-08-16 23:25:03,492 [   ERROR] arg1 = argument1
2016-08-16 23:25:03,492 [    INFO] False
2016-08-16 23:25:03,492 [    INFO] my_lib
2016-08-16 23:25:03,492 [    INFO] key1_value
2016-08-16 23:25:03,492 [    INFO] True
2016-08-16 23:25:03,492 [   ERROR] My Exception
Traceback (most recent call last):
  File "bin/my_batch.py", line 73, in <module>
    raise Exception("My Exception")
Exception: My Exception

単体テストの実行結果

bash-3.2$ python tests/test_my_lib.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

tests以下の全テストコードtest_*.pyをまとめてテストする場合

bash-3.2$ python -m unittest  discover tests "test_*.py"

githubでもこのファイルの内容を公開しています。お好きにどうぞ→ https://github.com/fetaro/python-batch-template

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away