177
57

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.

献血100回行ったので過去のデータを一覧で見たかった

Last updated at Posted at 2021-08-16

はい。

一覧で見たい

献血すると、その日の生化学検査や血球計数検査の結果を教えてくれます。
正常異常を表すものではないとの注意書きはありますが、それでもある程度の目安にはなるでしょう。
そして、公式サイトラブラッドでは、その過去のデータを閲覧することができます。

そんなわけで過去のデータを一覧表示して、各種値がどのように遷移しているか見てみたかったわけです。
1回目から100回目までの間で、生活環境とか食性とか体重とか色々と変わりましたからね。

ところがラブラッドでは、何故かたったの3回分しか比較することができません。

02.png

表示範囲を変更することで過去のデータに遡ることもできるのですが、それでも一度では3回分しか表示できないのは同じです。

03.png

なにこの制限。
私は過去全範囲の遷移を見たいんだよ!

ということで全てのデータを取り出すことにしましょう。

データを取り出す

もちろんAPIなんて存在しないので気合いでスクレイピングです。

(new Blood())->run();

class Blood
{
    const BLOODCODE = '1234567890'; // 献血者コード
    const PASSWORD = 'password';    // パスワード
    const RECORDPASSWORD = '1234';  // 献血記録確認用パスワード

    /**
     * コンストラクタ
     */
    public function __construct()
    {
        $this->client = new GuzzleHttp\Client(['cookies' => true, 'verify'=>false]);
    }

    /**
     * 実行
     */
    public function run()
    {

        // ログインページ
        $params = $this->getLoginPage();

        // ログインする
        $params = $this->postLogin($params);

        // frontdoor.jsp
        $params = $this->getFrontdoor($params);

        // 献血記録の確認ログインページ
        $params = $this->getRecordLogin($params);

        // 献血記録の確認にログインする
        $params = $this->postRecordLogin($params);

        // 献血記録の確認ページ
        $params = $this->getRecord($params);

        // 献血の個別ページ
        $data = $this->getRecordDetail($params);

        // 整形
        $data = $this->seikei($data);

        // 保存
        $this->saveData($data);
    }

    /**
     * ログインページ取得
     */
    private function getLoginPage()
    {
        // 取得
        $url = 'https://www.kenketsu.jp/Login?startURL=%2F';
        $res = $this->client->request('GET', $url);
        $body = $res->getBody()->getContents();

        /*
            com.salesforce.visualforce.[ViewState|ViewStateVersion|ViewStateMAC]を抜き出す
            一見ゴミっぽいけどこれがないと何故かログインできない。
        */
        preg_match('|name="com\.salesforce\.visualforce\.ViewState" value="(.*?)" />|', $body, $matches1);
        preg_match('|name="com\.salesforce\.visualforce\.ViewStateVersion" value="(.*?)" />|', $body, $matches2);
        preg_match('|name="com\.salesforce\.visualforce\.ViewStateMAC" value="(.*?)" />|', $body, $matches3);

        $ret = [
            'com.salesforce.visualforce.ViewState'=>$matches1[1],
            'com.salesforce.visualforce.ViewStateVersion'=>$matches2[1],
            'com.salesforce.visualforce.ViewStateMAC'=>$matches3[1],
        ];
        return $ret;
    }

    /**
     * ログインする
     */
    private function postLogin($params)
    {
        // ログイン実行
        $url = 'https://www.kenketsu.jp/Login';
        $params+= [
            'Login:j_id38' => 'Login:j_id38',
            'Login:j_id38:j_id40' => self::BLOODCODE,
            'Login:j_id38:j_id42' => self::PASSWORD,
            'Login:j_id38:j_id47' => 'Login:j_id38:j_id47',
        ];
        $res = $this->client->request('POST', $url, ['form_params'=>$params]);
        $body = $res->getBody()->getContents();

        // frontdoor.jspへのリダイレクトが返ってくる。
        preg_match('|SfdcApp.projectOneNavigator.handleRedirect\(\'(.*?)\'\); }|', $body, $matches);
        $params['frontDoorUrl'] = $matches[1];
        return $params;
    }

    /**
     * frontdoor.jspにアクセスする
     */
    private function getFrontdoor($params)
    {
        /*
            特に何もないが、一度アクセスしないと何故かログインしたことにならない。
        */
        $this->client->request('GET', $params['frontDoorUrl']);
        return $params;
    }

    /**
     * 献血記録の確認ログインページ
     */
    private function getRecordLogin($params)
    {
        /*
            献血記録は別途認証が必要。
            こっちはCSRF対策されてるのでViewStateCSRFが必要。
            あと他のViewStateとかも更新しないと動かなかった。
        */

        // 取得
        $url = 'https://www.kenketsu.jp/RecordLogin';
        $res = $this->client->request('GET', $url);
        $body = $res->getBody()->getContents();

        preg_match('|name="com\.salesforce\.visualforce\.ViewStateCSRF" value="(.*?)" />|', $body, $matches);
        preg_match('|name="com\.salesforce\.visualforce\.ViewState" value="(.*?)" />|', $body, $matches1);
        preg_match('|name="com\.salesforce\.visualforce\.ViewStateVersion" value="(.*?)" />|', $body, $matches2);
        preg_match('|name="com\.salesforce\.visualforce\.ViewStateMAC" value="(.*?)" />|', $body, $matches3);

        $params['com.salesforce.visualforce.ViewStateCSRF'] = $matches[1];
        $params['com.salesforce.visualforce.ViewState'] = $matches1[1];
        $params['com.salesforce.visualforce.ViewStateVersion'] = $matches2[1];
        $params['com.salesforce.visualforce.ViewStateMAC'] = $matches3[1];
        return $params;
    }

    /**
     * 献血記録の確認にログインする
     */
    private function postRecordLogin($params)
    {
        // ログイン実行
        $url = 'https://www.kenketsu.jp/RecordLogin?refURL=https%3A%2F%2Fwww.kenketsu.jp%2F';
        $requestParams = [
            'RecordLogin:RecordLoginForm'=>'RecordLogin:RecordLoginForm',
            'RecordLogin:RecordLoginForm:kenketsuPassword'=>self::RECORDPASSWORD,
            'RecordLogin:RecordLoginForm:j_id40'=>'RecordLogin:RecordLoginForm:j_id40',
            'com.salesforce.visualforce.ViewState'=>$params['com.salesforce.visualforce.ViewState'],
            'com.salesforce.visualforce.ViewStateVersion'=>$params['com.salesforce.visualforce.ViewStateVersion'],
            'com.salesforce.visualforce.ViewStateMAC'=>$params['com.salesforce.visualforce.ViewStateMAC'],
            'com.salesforce.visualforce.ViewStateCSRF'=>$params['com.salesforce.visualforce.ViewStateCSRF'],
        ];
        $res = $this->client->request('POST', $url, ['form_params'=>$requestParams]);

        return $params;
    }

    /**
     * 献血記録ページ
     */
    private function getRecord($params)
    {
        // 取得
        $url = 'https://www.kenketsu.jp/recordinspectionresult';
        $res = $this->client->request('GET', $url);
        $body = $res->getBody()->getContents();

        // mod-slider-column__slide-areaに過去記録のIDが入ってる
        preg_match('|mod-slider-column__slide-area(.*?)mod-slider-column__btn-area|s', $body, $matches);
        preg_match_all('|data-id="(.*?)">|', $matches[1], $ids);
        $ids = $ids[1];
        foreach ($ids as $k=>$v) {
            if (!$v) {
                unset($ids[$k]);
            }
        }
        $params['recordIds'] = array_reverse($ids);
        return $params;
    }

    /**
     * 献血記録の詳細
     */
    private function getRecordDetail($params)
    {
        /*
            一度に3回ぶんしか取れないので、順に過去に遡っていく。
        */
        $ret = [];

        $cnt = 0;
        foreach ($params['recordIds'] as $recordId) {
            // 1リクエストで過去3回取れるので2・3回目はパス
            if ($cnt++%3) {
                continue;
            }

            // 取得
            $url = 'https://www.kenketsu.jp/recordinspectionresult?id=' . $recordId;
            $res = $this->client->request('GET', $url);
            $body = $res->getBody()->getContents();

            // 気合いでスクレイピング
            $ret[] = $this->parseRecordInspectionResult($body);
        }

        return $ret;
    }

    /**
     * 献血記録ページを気合いでスクレイピング
     * @param string HTML
     * @return array [日付毎の各測定値]
     */
    private function parseRecordInspectionResult($html)
    {
        /*
            まず大まかな範囲を切り取り、その中で該当の値を取り出す
        */
        $patterns = [
            'date'=>[
                '|<div class="pbBody">(.*?)<section class="mod-section ">|s',
                '|<input id="dataLabel\d" type="hidden" value="(.*?)" />|'
            ],
            'pressure_max'=>[
                '|&#34880;&#22311;&#65288;&#26368;&#39640;&#65289;(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'pressure_min'=>[
                '|&#34880;&#22311;&#65288;&#26368;&#20302;&#65289;(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'pulse'=>[
                '| &#33032;&#25293;(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'alt'=>[
                '|ALT&#65288;GPT&#65289;(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'gtp'=>[
                '|&#947;-GTP(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'proteins'=>[
                '|&#32207;&#34507;&#30333;&#12288;TP(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'albumin'=>[
                '|&#12450;&#12523;&#12502;&#12511;&#12531;&#12288;ALB(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'albumin-globulin'=>[
                '|&#12450;&#12523;&#12502;&#12511;&#12531;&#23550;&#12464;&#12525;&#12502;&#12522;&#12531;&#27604;&#12288;A/G(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'cholesterol'=>[
                '|&#12467;&#12524;&#12473;&#12486;&#12525;&#12540;&#12523;&#12288;CHOL(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'glycoalbumin'=>[
                '|&#12464;&#12522;&#12467;&#12450;&#12523;&#12502;&#12511;&#12531;&#12288;GA(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'redbloodcell'=>[
                '|&#36196;&#34880;&#29699;&#25968;&#12288;RBC(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'hemoglobin'=>[
                '|&#12504;&#12514;&#12464;&#12525;&#12499;&#12531;&#37327;&#12288;Hb(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'hematocrit'=>[
                '|&#12504;&#12510;&#12488;&#12463;&#12522;&#12483;&#12488;&#20516;&#12288;Ht(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'meancellvolume'=>[
                '|&#24179;&#22343;&#36196;&#34880;&#29699;&#23481;&#31309;&#12288;MCV(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'meancellhemoglobin'=>[
                '|&#24179;&#22343;&#36196;&#34880;&#29699;&#12504;&#12514;&#12464;&#12525;&#12499;&#12531;&#37327;&#12288;MCH(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'meancellhemoglobinconcentration'=>[
                '|&#24179;&#22343;&#36196;&#34880;&#29699;&#12504;&#12514;&#12464;&#12525;&#12499;&#12531;&#28611;&#24230;&#12288;MCHC(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'whiteblood'=>[
                '|&#30333;&#34880;&#29699;&#25968;&#12288;WBC(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
            'bloodplatelet'=>[
                '|&#34880;&#23567;&#26495;&#25968;&#12288;PLT(.*?)mod-result-table__toggle-btn|s',
                '|<li class=".*?">(.*?)</li>|'
            ],
        ];

        $ret = [];
        foreach ($patterns as $key=>$pattern) {
            preg_match($pattern[0], $html, $matches1);
            preg_match_all($pattern[1], $matches1[1], $matches2);
            $ret[$key] = $matches2[1];
        }
        return $ret;
    }

    /**
     * 整形
     */
    private function seikei(array $array):array
    {
        /*
            3件ごとの配列で使いにくいので、日付ごとにまとめる
        */
        $ret = [];

        foreach ($array as $v1) {
            foreach ($v1 as $k2=>$v2) {
                foreach ($v2 as $k3=>$v3) {
                    $ret[$v1['date'][$k3]][$k2]= $v3;
                }
            }
        }

        ksort($ret);
        return $ret;
    }

    /**
     * 保存する
     */
    private function saveData($data)
    {
        // CSV
        $fp = fopen('./blood.csv', 'w');

        fputcsv($fp, [
            '日付', '最高血圧', '最低血圧', '脈拍', 'ALT(GPT)', 'γ-GTP', '総蛋白', 'アルブミン', 'アルブミン対グロブリン比', 'コレステロール', 'グリコアルブミン',
            '赤血球数', 'ヘモグロビン量', 'ヘマトクリット値', '平均赤血球容積', '平均赤血球ヘモグロビン量', '平均赤血球ヘモグロビン濃度', '白血球数', '血小板数',
        ]);
        foreach ($data as $v) {
            fputcsv($fp, $v);
        }
    }

}

なんだこりゃ!?

うるせえどうせ使い捨てなんだし一回動けばいいんだよ。

気になったところとか

かなり意味の分からない構造になっています。

まずログインページ。
いきなりcom.salesforce.visualforceというよくわからないパラメータを送っていますが、これを送らないとログインできません。
なんでSalesForceが必要なんだ。
まあ結果としてCSRF対策そのものになっているからいい……のか?

frontdoor.jsp
この謎のJSPに一度リクエストを送っておかないとログインできません。
なんなんですかねこれ。

詳細ページ。
ここはラブラッドにログインしたあと、さらに別のパスワードを入れないと入れないようになっています。
CSRFトークンも送信が必要です。
個人情報保護方針がしっかりしていますね。

04.png

まあ中入っても測定値見るくらいしかできないからあまり意味ないんだけど。

あとパスワードのパラメータ名がRecordLogin:RecordLoginForm:kenketsuPasswordです。
なにこれ。

あと、噂のfingerprint.jsが入ってました。
どうして入れる必要があるんですかね?

データが足りなかった

なぜか過去25回分しか取り出せませんでした。
なんで???

21.png

どこかスクレイピングに失敗したかと思えばそういうわけでもなく、Webサイト上で見てみても過去25回分しか遡れませんでした。

06.png

こことか間隔が3年も開いてるんだがなんでだ。

都道府県をまたいで引っ越ししたりしてたから、うまく連携できてないんですかね?
どうも昔のデータはうまく取得しきれないみたいです。
残念。

データを見てみる

完全なデータは作れなかったとはいえ、せっかくそれなりに取り出せたのでグラフにしてみることに。

血圧

22.png

微妙に高めですが、献血するには全く問題ない範囲です。
目指せ低血圧。

コレステロール

検査項目で一番気になるであろうコレステロール。

23.png

肉ばっか食ってるわりに下限付近で安定してますね。
めでたし。

γ-GTP

24.png

主に酒を飲むと上昇するようです。
私はお酒をまったく飲まない(飲めないわけではない)ので、下限をぶっちぎってしまいました。

高すぎると肝硬変など肝臓の危険が危ないことになるらしいですが、低すぎるとどうなるんだろう?

血小板数

25.png

普段は血小板献血がメインなので、この値が増えるのはよいことです。

検査項目は他にもありますが、これ以上並べてもしょうがないからいいでしょう。
自分で眺めてにやにやするだけにしておきます。

まとめ

みんなも献血に行け。

177
57
8

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
177
57

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?