コマンドラインツールを作る必要があったので、チャレンジしました。
某WEBサービスをスクレイピングして、入力履歴を表示するコマンドラインツールです。
PHPでコマンドラインツール?
そもそもPHPでコマンドラインツールって作れるの?ってところからスタート。
少し調べたら、コマンドラインでPHPファイルを読み込んで実行することみたいです。
ファイルに書いたphpを実行する
$ php /path/to/php/file.php
コマンドラインに書いたphpを実行する
$ php -r "echo '123';"
123 ← 実行結果
参考)https://www.softel.co.jp/blogs/tech/archives/2077
試しにターミナルを開いて、叩いてみました。
$ php -r "echo 'Hallo World!';"
無事表示されました。
Hallo World!を表示するだけのPHPファイルも用意してみました。
<?php
echo 'Hallo World!';
?>
ターミナルで実行。
$ php /path/to/hello.php
無事表示されました。
でも、これファイルの置き場が違うとうまく動かなかったりするのかな?
試しにデスクトップに置いて実行してみました。動いた。いいのかな。
file_get_contentsしてみる
スクレイピング自体はちょっと業務でしたことあります。
file_get_contentsして上手に加工するやつでしょ。
でも今回はログインも間にはさみますね。
とりあえずログイン前のトップページを取得できるかやってみます。
<?php
echo file_get_contents('https://xxxx.xxx/');
ターミナルで実行。
$ php /path/to/hello.php
ダダダダッとページのソースが表示されました。ひとまず成功です。
file_get_contentsをcURLでやってみる
ログインした後のページをスクレイピングするにはどうしたらいいんでしょう?
ちょっとググるとfile_get_contentsではなくcURLを使うといいって出てきますね。
そういえば前に業務でもそれやろうとして、何かがうまくいかなくて諦めたのでした。
とりあえずfile_get_contentsと同じことをcURLで書いてみます。
<?php
$url = "https://xxxx.xxx/";
$conn = curl_init(); // cURLセッションの初期化
curl_setopt($conn, CURLOPT_URL, $url); // 取得するURLを指定
curl_setopt($conn, CURLOPT_RETURNTRANSFER, true); // 実行結果を文字列で返す。
$res = curl_exec($conn);
echo $res;
curl_close($conn); //セッションの終了
#参考:https://techacademy.jp/magazine/11442
?>
ターミナルで実行し、file_get_contentsと同じようにダダダッとソースが表示できました。
cURLでログイン
参考サイトのソースを必要最低限いじってみて、ログインできるかテスト。
<?php
#ログインに必要なパラメーター
$params = array(
"email" => '自分のログイン用メールアドレス',
"password" => '自分のログイン用パスワード',
"submit" => "ログイン"
);
#ログインURL
$login_url = "https://xxxx.xxx/";
#フォームアクション先URL
$action_url = $login_url; //今回はログインフォームと同じ
# ターゲットURL
$data_url = "https://xxxx.xxx/xxxx";
#クッキー保存ファイルを作成
$cookie_file_path = dirname(__FILE__).'/cookie.txt';
touch($cookie_file_path);
#ログイン
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file_path);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file_path);
$put = curl_exec($ch) or dir('error ' . curl_error($ch));
curl_close($ch);
#アクション
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $action_url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HEADER, TRUE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file_path);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file_path);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
$output = curl_exec($ch) or dir('error ' . curl_error($ch));
curl_close($ch);
#データ取得
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $data_url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file_path);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file_path);
$output_comp = curl_exec($ch) or dir('error ' . curl_error($ch));
curl_close($ch);
#取得ページをUTF-8化して表示
mb_language("Japanese");
$complete_source = mb_convert_encoding($output_comp, "UTF-8", "auto");
echo $complete_source;
#クッキー保存ファイルの削除
unlink($cookie_file_path);
#参考:http://web-prog.com/php/curl-login-scraiping/
?>
ブラウザで実行すると、一瞬「認証完了」って出て成功したように見える。
けど、すぐログイン画面に戻っちゃう。
参考サイトのコードをコピペしてちょっと加工しただけだから、
いらなさそうなコードをコメントアウトしたり出したりしてみたけどうまくいかない。
ブラウザのcookieが邪魔してる気がして消してみたせいか、
むしろ「認証完了」画面にすら飛ばなくなった。なんだなんだ?
cURLでログイン再チャレンジ
試しに手作業でゆっくり動作確認して、情報整理。
ログアウトした状態でターゲットのURLを直接ブラウザのアドレスバーに入力して
アクセスすると、ログインURLにリダイレクトされる。
アイパスを入力・送信すると、一瞬「認証完了」ページが出た後、ターゲットURLにリダイレクトされる。
単純にターゲットページにアイパスをPOSTするだけではダメで、
認証完了ページを1枚挟んでからのリダイレクト処理が必要なんですな。
問題のリダイレクト処理は、こんな感じ↓にJSで書かれていました。
setTimeout(function(){ location.href = "
https://xxxx.xxx/user_session/callback
?oauth_token=とーくん
&oauth_verifier=べりふぁいあ";
}, 500);
// ※実際の引数はランダムな長い英数字
このURLをスクレイピングしてアクセスすれば、ターゲットページに行きつけそうです!
手順としては、こんな感じかな。
- ログイン画面にアイパスをPOST
- 認証完了画面のJSからURLを抜き出してリダイレクト
- ターゲットページにアクセス
<?php
class Log {
private $ch;
private $login_url = 'https://xxxx.xxxx.xxx/';
private $target_url = 'https://xxxx.xxx/xxxxx';
private $log_source;
//TODO あとでコマンドラインから受け取る
private $email = 'メールアドレス';
private $password = 'パスワード';
private $month = '201806'; //年月を指定(2018年5月は201805)
/**
* ログインして入力履歴ページを取得
*/
public function __construct() {
$this->ch = curl_init(); //cURLセッションを初期化
curl_setopt_array($this->ch, array(
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_COOKIEFILE => '',
CURLOPT_COOKIEJAR => '',
));
$this->login();
$this->get_log();
}
/**
* POST送信
*
* @access private
* @param string $url
* @param array $params
* @return string 送信後の画面
*/
private function post($url, $params) {
curl_setopt_array($this->ch, array(
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $params,
));
return curl_exec($this->ch);
}
/**
* GET送信
*
* @access private
* @param string $url
* @return string 送信後の画面
*/
private function get($url) {
curl_setopt_array($this->ch, array(
CURLOPT_URL => $url,
CURLOPT_HTTPGET => true,
));
return curl_exec($this->ch);
}
/**
* ログイン処理
*
* @access private
*/
private function login() {
$params = array(
'data[User][email]' => $this->email,
'data[User][password]' => $this->password,
'submit' => 'ログイン',
);
$source = $this->post($this->login_url, $params);
preg_match('/https:\/\/zaim\.net\/user_session\/callback\?oauth_token=(\w+)&oauth_verifier=(\w+)/',
$source, $matches
);
$this->get($matches[0]);
}
/**
* 指定された月の入力履歴を取得
*
* @access private
*/
private function get_log() {
$param = $this->month ? '?month='.$this->month : '';
$this->log_source = $this->get($this->target_url.$param);
}
/**
* 入力履歴を整形
*
* @access public
*/
public function scraping() {
//TODO 加工すること
echo $this->log_source;
}
}
$log = new Log();
$log->scraping();
#参考:http://www.tsubasa-note.blog/entry/php-curl-login/
#参考:https://qiita.com/mpyw/items/c65fb4ec4cef80909a47
?>
無事ブラウザで期待通りに動きました。
クラス名をどうするか、どこまでを__constructに含めるかに悩んだ…
#綺麗に加工する
取得したソースを綺麗に加工していきますよ。
あとはひとすら正規表現だから、ソースは割愛します。
#対話型にする
せっかくコマンドラインで実行するから、対話型にしたい!
調べてみたら案外簡単にできました。どういうやりとりにするかとか考えるほうが大変だった。
とはいえいちいち対話型で入力するのも面倒だし、引数でも渡せるようにしました。
一応ブラウザでも確認できます。
が、引数渡せないのでテストアカウントの当月表示のみです。
/**
* アカウント情報を設定
*
* @access private
*/
private function set_account() {
if(empty($this->argv)) { #引数が空の場合はブラウザで実行されている
echo <<<EOM
<div style='margin: 20px; padding: 20px; background-color: #fff;'>
<h1 style='text-align: center; line-height: 2;'>--+。*.゚:。ようこそ!+。*.゚:。 --</h1>
<p style='text-align: center; line-height: 1.5;'>
ほしさきひとみの今月の入力履歴を表示しています。<br>
コマンドラインから実行していただくと、ご自身のアカウントの履歴を確認いただけます。
</p>
</div>
EOM;
$this->email = 'メールアドレス';
$this->password = 'パスワード';
} else {
print ("\n───────── +。*.゚:。ようこそ!+。*.゚:。 ─────────\n");
print ("入力履歴を確認できるコマンドラインツールです。\n");
$this->input_account();
}
}
/**
* アカウント情報を入力してもらう
*
* @access private
*/
private function input_account($error = null) {
if($this->argv[1] AND !$error) {
$this->email = $this->argv[1];
} else {
ob_end_clean();
print ("\n▼ログイン用メールアドレスを入力してください(半角英数字)\n");
$this->email = trim(fgets(STDIN));
}
if($this->argv[2] AND !$error) {
$this->password = $this->argv[2];
} else {
print ("\n▼ログイン用パスワードを入力してください(半角英数字)\n");
$this->password = trim(fgets(STDIN));
}
}
/**
* ログイン処理
*
* @access private
*/
private function login() {
$params = array(
'data[User][email]' => $this->email,
'data[User][password]' => $this->password,
'submit' => 'ログイン',
);
$source = $this->post($this->login_url, $params);
preg_match('/https:\/\/zaim\.net\/user_session\/callback\?oauth_token=(\w+)&oauth_verifier=(\w+)/',
$source, $matches
);
if(empty($matches[0])) {
print ("\n[!]メールアドレスかパスワードが間違っています。再度入力してください。\n");
$this->input_account(1);
} else {
$this->get($matches[0]);
}
}
#参考:http://uxmilk.jp/15067
#参考:http://www.phppro.jp/phptips/archives/vol37/2
うん、いい感じ!な気がする!