ロリポップレンタルサーバで、クーロンを使ってバッチを実行して結果を必ずメールで受信

JavaScript

今回は、ロリポップレンタルサーバでクーロン設定をおこなう方法、および、シェル実行結果をメールで受け取る方法について紹介します。


ロリポップは、crontabを直接編集してクーロンの設定をおこなうことはできないですが、クーロン設定画面で細かい起動設定をおこなう事ができます。
このクーロン設定画面では実行結果を受け取るメールアドレスの設定も可能であり、実行結果のメール受信が可能です。


メールでの受信は、バッチの実行結果によってメール本文の内容を変更する方法についても紹介していきます。


環境情報


バッチから実行するバッチは、Pythonで実装されていることを前提とします。


  • レンタルサーバ:ロリポップ
  • シェルから呼び出すバッチ:Python

WEB画面でクーロン設定をおこなう


ロリポップでのクーロン設定はWEB画面でおこないます。


ロリポップのユーザー専用ページにログインし、左メニューで「サーバーの管理」から「cron設定」を選択します。


ロリポップでのクーロン設定

cron設定画面がひらきます。
クーロンの設定、および、設定済クーロンの確認は、本画面でおこないます。



画面上にあるように、日付(月)・日付(日)・曜日・時間(時)・時間(分)をプルダウンで形式で選択する形で設定をおこないます。
筆者の契約しているプランはエコノミープランですが、以下の設定となります。
プランによって異なるのは最小実行間隔のみ(1分毎、5分毎)となります。


日付(月)

毎月、1月~12月

日付(日)

毎日、1日~31日

曜日

毎日、日曜日~土曜日

時間(時)

毎時、0時~23時

時間(分)

1分毎~10分毎、0分~59分


このように設定項目は豊富なので、ほとんどの設定のニーズに対応することができます。
しかし、crontabを直接編集すれば可能な設定はおこなうことはできません。
例えば、毎週、月曜日と金曜日だけ実行するという設定は、1つの設定ではおこなうことができません。
毎週、月曜日と金曜日に実行するといった設定をおこなう場合は、月曜日に実行するという設定と、金曜日に実行するという設定の2つの設定が必要となります。


複数の設定をおこなえばcrontabを直接編集することと同じ事ができるのであれば問題ないと思われますが、クーロンの設定には上限があるので注意が必要です。
クーロンの設定上限は、契約しているプランによって異なります。


 

cron登録数

最小実行間隔

エコノミー

1

5分毎

ライト

5

5分毎

スタンダード

10

1分毎

ハイスピード

10

1分毎

エンタープライズ

10

1分毎


メールでシェル実行結果を受け取る


クーロンの実行結果をメールで受け取ることができます。
設定は簡単で、メールアドレスをWEB画面で設定するだけです。



この設定をおこなうだけで、クーロンの実行結果が登録したアドレス宛にメールが送信されます。


しかし、メールが送信される条件は、クーロンの実行結果に出力がある場合、のみとなります。
つまり、標準出力やエラー出力といった出力がおこなわれない場合は、メールは送信されません。


メールでバッチが動作した結果を受信するためには、バッチを実行するシェルファイルに標準出力をおこなうように設定をおこなえばよいです。


#!/bin/bash
python cronSample.py
echo "バッチ処理が終了しました。" >&1

WEB画面で設定したメールアドレスには、標準出力した”クーロン設定での処理完了”とだけ記載されたメールが送信されます。


上記の設定では、クーロンが実行されたタイミングで必ずメールが送信されます。
本来であれば、クーロンの実行が失敗した時、といった一定の条件と一致した場合についてのみメールを送信したい場合がほとんどです。


その場合は、プログラムでの実行結果をシェルで受け取り、受け取った実行結果によって標準出力有無を判断する必要があります。


#!/bin/bash
ret=$(python cronSample.py)
if [ $ret = 0 ]; then
  echo "バッチ処理で異常が発生しました。" >&1
fi
#!/usr/local/bin/python3.4
# coding: utf-8
import sys

sys.stdout.write("0")

シェルの中身はシンプルで、Pythonで実行した結果が失敗(0)であった場合に標準出力をおこなっています。
結果、失敗したらメールが送信される、ということになります。


Pythonのプログラムでは、0を標準出力しているのみです。
これはサンプルプログラムでなので0を標準出力しているのみなのですが、実際のプラグラムでは、この部分を修正すればよいです。



Pythonのloggingを使って、用途別に複数のログファイルにログ出力する

phtyonロゴ

Pythonでは、ログ出力については標準機能として「logging」が準備されています。
実際のシステムでログ出力をおこなう場合、ログファイルが一種類であることは少なく、用途によってログファイルを分ける事が通常であると思います。


今回は、loggingを使って用途別に複数のログファイルにログ出力する方法を紹介します。


環境情報


  • Python 3.4.1

ログ管理クラス


ログファイルは以下の3種類とします。

  • アプリケーションログ
  • SQLログ
  • エラーログ

この3つのログファイルを出力するログ管理クラスを作成します。


#!/usr/local/bin/python3.4
# coding: utf-8
import logging
from logging import getLogger, FileHandler

#
# ログクラス
#
class Log:

    # -------------------------------------
    # コンストラクタ
    # -------------------------------------
    def __init__(self):

        # 1.アプリケーションログ
        l = logging.getLogger('APL')
        formatter = logging.Formatter(
            '%(asctime)s <%(levelname)s> : %(message)s')
        fileHandler = 
            logging.FileHandler('./logs/APL.log', mode='a')
        fileHandler.setFormatter(formatter)
        l.setLevel(logging.INFO)
        l.addHandler(fileHandler)

        # 2.SQLログ
        l = logging.getLogger('SQL')
        formatter = logging.Formatter(
            '%(asctime)s <%(levelname)s> : %(message)s')
        fileHandler = 
            logging.FileHandler('./logs/SQL.log', mode='a')
        fileHandler.setFormatter(formatter)
        l.setLevel(logging.INFO)
        l.addHandler(fileHandler)

        # 3.エラーログ
        l = logging.getLogger('ERROR')
        formatter = logging.Formatter(
            '%(asctime)s <%(levelname)s> : %(message)s')
        fileHandler = 
            logging.FileHandler('./logs/ERROR.log', mode='a')
        fileHandler.setFormatter(formatter)
        l.setLevel(logging.INFO)
        l.addHandler(fileHandler)

        Log.__instance = self

    # -------------------------------------
    # 4.アプリケーションログにINFOでログ出力
    # -------------------------------------
    def aplInfo(self, msg):
        log = logging.getLogger('APL')
        log.info(msg)

    # -------------------------------------
    # 5.SQLログにINFOでログ出力
    # -------------------------------------
    def sqlInfo(self, msg):
        log = logging.getLogger('SQL')
        log.info(msg)

    # -------------------------------------
    # 6.エラーログにERRORでログ出力
    # -------------------------------------
    def errorError(self, msg):
        log = logging.getLogger('ERROR')
        log.error(msg)

順番に説明していきます。
まずは「1.アプリケーションログ」です。


‘APL’という名前のロガーを作成し、ログファイルの出力先パスや、ログファイルの出力フォーマット、ログ出力のモードを指定して、ログファイル出力のためのファイルハンドラを作成します。


l = logging.getLogger('APL')
formatter = logging.Formatter('%(asctime)s <%(levelname)s> : %(message)s')
fileHandler = logging.FileHandler('./logs/APL.log', mode='a')
fileHandler.setFormatter(formatter)

次に、ロガーのログレベルを設定し作成したファイルハンドラもロガーに設定すれば完了です。


l.setLevel(logging.INFO)
l.addHandler(fileHandler)

「2.SQLログ」「3.エラーログ」も、設定するロガーが「SQL」「ERROR」といったように異なるだけで、「1.アプリケーションログ」と同様の作りです


4~6はログ出力のメソッドになります。


「4.アプリケーションログにINFOでログ出力」のメソッドである「aplInfo」はAPLロガーを使っています。
つまり、APL.logにログレベルINFOでログ出力をおこないます。


「5.SQLログにINFOでログ出力」のメソッドである「sqlInfo」も「aplInfo」と同様です。
異なる部分はSQLロガーを使っているということのみであり、SQL.logにログレベルINFOでログ出力をおこないます。


「6.エラーログにERRORでログ出力」は、ERRORロガーを使い、ERROR.logにログレベルERRORでログ出力をおこないます。


今回のサンプルでは3つのログ出力メソッドしか用意していませんが、必要であれば”アプリケーションログにERRORでログ出力””エラーログにINFOでログ出力”といったログ出力も可能な事がわかると思います。


ログを出力する


ログクラスを呼び出せば、ログクラスのコンストラクタでログ出力に必要なインスタンスを使ってくれるので、ログ出力をおこなう方の実装は極めてシンプルです。
以下のログ出力処理を、ログ出力したい箇所に実装すればログ出力がおこなわれます。


# インスタンス作成
log = Log()

# アプリケーションログをINFOで出力
log.aplInfo("アプリケーションログ")

# SQLログをINFOで出力
log.aplInfo(SQLログ")

# エラーログをERRORで出力
log.aplInfo(エラーログ")


ロリポップでPythonを使ってスクレイピング。モジュールを指定フォルダにインストールして環境構築。

phtyonロゴ

筆者は、レンタルサーバはロリポップを使っているのですが、Pythonでスクレイピングをするための環境構築で、ちょっと手こずりました。
原因がわかってしまえば単純なことだったので、記事として残しておきます。


環境情報


  • ロリポップレンタルサーバ
  • Python 3.4.1

モジュールのインストールコマンド


ロリポップでPythonのモジュールをインストールする方法は以下になります。
TeraTermにSSHで接続し、以下のコマンドを実行します。


/usr/local/bin/python -m pip install ※モジュール名

Pythonでスクレイピングをおこなうためには「beautifulsoup3」というモジュールが必要です。
しかし、上記のコマンドで「beautifulsoup3」をインストールしようとするとエラーが発生し、インストールすることができません。


$ /usr/local/bin/python -m pip install beautifulsoup3
Downloading/unpacking beautifulsoup3
  Real name of requirement beautifulsoup3 is beautifulsoup4
  Could not find any downloads that satisfy the requirement beautifulsoup3
Cleaning up...
No distributions at all found for beautifulsoup3
Storing debug log for failure in /home/users/0/oops.jp-sakusaku/.pip/pip.log
[oops.jp-sakusaku@users443 winningCollection]$ /usr/local/bin/python -m pip install beautifulsoup
Downloading/unpacking beautifulsoup
  Downloading BeautifulSoup-3.2.2.tar.gz
  Running setup.py (path:/tmp/pip_build_oops.jp-sakusaku/beautifulsoup/setup.py) egg_info for package beautifulsoup
    Traceback (most recent call last):
      File "<string>", line 17, in <module>
      File "/tmp/pip_build_oops.jp-sakusaku/beautifulsoup/setup.py", line 3
        "You're trying to run a very old release of Beautiful Soup under Python 3. This will not work."<>"Please use Beautiful Soup 4, available through the pip package 'beautifulsoup4'."
                                                                                                        ^
    SyntaxError: invalid syntax
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):

  File "<string>", line 17, in <module>

  File "/tmp/pip_build_oops.jp-sakusaku/beautifulsoup/setup.py", line 3

    "You're trying to run a very old release of Beautiful Soup under Python 3. This will not work."<>"Please use Beautiful Soup 4, available through the pip package 'beautifulsoup4'."

                                                                                                    ^

SyntaxError: invalid syntax

----------------------------------------
Cleaning up...
Command python setup.py egg_info failed with error code 1 in /tmp/pip_build_oops.jp-sakusaku/beautifulsoup
Storing debug log for failure in /home/users/0/oops.jp-sakusaku/.pip/pip.log

インストールとパス設定


インストール、および、Python実行時のパス設定にコツがあります。
前提として、以下のフォルダ構成を前提とします。


------
    |
    |---python
    |    |
    |    |---vendor         ・・・  Pythonの個別モジュールインストール先
    |
    |---test
      |
      |---scriping
           |
           |---scriping.sh  ・・・  作成したプログラムを実行するシェル
           |
           |---module       ・・・  作成プログラムの格納フォルダ

まず、インストールしたいモジュールである「beautifulsoup3」は個別モジュールインストール先である「python/vendor」にインストールする必要があります。
詳しい理由はわからないですが、「mysql-connector-python-rf」はインストール先を選択しなくても正常にインストールできたのですが、どうやら「beautifulsoup3」は自身で作成するフォルダにインストールする必要があるようです。


コマンドは以下になります。
「python」フォルダで以下のコマンドを実行します。


/usr/local/bin/python -m pip install BeautifulSoup --target ./vendor

このコマンドを実行することで、vendorフォルダ配下に「beautifulsoup3」のモジュールがインストールされます。


次にパス設定です。
実行するPythonのプログラムに「python/vendor」へのパスを設定する必要があります。
実行するPythonのプログラムは、「test/scriping/module」に格納されています。


#!/usr/local/bin/python3.4
# coding: utf-8
import os
import sys
sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '../../../python/vendor'))
from bs4 import BeautifulSoup 

これで、Pythonでスクレイピングのプログラムを実行する環境構築は完了です。



PythonでMySQLに接続するクラスを共通クラスとして部品化

phtyonロゴ

何かしらのシステムを構築しようとした場合、DBにデータを登録する、更新する、取得する、といった機能は必須になります。
Pythonも例外ではないのですが、いろいろなサイトでDBアクセスの一般的な方法は紹介しているのですが、実システムに沿ったような部品化についてはきちんとまとまっているサイトがありませんでした。


今回は、Pythonを使ってDBアクセス、および、DBアクセスクラスの部品化について紹介します。


環境情報


  • Python 3.4.1
  • MySQL 5.6.23

MySQLのデータベースには、以下のテーブルが存在することを前提とします。


CREATE TABLE test (
 test_id INT NOT NULL AUTO_INCREMENT,
 value INT NOT NULL,
 register_name VARCHAR(40) NOT NULL,
 register_date DATETIME NOT NULL,
 del_flg CHAR(1) NOT NULL,
 PRIMARY KEY (test_id)
) ENGINE = InnoDB DEFAULT CHARACTER SET utf8;

メインクラスとDBアクセスクラス


今回のクラス構成としては、メインクラスとDBアクセスクラスの2つとなります。
DBアクセスクラスは、DBアクセスに必要な最低限の機能を保持する事します。
メインクラスは、DBアクセスが必要な際にDBアクセスクラスを使います。


メインクラス

#!/usr/local/bin/python3.4
# coding: utf-8
from dbAccessor import dbAccessor

#
# メインクラス
#
class Main:

    # -----------------------------------
    # メインクラス
    #
    # DBアクセスクラスを呼び出すメイン処理
    # -----------------------------------
    def excecuteMain():
        obj = dbAccessor(
            '※DB名', '※ホスト名', '※ユーザ名', '※パスワード');

        # SELECT実行
        rows = obj.excecuteQuery('select test_id, value from test;')
        for row in rows:
            print("%d %s" % (row[0], row[1]))

        # INSERT実行
        num = obj.excecuteInsert("INSERT INTO test (
            value, register_name, register_date, del_flg) 
            VALUES (100,'管理者', '2020-12-20 00:00:00','管理者', 
            '0')")

        # UPDATE実行
        num = obj.excecuteUpdate(
            "UPDATE test SET value = 100 where test_id = 10")

        # DELETE実行
        num = obj.excecuteDelete(
            "DELETE FROM test WHERE test_id = 10")

# メイン処理を実行
Main.excecuteMain()

DBアクセスクラス

#!/usr/local/bin/python3.4
# coding: utf-8
from urllib.parse import urlparse
import mysql.connector

#
# DBアクセス管理クラス
#
class dbAccessor:

    # -----------------------------------
    # コンストラクタ
    #
    # コネクションを取得し、クラス変数にカーソルを保持する。
    # -----------------------------------
    def __init__(self, dbName, hostName, id, password):
        print("start:__init__")

        # DB接続URLを作成してパース
        url = 'mysql://' + id + ':' + password + '@' + 
            hostName + '/' + dbName
        parse = urlparse(url)

        try:
            # DBに接続する
            self.conn = mysql.connector.connect(
                host = parse.hostname,
                port = parse.port,
                user = parse.username,
                password = parse.password,
                database = parse.path[1:],
            )

            # コネクションの設定
            self.conn.autocommit = False

            # カーソル情報をクラス変数に格納
            self.conn.is_connected()
            self.cur = self.conn.cursor()
        except (mysql.connector.errors.ProgrammingError) as e:
            print(e)

        print("end:__init__")

    # -----------------------------------
    # クエリの実行
    #
    # クエリを実行し、取得結果を呼び出し元に通知する。
    # -----------------------------------
    def excecuteQuery(self, sql):
        print("start:excecuteQuery")

        try:
            self.cur.execute(sql)
            rows = self.cur.fetchall()
            return rows
        except (mysql.connector.errors.ProgrammingError) as e:
            print(e)

        print("end:excecuteQuery")

    # -----------------------------------
    # インサートの実行
    #
    # インサートを実行する。
    # -----------------------------------
    def excecuteInsert(self, sql):
        print("start:excecuteInsert")

        try:
            self.cur.execute(sql)
            self.conn.commit()
            return self.cur.rowcount
        except (mysql.connector.errors.ProgrammingError) as e:
            self.conn.rollback()
            print(e)

        print("end:excecuteInsert")

    # -----------------------------------
    # アップデートの実行
    #
    # アップデートを実行する。
    # -----------------------------------
    def excecuteUpdate(self, sql):
        print("start:excecuteUpdate")

        try:
            self.cur.execute(sql)
            self.conn.commit()
            return self.cur.rowcount
        except (mysql.connector.errors.ProgrammingError) as e:
            self.conn.rollback()
            print(e)

        print("end:excecuteUpdate")

    # -----------------------------------
    # デリートの実行
    #
    # デリートを実行する。
    # -----------------------------------
    def excecuteDelete(self, sql):
        print("start:excecuteDelete")

        try:
            self.cur.execute(sql)
            self.conn.commit()
            return self.cur.rowcount
        except (mysql.connector.errors.ProgrammingError) as e:
            self.conn.rollback()
            print(e)

        print("end:excecuteDelete")

    # -----------------------------------
    # デストラクタ
    #
    # コネクションを解放する。
    # -----------------------------------
    def __del__(self):
        print("start:__del__")
        try:
            self.conn.close()
        except (mysql.connector.errors.ProgrammingError) as e:
            print(e)
        print("end:__del__")

DBアクセスクラスの各メソッド


上記に紹介したサンプルプログラムについて説明していきます。


DBコネクションの取得と解放


DBコネクションの取得はコンストラクタで、解放はデストラクタで実施しています。


    def __init__(self, dbName, hostName, id, password):
        print("start:__init__")

        # DB接続URLを作成してパース
        url = 'mysql://' + id + ':' + password + '@' + 
            hostName + '/' + dbName
        parse = urlparse(url)

        try:
            # DBに接続する
            self.conn = mysql.connector.connect(
                host = parse.hostname,
                port = parse.port,
                user = parse.username,
                password = parse.password,
                database = parse.path[1:],
            )

            # コネクションの設定
            self.conn.autocommit = False

            # カーソル情報をクラス変数に格納
            self.conn.is_connected()
            self.cur = self.conn.cursor()
        except (mysql.connector.errors.ProgrammingError) as e:
            print(e)

        print("end:__init__")

    def __del__(self):
        print("start:__del__")
        try:
            self.conn.close()
        except (mysql.connector.errors.ProgrammingError) as e:
            print(e)
        print("end:__del__")

コンストラクタで通知されたパラメータを使って、MySQLに接続するURLを作成しています。
実際に作成されるURLは以下になります。


url = urlparse('mysql://※ユーザ名:※パスワード@※ホスト名/※DB名')

また、オートコミットをオフ(False)に設定しています。
オートコミットをオフにすることにより、トランザクション管理が容易になります。
これも、RDB(リレーションナルデータベース)を使ったシステムでは必須の機能となります。


こういって取得したDBコネクションとカーソル定義を、DBアクセスクラスのクラス変数として保持しています。
クラス変数として保持することにより、オブジェクト内でコネクションの使いまわしが可能となっています。


最後にデストラクタです。
デストラクタでコネクションを解放することにより、オブジェクト解放時に同時にコネクションも解放されます。


SELECT


SELECTを実行するメソッドは、実行するSQLを呼び出し側(メインクラス)から文字列として通知される形としています。
SQL文を通知して、SELECTした結果をそのまま返却しています。
非常にシンプルです。


    def excecuteQuery(self, sql):
        print("start:excecuteQuery")

        try:
            self.cur.execute(sql)
            rows = self.cur.fetchall()
            return rows
        except (mysql.connector.errors.ProgrammingError) as e:
            print(e)

        print("end:excecuteQuery")

INSERT


INSERT文もSELECTと同様で、呼び出し側から通知されたSQLを発行しているのみです。


SELECTと違うのは返り値となり、「rowcount」を返却しているのがポイントです。
「rowcount」は、INSERTした数になります。


    def excecuteInsert(self, sql):
        print("start:excecuteInsert")

        try:
            self.cur.execute(sql)
            self.conn.commit()
            return self.cur.rowcount
        except (mysql.connector.errors.ProgrammingError) as e:
            self.conn.rollback()
            print(e)

        print("end:excecuteInsert")

UPDATEとDELETEのメソッドも、UPDATEと同じメソッドになります。
UPDATEのメソッドは更新した数、DELETEのメソッドは削除した数、を返却します。