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


<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/b5ed91a6-1cf4-0e30-dafd-11684918b5c2.jpeg">

はい。


# 一覧で見たい

献血すると、その日の[生化学検査や血球計数検査](https://www.bs.jrc.or.jp/ktks/tokyo/donation/m2_02_03_index4.html)の結果を教えてくれます。
正常異常を表すものではないとの注意書きはありますが、それでもある程度の目安にはなるでしょう。
そして、公式サイト[ラブラッド](https://www.kenketsu.jp/)では、その過去のデータを閲覧することができます。

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

ところが[ラブラッド](https://www.kenketsu.jp/)では、何故かたったの3回分しか比較することができません。

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/b004ac7e-202f-531b-63c2-20d35be8a75a.png)


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

![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/b3ce180b-e604-682d-34bd-d397d51cab50.png)

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

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


# データを取り出す

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


```php
(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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/836dd7d1-99ed-8869-ad2b-80b547f3d7c6.png)

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

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


あと、[噂のfingerprint.js](https://qiita.com/rana_kualu/items/b1882224aab05846883a)が入ってました。
どうして入れる必要があるんですかね？


# データが足りなかった

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

![21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/d35d595c-d283-f169-893f-955760815a98.png)

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

![06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/dc1ba8c3-da60-3c68-5bbb-4c1c553faeda.png)

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

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


# データを見てみる

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

## 血圧

![22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/260bec7f-d4bc-1cde-9c4f-443f2dc931a3.png)

微妙に高めですが、[献血するには全く問題ない範囲](https://www.bs.jrc.or.jp/ktks/tokyo/donation/m2_01_02_index2.html)です。
目指せ低血圧。

## コレステロール

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

![23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/aac56c05-6a46-479c-e64d-107ed482da13.png)

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


## γ-GTP

![24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/238b5519-1a60-4d17-f1fa-5e1fab25d108.png)

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

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

## 血小板数

![25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/4881934f-d8d5-0d9a-f4e9-e93e129d46b5.png)

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

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

# まとめ

みんなも献血に行け。