5
7

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 5 years have passed since last update.

PHPでコマンドラインツールを作る

Posted at

コマンドラインツールを作る必要があったので、チャレンジしました。
某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!';"

スクリーンショット 2018-06-01 12.03.58.png

無事表示されました。
Hallo World!を表示するだけのPHPファイルも用意してみました。

hallo.php
<?php
echo 'Hallo World!';
?>

ターミナルで実行。

$ php /path/to/hello.php

スクリーンショット 2018-06-01 12.15.07.png

無事表示されました。

でも、これファイルの置き場が違うとうまく動かなかったりするのかな?
試しにデスクトップに置いて実行してみました。動いた。いいのかな。

file_get_contentsしてみる

スクレイピング自体はちょっと業務でしたことあります。
file_get_contentsして上手に加工するやつでしょ。

でも今回はログインも間にはさみますね。
とりあえずログイン前のトップページを取得できるかやってみます。

index.php
<?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で書いてみます。

index.php
<?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でログイン

参考サイトのソースを必要最低限いじってみて、ログインできるかテスト。

index.php
<?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をスクレイピングしてアクセスすれば、ターゲットページに行きつけそうです!
手順としては、こんな感じかな。

  1. ログイン画面にアイパスをPOST
  2. 認証完了画面のJSからURLを抜き出してリダイレクト
  3. ターゲットページにアクセス
<?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

image.png

うん、いい感じ!な気がする!

5
7
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
5
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?