3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お名前.comのレンタルサーバRSプランでphpを定期的に自動実行させる方法

Posted at

目的

タイトルの通り、お名前.comのレンタルサーバをRSプランで借りてる人が、phpを定期実行させること。

いま私は、クラウドファンディングサイトを構築したくて、金融機関名や支店名の照合を行うためにデータベースに金融機関名、支店名を登録しているが、これを1日に1度(毎日0:00 AM)とかでインターネット上のjsonに同期したい。

ネットに情報がほとんどなかったし、公式サイトにも「OSはLinuxです」程度のことしか書いていなく、どのコマンドが使えるかとか、ディレクトリ構造がどうなのかとかの説明がほとんどなく苦労した。なので次回からは楽できるようにここにメモしておく。

手順

  1. SSH通信をする
  2. CRONを設定する。

1. SSH通信をする

SSH通信とは、「コンピュータを遠隔操作する通信」の手法の一つ。
お名前.comのレンタルサーバRSプランのコントロールパネルには、phpの定期実行を行うためのツールが用意されていないので、レンタルサーバを遠隔操作(ハッキング)して直接設定を変更しちゃえ!ということです。
手順は次の通りです。

  1. SSH鍵を作る
  2. パソコンにTera Termをインストールする
  3. Tera Termからレンタルサーバにログインする

1-1. SSH鍵を作る

SSH鍵とは公開鍵暗号の鍵です。SSHは、暗号通信を行うことで、第三者による傍聴や改竄、なりすましなどを防いでいます。

  1. https://cp.onamae.ne.jp/server/ssh へ行く
  2. 「SSH Key を追加」または「はじめる」ボタンを押す
  3. 指示に従って進めると、「レンタルサーバが持っておくべき鍵」(公開鍵)と「ユーザが持っておくべき鍵」(秘密鍵)が作られる。
  4. 秘密鍵ファイルをダウンロードし、大切に保管する。

1-2. パソコンにTera Termをインストールする

SSH通信を行うのに便利なソフト。

  1. https://ja.osdn.net/projects/ttssh2/releases/ から最新のexeファイルをダウンロード
  2. ダウンロードしたexeファイルを実行

1-3. Tera Termからレンタルサーバにログインする

  1. https://cp.onamae.ne.jp/server/ssh に行く
  2. 1-1で作成した鍵の「詳細」をクリックする
  3. ホスト名、ポート番号、ユーザ名をメモする
  4. Tera Termを起動する。
  5. 次の写真のようにする。
    image.png
  6. 次の写真のようにする
    image.png
  7. 次のような画面(ターミナル)になれば成功
    image.png

2. CRONを設定する

CRONとは「決まった時刻に決まった処理をする」ことを管理できる、UNIX系OSの常駐プログラムです。
例えばphpでサンタクロース派遣プログラム.phpを作ったら、
0 23 24 12 * /usr/bin/php /home/r0000000/cron/サンタクロース派遣プログラム.php
みたいに登録しておくと、毎年12月24日の23:00頃に自動でサンタクロース派遣プログラムが起動するようになるというわけです。

今回は例として、mysqlのデータベース上に次のような日本の金融機関コードや名前、そしてそれらの支店コードや名前を記載した一覧表があるとします。これを Cron + PHP で毎日更新することを考えましょう。

テーブル「banks」

bank_code TEXT| bank_name TEXT
:-:|:-:|:-:
0001|みずほ銀行
0005|三菱UFJ銀行
:|:

テーブル「bank_branches」

bank_code TEXT branch_code TEXT branch_name TEXT
0001 001 東京営業部
0001 004 丸の内中央支店
: : :

テーブル「banks_json_hash」

hash TEXT
(金融機関データソースファイルのmd5ハッシュ値)

テーブル「branches_json_hash」

bank_code TEXT hash TEXT
0001 (みずほ銀行の支店データソースファイルのmd5ハッシュ値)
0005 (三菱UFJ銀行 〃)
: :

(名前が「hash」で終わる2つのテーブルは、生データが更新されている(=今回phpファイルにて同期する必要がある)か、それとも更新されていない(=phpファイルは今回何もしなくていい)かを判断するために用意しています。)

手順は大まかに次の通りです。

  1. 自動実行させたいphpファイルを作る
  2. phpファイルの置き場を作る
  3. VIエディタでphpファイルを試しに登録する
  4. VIエディタでphpファイルをちゃんと登録する

2-1. 自動実行させたいphpファイルを作る

コードは次のようになります。金融機関(1200つ強あります)の一覧をbanksテーブルに同期するbanks.phpと、各金融機関の支店の一覧をbank_branchesテーブルに同期するbranches01.phpbranches89.phpに分かれています。
branchesXX.phpを5つに分けたのは、jsonファイルを提供してくれるサーバの負荷を分散させるためです。
「毎日0時0分に1200回も問い合わせを行う」よりは、「毎日0時0分に240回」、「毎日1時0分に240回」、...、「毎日4時0分に240回」に分けたほうが負荷が集中しません。

bank.php
<?php

require_once("funcs.php");
$ログ = new ログマネージャ($ログファイルパス, "banks");
$ログ -> 書く("金融機関のデータが更新されているか確認。");
if(金融機関のデータが更新されている($金融機関データのjsonファイルのパス, $pdo))
{
    $ログ -> 書く("金融機関のデータが更新されているので同期開始");
    $banks = new Banksテーブル($pdo); // データベース上のテーブルをいじるクラスを自作しました
    $banks -> 空にする();

    foreach(
        get金融機関データ($金融機関データのjsonファイルのパス) 
        as $金融機関コード => $金融機関の情報
    )
    {
        $金融機関名 = $金融機関の情報["name"];
        $banks -> 行を挿入する($金融機関コード, $金融機関名);
    }
    $ログ -> 書く("金融機関のデータ同期終了");
}
else
{
    $ログ -> 書く("金融機関のデータは更新されていませんでした。");
}
branches89.php,「8」と「9」は各自で置き換えてください
<?php

require_once("funcs.php");
$ログ = new ログマネージャ($ログファイルパス, "branches89");
$金融機関データ = get金融機関データ($金融機関データのjsonファイルのパス);
$金融機関コードたち = 金融機関コードを集める($pdo, $金融機関データ);
$ログ -> 書く("コードの千の位が8または9である各金融機関の支店データが更新されているか確認。");
foreach($金融機関コードたち as $金融機関コード)
{
    if(substr($金融機関コード, 0, 1) != "8" && substr($金融機関コード, 0, 1) != "9")
    // ここで各金融機関の支店情報について、サーバに問い合わせる/問い合わせないを決めています
        continue;

    $bank_branches = new Bank_branchesテーブル($pdo); 
    if(!($bank_branches -> 金融機関がある($金融機関コード)))
    {
        $ログ -> 書く("金融機関" . $金融機関コード . "は、生データに存在し、データベースに存在しませんでした。よって追加を行います。");
        foreach(get支店データ(支店データのjsonファイルのパス($金融機関コード)) as $支店コード => $支店の情報)
            $bank_branches -> 行を挿入する($金融機関コード, $支店コード, $支店の情報["name"]);
    }
    else
    if(!array_key_exists($金融機関コード, $金融機関データ))
    {
        $ログ -> 書く("金融機関" . $金融機関コード . "は、データベースに存在し、生データに存在しませんでした。よって消去を行います。");
        $bank_branches -> 金融機関を消去する($金融機関コード);
    }
    else
    if(支店のデータが更新されている($金融機関コード, $pdo)) 
    {
        $生データのハッシュ値 = md5(file_get_contents(支店データのjsonファイルのパス($金融機関コード)));
        $データベースのハッシュ値 = $pdo -> query("SELECT hash FROM branches_json_hash WHERE bank_code = '" . $金融機関コード . "'") -> fetch()["hash"];
        $ログ -> 書く("金融機関" . $金融機関コード . "の支店データについて、生データ(" . $生データのハッシュ値 . ")とデータベース(" . $データベースのハッシュ値 . ")でハッシュ値が一致しませんでした。よって更新を行います。");
        $bank_branches -> 金融機関を消去する($金融機関コード);
        foreach(get支店データ(支店データのjsonファイルのパス($金融機関コード)) as $支店コード => $支店の情報)
            $bank_branches -> 行を挿入する($金融機関コード, $支店コード, $支店の情報["name"]);
    }
    else
    {
        $ログ -> 書く("金融機関" . $金融機関コード . "の支店データについて、生データとデータベースでハッシュ値が一致しました。更新の必要はありません。");
    }
}
$ログ -> 書く("条件に合致する各金融機関の支店データ確認が終了しました。");

さて、上2つのファイルは処理をわかりやすくするため、ほとんどメソッドや関数の呼び出しとなっております。
それらのメソッドや関数を定義しているのが、次のfuncs.phpです。

funcs.php,最初6行について、右辺値は各自で指定してください
$データベース名 = ;
$ホスト名 = ;
$文字コード = ;
$ユーザ名 = ; // レンタルサーバではなく、データベースのユーザ名です
$パスワード = ; // 同上
$カレントディレクトリの絶対パス = ; // 手順2-3に進むまでの間は "./" で構いません。手順2-3では"/home/(レンタルサーバのユーザ名、rから始まるやつ)/cron/" としてください

// ----

if (!file_exists($カレントディレクトリの絶対パス . "logs"))
    mkdir($カレントディレクトリの絶対パス . "logs");

$pdo = new PDO(
        "mysql:dbname=".$データベース名.";host=".$ホスト名.";charset=". $文字コード ."",
        $ユーザ名,
        $パスワード,
        array(
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, /*SQLがエラーの際に例外発生*/
            PDO::ATTR_EMULATE_PREPARES => false,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_BOTH, /*カラム名とカラム番号の両方をキーとする連想配列で取得*/
            PDO::ATTR_PERSISTENT => false, /*スクリプト終了後はDB切断*/
            PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true /*クエリの結果がメモリに乗らないくらい大きいときはfalseとせよ*/
        )
);

$ログファイルパス = $カレントディレクトリの絶対パス . "logs/" . date("Y-m-d-H-i-s") . ".txt";

class ログマネージャ // 本当はシングルトンにしたほうが安全
{
    private $パス;
    private $fp;
    private $開始時刻;

    function __construct($パス, $コメント)
    {
        $this -> パス = $パス;
        $this -> fp = fopen($this -> パス, "w");
        fwrite($this -> fp, $コメント . "\r");
        $this -> 開始時刻 = microtime(true);
    }

    function 経過秒()
    {
        $経過マイクロ秒 = microtime(true) - $this -> 開始時刻;
        $経過秒_as_str = (($経過マイクロ秒)) . "";
        $整数部分 = preg_split("/\./", $経過秒_as_str)[0];
        $小数部分 = substr(preg_split("/\./", $経過秒_as_str)[1], 0, 3);
        return $整数部分 . "." . $小数部分;
    }

    function __destruct()
    {
        fwrite($this -> fp, "killed at " . $this -> 経過秒());
        fclose($this -> fp);
    }

    function 書く($内容)
    {
        fwrite($this -> fp, "msg at " . $this -> 経過秒() . " >> ");
        fwrite($this -> fp, $内容 . "\r");
    }
}

$金融機関データのjsonファイルのパス = 
"https://raw.githubusercontent.com/zengin-code/source-data/master/data/banks.json";
function 支店データのjsonファイルのパス($金融機関コード)
{
    return "https://raw.githubusercontent.com/zengin-code/source-data/master/data/branches/".$金融機関コード.".json";
}

class Banksテーブル
{
    private $pdo;

    function __construct($pdo)
    {
        $this -> pdo = $pdo;
    }

    function 空にする()
    {
        $this -> pdo -> query("DELETE FROM banks");
    }

    function 行を挿入する($金融機関コード, $金融機関名)
    {
        $this -> pdo -> query("INSERT INTO banks VALUES('".$金融機関コード."', '".$金融機関名."')");
    }
}

class Bank_branchesテーブル
{
    private $pdo;
    function __construct($pdo)
    {
        $this -> pdo = $pdo;
    }
    function 金融機関がある($金融機関コード)
    {
        return !!(
            $this -> pdo -> query("SELECT count(*) AS is_exists FROM bank_branches WHERE bank_code = '". $金融機関コード . "'") -> fetch()["is_exists"]
        );
    }
    function 行を挿入する($金融機関コード, $支店コード, $支店名)
    {   
        $this -> pdo -> query("INSERT INTO bank_branches VALUES('".$金融機関コード."', '". 
$支店コード . "', '".$支店名."')");        
    }
    function 金融機関を消去する($金融機関コード)
    {
        $this -> pdo -> query("DELETE FROM bank_branches WHERE bank_code = '" . $金融機関コード . "'");
    }
}

function 金融機関のデータが更新されている($パス, $pdo)
{
    $行 = $pdo -> query("SELECT hash FROM banks_json_hash") -> fetch();
    if($行)
    {
        $データベース上のハッシュ値 = $行["hash"];
        $今回計算したハッシュ値 = md5(file_get_contents($パス));
    }

    if((!$行) || $データベース上のハッシュ値 != $今回計算したハッシュ値)
    {
        $pdo -> query("DELETE FROM banks_json_hash");
        $pdo -> query("INSERT INTO banks_json_hash VALUES('" . $今回計算したハッシュ値 . "')");
    }
    // 新しいハッシュ値に更新

    return ((!$行) || $データベース上のハッシュ値 != $今回計算したハッシュ値);
}

function get金融機関データ($jsonパス)
{
    return json_decode(file_get_contents($jsonパス), true);
}

function get支店データ($jsonパス)
{
    return get金融機関データ($jsonパス);
}

function 金融機関コードを集める($pdo, $金融機関データ)
{
    $金融機関コードたち = [];
    foreach($pdo -> query("SELECT DISTINCT bank_code FROM bank_branches") as $行)
        array_push($金融機関コードたち, $行["bank_code"]);
    foreach($金融機関データ as $金融機関コード => $金融機関の情報)
        array_push($金融機関コードたち, $金融機関コード);
    return array_unique($金融機関コードたち);
}

function 支店のデータが更新されている($金融機関コード, $pdo)
{
    $行 = $pdo -> query("SELECT hash FROM branches_json_hash WHERE bank_code = '" . $金融機関コード . "'") -> fetch();
    if($行)
    {
        $データベース上のハッシュ値 = $行["hash"];
        $今回計算したハッシュ値 = md5(file_get_contents(支店データのjsonファイルのパス($金融機関コード)));
    }
    if((!$行) || $データベース上のハッシュ値 != $今回計算したハッシュ値)
    {
        // $pdo -> query("DELETE FROM branches_json_hash WHERE hash = '" . $今回計算したハッシュ値 . "'"); ←ダメ。「本店しかない」「東京支店しかない」など、まったく同じjsonファイルが複数の金融機関に対応している。
        $pdo -> query("DELETE FROM branches_json_hash WHERE bank_code = '" . $金融機関コード . "'");
        $pdo -> query("INSERT INTO branches_json_hash VALUES('" . $金融機関コード . "', '" . $今回計算したハッシュ値 . "')");
    }
    return ((!$行) || $データベース上のハッシュ値 != $今回計算したハッシュ値);
}

2-2. phpファイルの置き場を作る

  1. レンタルサーバのファイルマネージャを開き、一番上のフォルダ((番号)_(ユーザ名)@localhost)を開き、この直下にcronというフォルダがないことを確認してください。
  2. 第1章の手順でSSH通信を開始してターミナルを開いてください。
  3. ターミナルが[(ユーザ名)@web(番号) ~]$ のようになっていることを確認したら、その隣にmkdir cronと入力してenterキーを押します。これで、/home/(ユーザ名)/ (~が表しているディレクトリです)というディレクトリの直下に「cron」という名のディレクトリが作られました。
  4. 再びファイルマネージャを開き、(番号)_(ユーザ名)@localhostの直下にcronというフォルダがいつの間にか生成されていることを確認してください。
  5. 以上のことから、レンタルサーバ上のディレクトリ /home/(ユーザ名)/ が、ファイルマネージャ上では(番号)_(ユーザ名)@localhost/ として表現されていることが分かります。
  6. なので、(番号)_(ユーザ名)@localhost/cronfuncs.phpをアップロードして(番号)_(ユーザ名)@localhost/cron/funcs.phpを作ると、SSH通信しているレンタルサーバ上でも/home/(ユーザ名)/cron/funcs.phpが作られます。
  7. 2-1節でつくった他のphpファイルも、このcronフォルダにアップロードしてください。

2-3. VIエディタでphpファイルを試しに登録する

  1. データベースにbanksテーブルを作っておいてください。列のみを定義し、行は空にしておいてください。
  2. SSH通信を開始し、ターミナルを開き、crontab -eを実行してください。
  3. 次のような画面になるはずです。
    image.png
  4. ここで a キーを押すと、下に-- INSERT --と表示され、文字が打てるようになります。
  5. */1 * * * * /usr/bin/php /home/(ユーザ名)/cron/banks.php と打ち込みます。これは、「1分ごとにbanks.phpを実行してね」という指示になります。
  6. 1分経ったら、データベースにログインし、banksテーブルを確認してください。空であったはずのbanksテーブルに、金融機関コードと金融機関名が大量に記録されているはずです。これは、banks.phpが勝手に実行された証拠となります。また、logsフォルダが作られ、ログが記録されていることからも、banks.phpが動いたことを確認できます。

2-4. VIエディタでphpファイルをちゃんと登録する

crontab -e でcronを設定するときの文法について述べます。
文法は(分) (時) (日) (月) (曜日) (コマンド)となっています。
(分)(月)には、(コマンド)を実行させたい分~月の値を入れます。
曜日は英語の先頭3文字を。
こだわりがなければ「*」とします。
たとえば1 2 3 4 * (cmd)とすると毎年4月3日の午前2時1分に(cmd)が実行されます。
0 0 13 * fri echo "不吉なことが起きないといいね" >> hoge.txt なんてすれば、「13日の金曜日」の深夜0時に教えてくれます。
数字の代わりに */2 なんて指定すると、2分(2時間、2日、2か月)毎に実行したりもできます。
他にも範囲を指定する方法だったり、複数指定だったりもできます。
参考になるのが、 https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/6/html/deployment_guide/ch-automating_system_tasks です。

私の場合は次のようにしました。

image.png

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?