目的
タイトルの通り、お名前.comのレンタルサーバをRSプランで借りてる人が、phpを定期実行させること。
いま私は、クラウドファンディングサイトを構築したくて、金融機関名や支店名の照合を行うためにデータベースに金融機関名、支店名を登録しているが、これを1日に1度(毎日0:00 AM)とかでインターネット上のjsonに同期したい。
ネットに情報がほとんどなかったし、公式サイトにも「OSはLinuxです」程度のことしか書いていなく、どのコマンドが使えるかとか、ディレクトリ構造がどうなのかとかの説明がほとんどなく苦労した。なので次回からは楽できるようにここにメモしておく。
手順
- SSH通信をする
- CRONを設定する。
1. SSH通信をする
SSH通信とは、「コンピュータを遠隔操作する通信」の手法の一つ。
お名前.comのレンタルサーバRSプランのコントロールパネルには、phpの定期実行を行うためのツールが用意されていないので、レンタルサーバを遠隔操作して直接設定を変更しちゃえ!ということです。
手順は次の通りです。
- SSH鍵を作る
- パソコンにTera Termをインストールする
- Tera Termからレンタルサーバにログインする
1-1. SSH鍵を作る
SSH鍵とは公開鍵暗号の鍵です。SSHは、暗号通信を行うことで、第三者による傍聴や改竄、なりすましなどを防いでいます。
- https://cp.onamae.ne.jp/server/ssh へ行く
- 「SSH Key を追加」または「はじめる」ボタンを押す
- 指示に従って進めると、「レンタルサーバが持っておくべき鍵」(公開鍵)と「ユーザが持っておくべき鍵」(秘密鍵)が作られる。
- 秘密鍵ファイルをダウンロードし、大切に保管する。
1-2. パソコンにTera Termをインストールする
SSH通信を行うのに便利なソフト。
- https://ja.osdn.net/projects/ttssh2/releases/ から最新のexeファイルをダウンロード
- ダウンロードしたexeファイルを実行
1-3. Tera Termからレンタルサーバにログインする
- https://cp.onamae.ne.jp/server/ssh に行く
- 1-1で作成した鍵の「詳細」をクリックする
- ホスト名、ポート番号、ユーザ名をメモする
- Tera Termを起動する。
- 次の写真のようにする。
- 次の写真のようにする
- 次のような画面(ターミナル)になれば成功
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ファイルは今回何もしなくていい)かを判断するために用意しています。)
手順は大まかに次の通りです。
- 自動実行させたいphpファイルを作る
- phpファイルの置き場を作る
- VIエディタでphpファイルを試しに登録する
- VIエディタでphpファイルをちゃんと登録する
2-1. 自動実行させたいphpファイルを作る
コードは次のようになります。金融機関(1200つ強あります)の一覧をbanks
テーブルに同期するbanks.php
と、各金融機関の支店の一覧をbank_branches
テーブルに同期するbranches01.php
~branches89.php
に分かれています。
branchesXX.php
を5つに分けたのは、jsonファイルを提供してくれるサーバの負荷を分散させるためです。
「毎日0時0分に1200回も問い合わせを行う」よりは、「毎日0時0分に240回」、「毎日1時0分に240回」、...、「毎日4時0分に240回」に分けたほうが負荷が集中しません。
<?php
require_once("funcs.php");
$ログ = new ログマネージャ($ログファイルパス, "banks");
$ログ -> 書く("金融機関のデータが更新されているか確認。");
if(金融機関のデータが更新されている($金融機関データのjsonファイルのパス, $pdo))
{
$ログ -> 書く("金融機関のデータが更新されているので同期開始");
$banks = new Banksテーブル($pdo); // データベース上のテーブルをいじるクラスを自作しました
$banks -> 空にする();
foreach(
get金融機関データ($金融機関データのjsonファイルのパス)
as $金融機関コード => $金融機関の情報
)
{
$金融機関名 = $金融機関の情報["name"];
$banks -> 行を挿入する($金融機関コード, $金融機関名);
}
$ログ -> 書く("金融機関のデータ同期終了");
}
else
{
$ログ -> 書く("金融機関のデータは更新されていませんでした。");
}
<?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
です。
$データベース名 = ;
$ホスト名 = ;
$文字コード = ;
$ユーザ名 = ; // レンタルサーバではなく、データベースのユーザ名です
$パスワード = ; // 同上
$カレントディレクトリの絶対パス = ; // 手順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ファイルの置き場を作る
- レンタルサーバのファイルマネージャを開き、一番上のフォルダ(
(番号)_(ユーザ名)@localhost
)を開き、この直下にcron
というフォルダがないことを確認してください。 - 第1章の手順でSSH通信を開始してターミナルを開いてください。
- ターミナルが
[(ユーザ名)@web(番号) ~]$
のようになっていることを確認したら、その隣にmkdir cron
と入力してenterキーを押します。これで、/home/(ユーザ名)/
(~
が表しているディレクトリです)というディレクトリの直下に「cron
」という名のディレクトリが作られました。 - 再びファイルマネージャを開き、
(番号)_(ユーザ名)@localhost
の直下にcron
というフォルダがいつの間にか生成されていることを確認してください。 - 以上のことから、レンタルサーバ上のディレクトリ
/home/(ユーザ名)/
が、ファイルマネージャ上では(番号)_(ユーザ名)@localhost/
として表現されていることが分かります。 - なので、
(番号)_(ユーザ名)@localhost/cron
にfuncs.php
をアップロードして(番号)_(ユーザ名)@localhost/cron/funcs.php
を作ると、SSH通信しているレンタルサーバ上でも/home/(ユーザ名)/cron/funcs.php
が作られます。 - 2-1節でつくった他のphpファイルも、この
cron
フォルダにアップロードしてください。
2-3. VIエディタでphpファイルを試しに登録する
- データベースに
banks
テーブルを作っておいてください。列のみを定義し、行は空にしておいてください。 - SSH通信を開始し、ターミナルを開き、
crontab -e
を実行してください。 - 次のような画面になるはずです。
- ここで
a
キーを押すと、下に-- INSERT --
と表示され、文字が打てるようになります。 -
*/1 * * * * /usr/bin/php /home/(ユーザ名)/cron/banks.php
と打ち込みます。これは、「1分ごとにbanks.php
を実行してね」という指示になります。 - 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 です。
私の場合は次のようにしました。