Postman のビジュアライズ機能で Qiita アドベントカレンダーごとの人気記事を集計

この記事は [Postman Advent Calendar 2023](https://qiita.com/advent-calendar/2023/postman) のシリーズ 3、1日目の記事です。

2023年のアドベントカレンダーの季節が終わりました。記事を投稿した皆さん、大変お疲れ様でございます！

さて、私たちは [Postman Advent Calendar 2023](https://qiita.com/advent-calendar/2023/postman) の中の人でもあるので、今回のプレゼント付きカレンダーの受賞者を選定するという役割があります。受賞の対象は多面的な観点から選ばせていただくものの、記事に対する「いいね数」「ストック数」は重要な評価要素です。

そのため、アドベントカレンダーに投稿された記事に関する情報を集計する必要があるのですが、Qiita には [API があります](https://qiita.com/api/v2/docs)！せっかくですので、Postman の[スクリプト内からのリクエストの送信](https://learning.postman.com/docs/writing-scripts/script-references/postman-sandbox-api-reference/#sending-requests-from-scripts)および[ビジュアライズ機能](https://learning.postman.com/docs/sending-requests/visualizer/)を使って、記事別およびユーザー別のランキングを作ってみました。

<details><summary>Postman Advent Calendar 2023 のランキング（大きいので折りたたんでいます）</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32610/a67469c2-9fe9-2c03-fe65-89b21e4d720a.png)

</details>

Postman をお使いであれば、誰でも任意のアドベントカレンダー（過去の年のカレンダーも含む）のランキングを表示できます。以下で詳しい手順を解説します。

---

まずは Qiita API の利用に必要なアクセストークンを、[Qiita 設定の「アプリケーション」](https://qiita.com/settings/applications)→「個人用アクセストークン」から発行します。スコープは「read_qiita」にチェックが入っていれば OK です。一度しか表示されないので、忘れずにコピーしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32610/e8950ace-5f7b-47ba-6af1-953ced8ebfd2.png)

ランキング表示のリクエストが入っているコレクションは、**Postman Japan** のパブリックワークスペース（世界中の誰もがアクセスできるワークスペース）で公開しています。まずはお手元の Postman でこのワークスペースを検索します。

Postman アプリの上部にある検索窓で「Postman Japan」と入力し、Postman Japanパブリックワークスペースを探します。ワークスペースは、四角が4つ組み合わさったアイコンで表示されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32610/aa5c7570-a4f7-d431-77a0-b8ca423b0064.png)

「Postman Japan」を選び、ワークスペースに移動します。次に、ワークスペースサイドバーで「コレクション」が選択されていることを確認します。コレクション「Qiita」が今回使うコレクションですが、パブリックワークスペース上では直接リクエストの実行ができないので、まずはコレクションを自分のプライベートワークスペースに**フォーク**します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32610/1669e79f-a2df-85fd-0992-9720833a12ca.png)

サイドバーのコレクション「Qiita」表示の右端にある「•••」メニューから「フォークを作成」を選択します。「コレクションをフォーク」画面の「ワークスペース」で自分のプライベートワークスペースを選択します。デフォルトではどのユーザーも「My Workspace」というプライベートワークスペースを持っているので、特に好みがなければこれを選ぶと良いでしょう。そして「コレクションをフォーク」ボタンを押してフォークします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32610/3452d01f-f4b5-8a36-2694-0e8a1f021208.png)

フォークを行うとフォーク先のプライベートワークスペースに移動しますが、ここでは戻るボタン（画面左上の「←」）をクリックして「Postman Japan」ワークスペースに戻ります。今度は、ワークスペースサイドバーで「環境」を選択します。サイドバーの環境「Qiita」の「•••」メニューから「フォークを作成」を選択します。「環境をフォーク」画面の「ワークスペース」で自分のプライベートワークスペースを選択し、「環境をフォーク」ボタンを押して環境もフォークします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32610/ee9eb7f5-aa22-e861-1d8b-f4c818621316.png)

さて、ここで自分のプライベートワークスペースにいること、そしてワークスペースサイドバーで「環境」が選択されていることを確認します。サイドバーにあるフォークされた環境「Qiita」を選択して、下記のように変数「token」の初期値および現在値に Qiita アクセストークンを入力し、保存ボタンを押します。

変数 | タイプ | 初期値 | 現在値
--- | --- | --- | ---
token | シークレット | \<Qiita アクセストークン\> | \<Qiita アクセストークン\>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32610/38957eb5-fcf5-dd53-bea5-1c5764bcabe5.png)

ここで設定した変数はそのままでは使用されないため、画面右上の環境メニューから「Qiita」を選択してアクティブにするのを忘れないようにしましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32610/3a56e7d4-2b4f-0c92-44dc-24959eabfb0e.png)

続いて、ワークスペースサイドバーで「コレクション」を選択すると、サイドバーにフォークされたコレクション「Qiita」が見えます。このコレクションを選択し、「認証」タブを開いてみましょう。認証タイプとして「Bearer トークン」が選択されており、トークンに変数「token」が参照されるように設定されています。この設定により、Qiita API ドキュメントの[アクセストークン](https://qiita.com/api/v2/docs#アクセストークン)で説明されている通り、コレクションのすべてのリクエストの Authorization リクエストヘッダに、アクセストークンが含められるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32610/d8265c37-b648-c8da-fdd8-4d4bfaeed7d1.png)

このコレクションを展開して、中にあるリクエスト「Advent Calendar ランキング」を選択し、中身を詳しく見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32610/1fb362ab-6f0b-6487-bbc3-226b7fa9293d.png)

これは、次のようなエンドポイントです。

```
GET https://qiita.com/advent-calendar/:year/:calendar
```

「year」と「calendar」の 2 つのパス変数をパラメーターとして指定する形です。デフォルトでは2023年の Postman アドベントカレンダーを対象にするようになっています。

キー | 値
--- | ---
year | 2023
calendar | postman

実はこのリクエストはアドベントカレンダーのメインページの HTML を GET メソッドで取得するだけなのですが、テストスクリプトの中で Qiita API や外部サイトへのアクセスを行い、各記事のいいね数、ストック数などの情報を収集して可視化を行なっています。

以下では、「テスト」タブに記入されているスクリプトについて説明していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32610/65d39a08-434c-70b9-1e61-dc1694da2dff.png)

冒頭、ビジュアライズ機能のための、HTML のテンプレート文字列が定義されています。これは [Handlebars](https://handlebarsjs.com) テンプレートエンジンの形式に基づいており、二重波括弧内で入力が評価されます。本スクリプトでは、記事別ランキングとユーザー別ランキングの 2 つの表を作っています。

```js
// ビジュアライザーテンプレート定義
const template = `
    <h5>{{name}} Advent Calendar {{year}} 記事別ランキング</h5>
    <table bgcolor="#FFFFFF">
        <tr>
            <th>タイトル</th>
            <th>ユーザーID</th>
            <th>いいね数</th>
            <th>ストック数</th>
        </tr>

        {{#each responses}}
            <tr>
                <td>{{title}}</td>
                <td><img src="{{user.profile_image_url}}" height="32" style="vertical-align: middle"> {{user.id}}</td>
                <td>{{likes_count}}</td>
                <td>{{stocks_count}}</td>
            </tr>
        {{/each}}
    </table>
    <h5>{{name}} Advent Calendar {{year}} ユーザー別ランキング</h5>
    <table bgcolor="#FFFFFF">
        <tr>
            <th>ユーザーID</th>
            <th>いいね数</th>
            <th>ストック数</th>
            <th>投稿数</th>
        </tr>

        {{#each users}}
            <tr>
                <td><img src="{{user.profile_image_url}}" height="32" style="vertical-align: middle"> {{user.id}}</td>
                <td>{{likes_count}}</td>
                <td>{{stocks_count}}</td>
                <td>{{items_count}}</td>
            </tr>
        {{/each}}
    </table>
`;
```

次の部分は、レスポンスを取得して、レスポンスのテキストからカレンダー名、年、投稿のリストを取得しています。

```javascript
// レスポンス文字列を取得
const response = pm.response.text();
// 最後のscriptタグのマッチを取得
const matches = response.match(/<script.*?>(.*?)<\/script>.*?$/);
// カレンダー名、年、投稿のリストを取得
const {name, year, items} = JSON.parse(matches[1]).adventCalendars.tableAdventCalendars[0];
```

次は、各投稿が Qiita の記事か、Zenn の記事か、それ以外かを判定し、Qiita の記事の場合は Qiita API を呼び出し、Zenn の記事の場合は記事ページをリクエストしています。[`pm.environment.get()`](https://learning.postman.com/docs/writing-scripts/script-references/postman-sandbox-api-reference/#using-environment-variables-in-scripts) を使って環境からアクセストークンを取得している点、[`pm.sendRequest()`](https://learning.postman.com/docs/writing-scripts/script-references/postman-sandbox-api-reference/#sending-requests-from-scripts) を使って非同期にリクエストを送信している点がポイントです。取得したデータは `Promise` のリストとして格納されます。

```js
// APIトークンを取得
const token = pm.environment.get('token');
// 各投稿のデータを非同期で取得
const promises = [];
for (const {comment, url, user} of items) {
    // 投稿があったかチェック
    if (url) {
        // Qiitaの投稿かチェック
        if (url.startsWith('https://qiita.com')) {
            // 記事IDの取得
            const id = url.match(/\w+$/)[0];
            promises.push(new Promise((resolve, reject) => {
                // Qiita API呼び出し
                pm.sendRequest({
                    url: `https://qiita.com/api/v2/items/${id}`,
                    header: {'Authorization': `Bearer ${token}`}
                }, (error, response) => {
                    if (error) {
                        reject(error);
                    } else {
                        resolve(response.json());
                    }
                });
            }));
        } else if (url.startsWith('https://zenn.dev')) {
            // Zennの投稿
            promises.push(new Promise((resolve, reject) => {
                // 記事を取得
                pm.sendRequest(url, (error, response) => {
                    if (error) {
                        reject(error);
                    } else {
                        // いいね数の取得
                        const matches = response.text().match(/<script.*?>(.*?)<\/script>.*?$/);
                        const {likedCount} = JSON.parse(matches[1]).props.pageProps.article;
                        resolve({
                            title: comment,
                            user: {id: user.urlName, profile_image_url: user.profileImageUrl},
                            likes_count: likedCount,
                            stocks_count: 0
                        });
                    }
                });
            }));
        } else {
            // Qiita、Zenn以外の投稿
            promises.push(Promise.resolve({
                title: comment,
                user: {id: user.urlName, profile_image_url: user.profileImageUrl},
                likes_count: 0,
                stocks_count: 0
            }));
        }
    }
}
```

最後に、すべての非同期レスポンスが得られるまで待機した後、取得したいいね数、ストック数、ユーザー毎の記事数をもとに、投稿リストとユーザーリストを並べ替えて、[`pm.visualizer.set()`](https://learning.postman.com/docs/writing-scripts/script-references/postman-sandbox-api-reference/#scripting-postman-visualizations) でビジュアライザーにテンプレートと入力データをセットします。

```js
// すべてのレスポンスを待機
Promise.all(promises).then(responses => {
    // 投稿をいいね数を第一優先、ストック数を第二優先で並び替え
    responses.sort(sortFn);
    // ユーザーで集計
    dict = {};
    for (const {user, likes_count, stocks_count} of responses) {
        if (dict[user.id]) {
            dict[user.id].likes_count += likes_count;
            dict[user.id].stocks_count += stocks_count;
            dict[user.id].items_count++;
        } else {
            dict[user.id] = {user, likes_count, stocks_count, items_count: 1};
        }
    }
    const users = Object.values(dict);
    // ユーザーをいいね数を第一優先、ストック数を第二優先、投稿数を第三優先で並び替え
    users.sort(sortFn);
    // ビジュアライザーにデータをセット
    pm.visualizer.set(template, {name, year, responses, users});
});
```

こちらはソート用の関数です。各オブジェクトを、いいね数を第一優先、ストック数を第二優先、投稿数を第三優先で並び替えます。

```js
// 並べ替え関数
const sortFn = (a, b) => {
    if (a.likes_count !== b.likes_count) {
        return b.likes_count - a.likes_count || 0;
    }
    if (a.stocks_count !== b.stocks_count) {
        return b.stocks_count - a.stocks_count || 0;
    }
    return b.items_count - a.items_count || 0;
};
```

ここまで確認したら、リクエストの「送信」ボタンをクリックしてみましょう。レスポンスの「ボディ」タブの「可視化」ボタンを押すと、ランキングが表示されているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32610/9c2bb814-13a1-d1a4-d936-63b484ad962b.png)

パス変数を書き換えることで、任意の年の任意のカレンダーのランキングを表示することができます。可視化ビューにうまく結果が表示されない、という方は、見落としがちな次の点を再確認してください。

- 環境「Qiita」画面で、変数を設定した後に「保存」ボタンをクリックしたか
- リクエスト「Advent Calendar ランキング」を実行する時に、画面右上の環境メニューで「Qiita」が選択されてアクティブになっているか