【Qiita】新着記事一覧ページを疑似再現する

Qiitaの[新着記事一覧](https://qiita.com/items)が[殺されました](https://blog.qiita.com/close-item-list-page/)。

:::note
そのため現在は、全ユーザーに一律の情報を見せるのではなく、ユーザー1人1人の興味に合わせて自分が欲しい情報に出会えるように、ホームフィードや、タイムラインフィードを用意しております。
:::

そんな余計なことしなくていいから全記事を一覧させろ。

と嘆いていても死んだ子は帰ってこないので、ここでは逆転の発想で考えましょう。
すなわち、ありとあらゆるタグを全てフォローすることで、タイムラインに新着記事一覧ページを疑似再現するのです。

Qiita APIに[tags/:tag_id/following](https://qiita.com/api/v2/docs#put-apiv2tagstag_idfollowing)があるので、これをひたすら延々叩くことにしましょう。

……ところで`:tag_id`ってなんだ？
[タグ一覧ページ](https://qiita.com/tags?page=1)にも[タグ詳細](https://qiita.com/tags/php)にもそれっぽい値が見当たらないんだが。
と調べてみたらIDとかではなくタグの文字をそのまま入れればいみたいでした。
つまりは`tags/php/following`にPUTすればタグPHPをフォローできるということです。

そんなわけでさくっと作成。

```php
(new ITEMS())->run();

class ITEMS{

    public function run(){
        // とりあえず100ページ
        $i = 0;
        while($i++<100){
            // タグ一覧を取得
            $tags = $this->getTags($i);

            foreach($tags as $tag){
                // タグをフォローする
                $this->followTag($tag);
            }
        }
    }

    /**
     * タグ一覧を取得
     * @param int ページ
     * @return array タグリスト
     */
    private function getTags(int $page):array{
        // 取得
        $url = 'https://qiita.com/tags?page='.$page;
        $f = file_get_contents($url);

        // タグを取り出す
        preg_match_all('|TagList__label" href="/tags/(.*?)">|', $f, $matches);
        return $matches[1];
    }

    /**
     * タグをフォローする
     * @param string タグ
     */
    private function followTag(string $tag):void{
        static $count = 0;
        $count++;

        $url = sprintf('https://qiita.com/api/v2/tags/%s/following', $tag);

        $curl = curl_init($url);
        $option = [
            CURLOPT_CUSTOMREQUEST => 'PUT',
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer XXXXXXXX',
                'Content-Type: application/json',
            ],
        ];
        curl_setopt_array($curl, $option);
        $result = curl_exec($curl);

        // 失敗したら死
        if($e = curl_error($curl)){
            var_dump($e, curl_getinfo($curl));
            die;
        }

        echo sprintf("COUNT: %d  TAG: %s follow. \n", $count, urldecode($tag));
        sleep(1);
    }
}
```

[タグ一覧ページ](https://qiita.com/tags)を100ページ目まで拾ってきて、取得したタグを全てフォローするという力業になっています。

# いろいろ失敗

なんか途中から急にAPIの呼び出しが早くなったんですよ。
Webで見るとフォローが972で止まっていました。

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/5887f1d9-b82a-d2b6-c999-8607374aa529.png)

なんで？と思ったら、APIの返り値が`403 Forbidden`になっていました。
そして`403 Forbidden`は`curl_error()`に反応しなかったのでループが止まらなかったというわけです。

よく考えたら`sleep(1)`ではAPIの呼び出し制限`1時間に1000回`に思いっきり引っかかってましたね。

ということで`sleep(5)`にして、さらにエラーが引っかかるように↓を追加して実行を続行。

```php
if(curl_getinfo($curl)['http_code']>300){
    var_dump(curl_getinfo($curl), $result);
    die;
}
```

今度は`a&b`と`&`の入ったタグの登録で引っかかりました。
おそらくクエリパラメータと間違えられて、そこでURLのパースが途切れたと思われます。
`&amp;`にしても駄目でした。
解決策はあるんだろうけど、面倒なので`&`が入っていたらパスすることにして続行。

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/fa6f312d-1653-4069-914a-ee2c44454cff.png)

はい、無事に10000タグのフォローに成功しました。

あ、最後の3タグは手動です。


[タグ10000番目](https://qiita.com/tags?page=100)が15記事とかです。
従って、これ以下のマイナーなタグしか指定されていないとか、そもそもタグを設定してない記事は引っかからないのですが、そのような記事以外は全て新着順に表示されるようになったはずです。
10000なのは単にきりがよかったからというだけなので、もっと伸ばして10万タグをフォローしたりすれば漏れもほとんどなくなるでしょう。


# 実行後のタイムライン

![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/eb15f366-2414-dba9-6329-76dd58d98e6b.png)

※実際は30件表示された下に『もっと読む』ボタンです。

どうしてここページングじゃないんですか？

ページングにするか、ページングしないんならせめて自動読み込みにするかのどちらかにしてくれ。
自動じゃない追加読み込みは、非常に苦痛なのでやめてほしいところ。

とまあ大きな不満こそありますが、ひとまず新着記事一覧をほぼ再現することに成功しました。
めでたし。

ところで1万タグを含む記事の検索ってどうやってるんだろうか。


# おまけ

検索画面で[createdだけ設定して新着順検索](https://qiita.com/search?q=created%3A%3E2022-01-01&sort=created)すると新着記事一覧になります。

一日一万件感謝のフォロー、意味なし！

![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/0822b4c2-490b-1f63-5c0a-6c772b82b57d.png)

検索画面とタイムラインの表示内容が、ほぼ同じになっていますね。
考え方自体は間違っていなかったということでよいでしょう。
問題は、わざわざこんなことしなくてもほぼ同じ機能が既に存在していたということだけですな。

ちなみに実は1記事だけ異なるのですが、これは検索画面は作成日時、タイムラインの場合は更新日時の順になっているからのようです。