Qiitaの投稿数やリアクション数を集計する

# はじめに

特定期間の自身の活動を振り返るため、Qiitaの投稿数やリアクション数を集計するプログラムを作成しました。

# コード

```
pip install pydantic-settings
```

```python:qiita_count.py
import datetime
from collections import defaultdict

import requests
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    qiita_token: str = Field(..., description="Qiitaのアクセストークン")
    start_month: str = Field(default="1900-01", description="合計期間の開始月")
    end_month: str = Field(default="2100-01", description="合計期間の終了月")

    model_config = SettingsConfigDict(
        env_file=".env",
        case_sensitive=False,
        cli_parse_args=True,
        extra="allow",
    )


def main():
    # 設定の読み込み
    settings = Settings()

    # ここにQiitaのアクセストークンを設定
    ACCESS_TOKEN = settings.qiita_token

    headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}

    # 認証済みユーザーの投稿を取得するエンドポイント
    url = "https://qiita.com/api/v2/authenticated_user/items"

    items = []
    page = 1
    per_page = 100  # 1ページあたりの取得件数

    # ページネーションに対応してすべての投稿を取得する
    while True:
        params = {"page": page, "per_page": per_page}
        resp = requests.get(url, headers=headers, params=params)
        if resp.status_code != 200:
            print("Error:", resp.text)
            break
        data = resp.json()
        if not data:  # 取得データがなくなったらループ終了
            break
        items.extend(data)
        page += 1

    # 月ごとの投稿数とリアクション数を集計する
    monthly_stats = defaultdict(
        lambda: {
            "post_count": 0,
            "likes_count": 0,
            "stocks_count": 0,
            "page_views_count": 0,
        }
    )
    for item in items:
        created_at = item.get("created_at")
        # 日時文字列をdatetimeオブジェクトに変換
        dt = datetime.datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S%z")
        month_str = dt.strftime("%Y-%m")
        if settings.start_month <= month_str <= settings.end_month:
            monthly_stats[month_str]["post_count"] += 1
            monthly_stats[month_str]["likes_count"] += item.get("likes_count", 0)
            monthly_stats[month_str]["stocks_count"] += item.get("stocks_count", 0)
            monthly_stats[month_str]["page_views_count"] += item.get(
                "page_views_count", 0
            )

    # 結果の出力
    for month, stats in sorted(monthly_stats.items()):
        print(
            f"{month}: 投稿数: {stats['post_count']}, いいね数: {stats['likes_count']}, ストック数: {stats['stocks_count']}, 閲覧数: {stats['page_views_count']}"
        )

    # 指定された期間の合計を計算
    total_posts = sum(
        monthly_stats[m]["post_count"]
        for m in monthly_stats
        if settings.start_month <= m <= settings.end_month
    )
    total_reactions = sum(
        monthly_stats[m]["likes_count"] + monthly_stats[m]["stocks_count"]
        for m in monthly_stats
        if settings.start_month <= m <= settings.end_month
    )
    total_page_views = sum(
        monthly_stats[m]["page_views_count"]
        for m in monthly_stats
        if settings.start_month <= m <= settings.end_month
    )
    print(f"\n{settings.start_month}から{settings.end_month}の合計:")
    print(f"投稿数: {total_posts}")
    print(f"リアクション数: {total_reactions}")
    print(f"閲覧数: {total_page_views}")


if __name__ == "__main__":
    main()
```

出力は以下のような感じです。
QIITA_TOKENを環境変数に設定しますstart_month、end_monthで指定した期間の月ごとの数値と合計を出力します。
合計を出力する際、いいね数とストック数はリアクション数としてまとめられます。

```
% python qiita_count.py --start_month 2024-10 --end_month 2025-03
2024-10: 投稿数: 19, いいね数: 13, ストック数: 4, 閲覧数: 15617
2024-11: 投稿数: 8, いいね数: 9, ストック数: 1, 閲覧数: 6732
2024-12: 投稿数: 6, いいね数: 3, ストック数: 3, 閲覧数: 3802
2025-01: 投稿数: 6, いいね数: 2, ストック数: 3, 閲覧数: 4291
2025-02: 投稿数: 5, いいね数: 1, ストック数: 0, 閲覧数: 1882
2025-03: 投稿数: 7, いいね数: 1, ストック数: 0, 閲覧数: 894

2024-10から2025-03の合計:
投稿数: 51
リアクション数: 40
閲覧数: 33218
```

# 解説

## 設定値の取得

pydantic-settingを使うことで、環境変数またはコマンドライン引数から必要な情報を取得します。両方で指定される場合はコマンドライン引数が優先されます。

```python
class Settings(BaseSettings):
    qiita_token: str = Field(..., description="Qiitaのアクセストークン")
    start_month: str = Field(default="1900-01", description="合計期間の開始月")
    end_month: str = Field(default="2100-01", description="合計期間の終了月")

    model_config = SettingsConfigDict(
        env_file=".env",
        case_sensitive=False,
        cli_parse_args=True,
        extra="allow",
    )
```

## 投稿数とリアクション数の取得

QIITA_TOKENの発行元ユーザのすべての投稿を取得し、月単位で合計します。

```python
    # 認証済みユーザーの投稿を取得するエンドポイント
    url = "https://qiita.com/api/v2/authenticated_user/items"

    items = []
    page = 1
    per_page = 100  # 1ページあたりの取得件数

    # ページネーションに対応してすべての投稿を取得する
    while True:
        params = {"page": page, "per_page": per_page}
        resp = requests.get(url, headers=headers, params=params)
        if resp.status_code != 200:
            print("Error:", resp.text)
            break
        data = resp.json()
        if not data:  # 取得データがなくなったらループ終了
            break
        items.extend(data)
        page += 1

    # 月ごとの投稿数とリアクション数を集計する
    monthly_stats = defaultdict(
        lambda: {
            "post_count": 0,
            "likes_count": 0,
            "stocks_count": 0,
            "page_views_count": 0,
        }
    )
    for item in items:
        created_at = item.get("created_at")
        # 日時文字列をdatetimeオブジェクトに変換
        dt = datetime.datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S%z")
        month_str = dt.strftime("%Y-%m")
        if settings.start_month <= month_str <= settings.end_month:
            monthly_stats[month_str]["post_count"] += 1
            monthly_stats[month_str]["likes_count"] += item.get("likes_count", 0)
            monthly_stats[month_str]["stocks_count"] += item.get("stocks_count", 0)
            monthly_stats[month_str]["page_views_count"] += item.get(
                "page_views_count", 0
            )
```