0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Chatworkシリーズ #11】過去メッセージを全件取得しようとしたら、APIの「100件の壁」にハマった

0
Last updated at Posted at 2026-03-19

8つのルームから過去メッセージを全部抜いて、FAQ を自動生成したかった。

やりたいことはシンプルだ。Chatwork のルームに蓄積された質問と回答のペアを抽出して、FAQ データベースを作る。手でコピペするのは論外なので、API で全件取得して機械的にフィルタリングする方針にした。

結果として 429 件の FAQ を抽出できたが、そこに至るまでに 3 つの罠にハマった。同じことをやろうとしている人の参考になれば。

罠 1: force=0 だとメッセージが返ってこない

Chatwork API でメッセージを取得するエンドポイントはこれだ。

GET /rooms/{room_id}/messages

最初、パラメータなしで叩いたら空っぽが返ってきた。

curl -s -H "X-ChatWorkToken: $TOKEN" \
  "https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages"
# → [] (空配列)

メッセージは確実にある。なのに空。

原因は force パラメータ。デフォルトは force=0 で、未読メッセージだけを返す仕様になっている。既読のメッセージは返ってこない。

# force=1 で既読含む全件取得
curl -s -H "X-ChatWorkToken: $TOKEN" \
  "https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages?force=1"
# → ドバッとJSON が返ってくる

ドキュメントには書いてあるが、最初の一発が空だと「APIキーが間違ってるのか?」と別の方向に迷走する。force=1 は必須パラメータだと思った方がいい。

罠 2: 100 件の壁

force=1 でメッセージが返ってくるようになった。が、数えてみると 最大 100 件しか返ってこない。

import requests

headers = {"X-ChatWorkToken": TOKEN}
url = f"https://api.chatwork.com/v2/rooms/{room_id}/messages?force=1"
res = requests.get(url, headers=headers)
messages = res.json()
print(len(messages))  # → 100

1 つのルームに数千件のメッセージがあるのに、100 件で打ち切られる。ページネーション用のパラメータは公式ドキュメントに明記されていないが、実際にはレスポンスに含まれる message_id を使って次のページを取得できる。

やり方はこうだ。

def fetch_all_messages(room_id, token):
    """全メッセージをページネーションで取得"""
    all_messages = []
    headers = {"X-ChatWorkToken": token}
    base_url = f"https://api.chatwork.com/v2/rooms/{room_id}/messages"

    # 初回リクエスト
    url = f"{base_url}?force=1"

    while True:
        res = requests.get(url, headers=headers)

        if res.status_code == 204:
            # メッセージなし
            break

        messages = res.json()
        if not messages:
            break

        all_messages.extend(messages)

        # 最後のmessage_idを使って次ページ
        last_id = messages[-1]["message_id"]
        url = f"{base_url}?force=1&message_id={last_id}"

        # レート制限対策
        time.sleep(5)

    return all_messages

ポイントは message_id パラメータ。取得したメッセージの最後の ID を次のリクエストに渡すと、その ID より新しいメッセージが返ってくる。100 件ずつ繰り返して全件取得する。

force=1 のレート制限

ここにもう一つ罠がある。force=15 分に 1 回程度の制限がかかる(公式ドキュメントには明記されていないが、実際に叩いて確認した)。連続で叩くと 429 Too Many Requests が返ってくる。

# 5分待つのは現実的ではないので、sleepを入れつつリトライ
import time

def fetch_with_retry(url, headers, max_retries=3):
    for i in range(max_retries):
        res = requests.get(url, headers=headers)
        if res.status_code == 429:
            wait = 60 * (i + 1)  # 1分、2分、3分と待つ
            print(f"Rate limited. Waiting {wait}s...")
            time.sleep(wait)
            continue
        return res
    raise Exception("Rate limit exceeded")

8 ルーム × 数十ページの取得には、素朴にやると数時間かかる。並列化したくなるが、API トークンごとのレート制限なので並列にしても意味がない。寝る前に回して朝に結果を見る運用にした。

罠 3: HTTP 204 の扱い

メッセージが 0 件のとき、API は 200 + 空配列 ではなく 204 No Content を返す。レスポンスボディが空なので、res.json() を呼ぶと例外が飛ぶ。

res = requests.get(url, headers=headers)

# NG: 204のときjson()が死ぬ
messages = res.json()  # → JSONDecodeError

# OK: ステータスコードを先にチェック
if res.status_code == 204:
    messages = []
else:
    messages = res.json()

これは Chatwork API の設計上の特徴で、他の多くの REST API が空配列を返すのと異なる。知らないとハマる。

取得したメッセージから FAQ を作る

全件取得できたら、次は FAQ の抽出だ。3 段階のフィルタリングで絞り込んだ。

全メッセージ(数千件)
  ↓ ① パターンマッチ(?/でしょうか/ですか 等) → 約1,200件
  ↓ ② LLMフィルタ(質問かどうかを判定)        → 約500件
  ↓ ③ 類似質問のクラスタリング・統合             → 429件

① パターンマッチ

import re

question_patterns = [
    r'',
    r'\?',
    r'でしょうか',
    r'ですか',
    r'ありますか',
    r'できますか',
    r'どうすれば',
    r'教えて',
]

pattern = '|'.join(question_patterns)

questions = [
    msg for msg in all_messages
    if re.search(pattern, msg['body'])
]

② 回答の紐付け

質問だけ抽出しても意味がない。回答とペアにしないと FAQ にならない。Chatwork には [rp] タグでリプライが紐づく。

def find_answer(question_msg, all_messages):
    msg_id = question_msg['message_id']

    # 1. [rp]リプライを探す(最も確実)
    for msg in all_messages:
        if f'[rp aid={question_msg["account"]["account_id"]}' in msg.get('body', ''):
            return msg

    # 2. 時間的近接性(10分以内の返答)
    q_time = question_msg['send_time']
    candidates = [
        msg for msg in all_messages
        if msg['send_time'] > q_time
        and msg['send_time'] - q_time < 600  # 10分
        and msg['account']['account_id'] != question_msg['account']['account_id']
    ]
    if candidates:
        return candidates[0]

    return None

この方法で約 7 割の質問に回答を紐付けられた。残り 3 割は会話の流れの中で暗黙的に解決されていたり、そもそも回答がないものだった。

定常運用: 差分取得の仕組み

全件取得は初回だけでいい。日常的には差分だけ取得する。状態ファイルに最後の message_id を保存しておけばいい。

#!/bin/bash
STATE_FILE=".last_message_id"

# 前回の最終message_idを読む
if [ -f "$STATE_FILE" ]; then
    LAST_ID=$(cat "$STATE_FILE")
    URL="https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages?force=1&message_id=${LAST_ID}"
else
    URL="https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages?force=1"
fi

# メッセージ取得
RESPONSE=$(curl -s -w "\n%{http_code}" -H "X-ChatWorkToken: $TOKEN" "$URL")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')

if [ "$HTTP_CODE" = "204" ]; then
    echo "新着メッセージなし"
    exit 0
fi

# 最新のmessage_idを保存
echo "$BODY" | python3 -c "
import json, sys
msgs = json.load(sys.stdin)
if msgs:
    print(msgs[-1]['message_id'])
" > "$STATE_FILE"

この仕組みで、15 分おきにメッセージを取得して蓄積している。3 週間回して取りこぼしはゼロだ。

まとめ

内容 対策
force=0 既読メッセージが返らない force=1 を常に指定
100 件制限 ページネーションが必要 message_id で次ページ取得
HTTP 204 ボディが空で json() が死ぬ ステータスコードを先にチェック

Chatwork API は素直な REST API だが、このあたりの仕様は実際に叩かないとわからない。ドキュメントに書いてあることと書いてないことの差が大きい。

8 ルーム × 全期間のメッセージから 429 件の FAQ を自動生成できたのは、この地味なページネーション処理のおかげだ。手で FAQ を書くより、過去の実際のやり取りから抽出した方が、現場の言葉遣いそのままで実用的なものになる。


Chatworkシリーズ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?