QiitaのContribution数ランキング作った

## まえがき

先人のサイトを二つほど見付けたがどちらも既に[死](http://pronama.azurewebsites.net/2015/07/05/qiita-contribution-ranking/)[去](http://blog.y-temp4.com/entry/qiita-user-ranking)済だったので、似たようなものを作った。

[QiitaのContribution数ランキング](http://qiita.com/rana_kualu/items/f20ec1849ba4a12b3315)

## ロジック

[デイリーストックランキング](http://qiita.com/takeharu/items/bb154a4bc198fb102ff3)を取得 → ユーザ名を適当に拾う → 該当ユーザページからContribution数を取得 → 内部DBを更新 → 内部DBからORDER BY Contributionでランキング作成 → HTML作る → Qiitaに[アップロード](http://qiita.com/hikarut/items/f6349e74fd02eb94a2ab)
↑を毎日06:30に実行。

単純。

これによってQiitaへのリクエスト数を一日あたり最大12回(取得11+更新1)に抑えている。


## 問題点

・その日ランキングに乗らなかったユーザは更新されない
・ランキングに入らない程度に細く長く伸びるユーザは更新されない

下位まで網羅した完全なランキングを作るつもりはなく、上位100人くらい見れればいいや的なランキングなので、大勢に影響はしないと判断して切り捨てます。
そのクラスであれば、細く長いコンテンツだけではそうそう順位逆転は起こらないでしょう。

というか、元のランキングってどうやっていいね数を取得してるんですかね？
特定タグとかならまだわかるけど、色々網羅してますし。

## ソース

```php

$c = new QiitaContribution();
$c->run();

class QiitaContribution{
    //-----------------------------------------------------------------------------------
    /**
    * コンストラクタ
    */
    public function __construct(){
        // DB
        $this->pdo = new PDO(MYSQL_DSN, MYSQL_USER, MYSQL_PASS, [
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]);
    }

    //-----------------------------------------------------------------------------------
    /**
    * 実行
    */
    public function run(){
        // ランキングを取得
        $html = $this->getUrl(URL_RANKING);
        // ユーザ一覧を取得
        $users = $this->parseRanking($html);
        // ユーザでくるくる
        foreach($users as $user){
            // ユーザページを取得
            $html = $this->getUrl(URL_DOMAIN . $user);
            // Contribution抽出
            $contribution = $this->parseUser($html);
            // 更新
            $this->updateTable($user, $contribution);
        }

        // Contribution上位を取得
        $cont = $this->getTable(50);
        
        // HTMLを作成
        $array = $this->makeQiitaJsonArray($cont);
        // 更新
        $this->uploadQiita($array);
    }

    //-----------------------------------------------------------------------------------
    /**
    * アップロード
    * @param array [title, body, tags]
    * @return result
    */
    private function uploadQiita($array){
        $headers = [
            'Content-Type: application/json',
            'Authorization: Bearer ' . URL_TOKEN,
        ];

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, URL_UPDATE);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($array));
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $result = curl_exec($ch);

        return json_decode($result, true);
    }

    //-----------------------------------------------------------------------------------
    /**
    * 更新用HTML作成
    * @param PDOStatement
    * @return array [title, body, tags]
    */
    private function makeQiitaJsonArray($users){
        // タイトル
        $title = sprintf(QIITA_TITLE, date('Y/m/d'));

        // 本体
        $table = '';
        $c = 1;
        $escapeArray = ['|']; // 名前に入れられては困る文字

        foreach($users as $v){
            $u = str_replace($escapeArray, '', $v['user']);
            $table .= sprintf("| %d | @%s | %d |\n", $v['contribution'], $u, $c++);
        }
        $body = sprintf(QIITA_BODY, $table);

        // 返り値
        $ret = [
            'title' => $title,
            'body'  => $body,
            'tags'  => QIITA_TAGS,
        ];
        return $ret;
    }



    //-----------------------------------------------------------------------------------
    /**
    * ランキング取得
    * @param int 件数
    * @return PDOStatement 
    */
    private function getTable($cnt){
        $sql = sprintf('SELECT * FROM qiita_contribution ORDER BY contribution DESC LIMIT %d', $cnt);
        return $this->pdo->query($sql);
    }

    /**
    * DBを更新
    * @param string ユーザ名
    * @param int contribution
    * @return bool true
    */
    private function updateTable($user, $c){
        $sql = sprintf('INSERT INTO qiita_contribution (user, contribution) VALUES (:user, %1$d) ON DUPLICATE KEY UPDATE contribution = %1$d', $c);
        $stmt = $this->pdo->prepare($sql);
        return $stmt->execute([':user' => $user]);
    }

    //-----------------------------------------------------------------------------------
    /**
    * HTMLをパースしてContributionの数値を返す
    * @param string HTML
    * @return int
    */
    private function parseUser($html){
        $s = explode('contributions"><span class="userActivityChart_statCount">', $html, 2);
        $t = explode('</span>', $s[1], 2);
        return (int)$t[0];
    }
    
    //-----------------------------------------------------------------------------------
    /**
    * HTMLをパースしてユーザの配列にして返す
    * @param string HTML
    * @return array [a, b, c]
    */
    private function parseRanking($html){
        /*
            DOM使ってもどうせ最後は物理パースがいるので、手っ取り早く最初から物理パース
        */
        $s = explode('# デイリーいいねランキング', $html, 2);
        $t = explode('# ウィークリーいいねランキング', $s[1], 2);
        if(!$t[0]){ return false; }
        $a = explode("\n", $t[0]);

        $ret = [];
        foreach($a as $v){
            if(strpos($v, 'by ') === 0){
                $v2 = substr($v, 3);
                $ret[$v2] = $v2;
            }
        }
        return $ret;
    }

    //-----------------------------------------------------------------------------------
    /**
    * URLを取得
    * @param string URL
    * @return string HTML
    */
    private function getUrl($url){
        return file_get_contents($url);
    }

}

```

見てのとおり超アバウト

## いいねやContributionについて思うこと

正直どうでもいい。

## まとめ

ユーザランキング取得APIつくって。