0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonでX(Twitter)完全自動投稿システムを作った話【Claude API × Windows タスクスケジューラー】

0
Posted at

はじめに

LINEで買い物リストを共有できるサービス「Share Cart」の認知拡大を目的に、X(Twitter)への投稿を完全自動化しました。

  • リサーチ → 生成 → レビュー → スケジュール → 投稿 → 分析 → 改善
  • 7ステップのサイクルがすべて自動で回ります
  • Windows PC(タスクスケジューラー)のみで完結、サーバー不要

システム全体像

毎朝 6:00 [run_daily.py]
  ├─ 1_research.py   Claude web_search でトレンドリサーチ
  └─ 4_scheduler.py  朝・夜の投稿時刻スロットを登録

30分ごと [run_post.py]
  └─ 投稿時刻を過ぎたスロットを検知
      ├─ 2_generate.py  Claude でその場で投稿文生成
      ├─ 3_review.py    Claude でレビュー・修正
      └─ 5_post_to_x.py X API v2 で投稿

毎週月曜 9:00 [run_weekly.py]
  ├─ 6_analyze.py   投稿パフォーマンス分析
  └─ 7_improve.py   Claude で改善提案 → 次週の生成に反映

技術スタック

用途 ツール
言語 Python 3.13
AI Claude API(claude-sonnet-4-6)
X投稿 X API v2 / OAuth 2.0 User Context
スケジューラー Windows タスクスケジューラー
環境 Windows 11 / ローカルPC

ファイル構成

x-auto/
├── .env                  # API認証情報
├── config.py             # 共通設定・ロガー
├── loader.py             # 数字プレフィックスモジュールの動的ロード
├── account_profile.py    # AIキャラクター人格定義
├── 1_research.py         # トレンドリサーチ
├── 2_generate.py         # 投稿文生成(20/80ルール)
├── 3_review.py           # 品質レビュー・自動修正
├── 4_scheduler.py        # 投稿時刻管理
├── 5_post_to_x.py        # X API投稿
├── 6_analyze.py          # パフォーマンス分析
├── 7_improve.py          # 自動改善
├── run_daily.py          # 毎朝6:00バッチ
├── run_post.py           # 30分ごとの投稿チェック
├── run_weekly.py         # 週次分析・改善バッチ
├── setup_tasks.py        # タスクスケジューラー登録
└── data/
    ├── latest_research.json
    ├── posts_queue.json
    ├── posted_history.json
    └── improvements.json

ポイント解説

1. 数字プレフィックスファイルの動的ロード

Pythonは 1_research.py のように数字で始まるファイルを import でロードできません。importlib を使って解決しました。

# loader.py
import importlib.util, sys
from pathlib import Path

BASE = Path("C:/Users/user/x-auto")

def load(filename: str):
    path = BASE / filename
    name = filename.replace(".py", "").replace("-", "_")
    spec = importlib.util.spec_from_file_location(name, path)
    mod  = importlib.util.module_from_spec(spec)
    sys.modules[name] = mod
    spec.loader.exec_module(mod)
    return mod
# 使い方
from loader import load
research = load("1_research.py").run_research()

2. リサーチ(1_research.py)

Claude の web_search ツールを使い、6ジャンルのトレンドを一括取得します。

resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=5000,
    tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 5}],
    messages=[{"role": "user", "content": prompt}]
)

取得するジャンル:

  • 日常・生活
  • Web系開発
  • WordPress
  • AI
  • Webセキュリティ
  • Share Cart関連(買い物リスト共有)

web_searchが失敗した場合は 60秒待機後にフォールバック(web_searchなしで生成)、それも失敗したら前回キャッシュを使う3段構えにしています。


3. 投稿文生成(2_generate.py)

20/80ジャンル比率ロジック

投稿の約20%をサービス訴求(Share Cart)、80%を一般ジャンルにする比率を維持しつつ、同一ジャンルが連続2回を超えないように管理します。

ALL_GENRES     = ["日常", "Web開発", "WordPress", "AI", "セキュリティ", "ShareCart"]
MAX_CONSECUTIVE = 2

def _pick_one_genre(recent: list[str]) -> str:
    # 直近MAX_CONSECUTIVE件が全て同じなら除外
    if len(recent) >= MAX_CONSECUTIVE and len(set(recent[-MAX_CONSECUTIVE:])) == 1:
        banned = recent[-1]
    else:
        banned = None

    candidates, weights = [], []
    for g in ALL_GENRES:
        if g == banned:
            continue
        candidates.append(g)
        weights.append(1 if g == "ShareCart" else 4)  # 20%:80%

    return random.choices(candidates, weights=weights, k=1)[0]

ジャンル履歴は data/genre_history.json に保存し、セッションをまたいで連続制限を維持します。

エンゲージメント最大化プロンプト

【エンゲージメント最大化ルール】
1. 1行目は必ず「フック」: 疑問形・あるある・数字インパクト
2. 文章ごとに改行(1文1行)、段落間は空行
3. ハッシュタグは必ず2$301C3個
4. ShareCartのみLINEリンクを含める
5. 具体的シーン・数字を必ず1つ入れる

生成される投稿例(日常ジャンル):

4月3日でもう疲れてるの、正常モー$D83D$DC2E

新しい現場のREADME開いたら知らないツールしか書いてなかったモー$D83D$DC04
桜が散るスピードとやる気が散るスピード、完全一致してるモー$D83D$DC2E

会社で疲弊しながら夜に個人開発してる牛がここにいるモー$D83D$DC04

#新生活疲れ #エンジニアあるある #4月の洗礼

4. レビュー・自動修正(3_review.py)

生成した投稿をClaudeが審査し、問題があれば自動で修正します。

審査項目(抜粋):

  • AI生成っぽくないか(定型文チェック)
  • 改行が正しく入っているか → なければ自動修正
  • ハッシュタグが2$301C3個あるか → 不足なら自動追加
  • ShareCart以外にLINEリンクが混入していないか → あれば自動削除
  • 140字以内か(URL=23字換算、改行=1字換算)
# 修正テキストがあれば採用
if review.get("revised_text") and review["revised_text"] != post["text"]:
    post["text"] = review["revised_text"]

5. 投稿スケジューラー(4_scheduler.py)

ボット検知を避けるため、投稿時刻をランダム化します。

# X.comエンゲージメント研究に基づく時間帯
# 朝: 7:02$301C7:57(通勤ピーク、投稿者が少なく競合が少ない)
# 夜: 20:01$301C21:28(ゴールデンタイム、エンゲージ率15$301C20%)
MORNING_MIN = 7 * 60 + 2
MORNING_MAX = 7 * 60 + 57
EVENING_MIN = 20 * 60 + 1
EVENING_MAX = 21 * 60 + 28

def _rand_time(min_minute: int, max_minute: int) -> str:
    m   = random.randint(min_minute, max_minute)
    sec = random.randint(5, 58)  # 秒もランダム化
    return f"{m//60:02d}:{m%60:02d}:{sec:02d}"

:00/:30 を避ける理由:X.comでは70%のBot投稿がキリの良い時刻に集中するため、アルゴリズムが切りの良い時刻の投稿を低優先扱いするという研究結果に基づいています。

「スロット登録→投稿時に生成」方式

毎朝6:00に投稿時刻だけを登録し、投稿文はその時刻になってから生成します。これにより常に最新のトレンドで投稿できます。

6:00 run_daily → 「07:23:41」「20:44:17」という時刻だけ登録
       ↓
07:23 run_post → 時刻検知 → その場でリサーチ結果を使って生成・レビュー・投稿

6. X API v2 投稿(5_post_to_x.py)

OAuth 2.0 User Context でテキスト投稿します。トークン期限切れ(401)を自動検知してリフレッシュトークンで再取得します。

def _post_with_oauth2(text: str) -> str:
    import requests
    url     = "https://api.twitter.com/2/tweets"
    headers = {"Authorization": f"Bearer {config.X_OAUTH2_TOKEN}",
               "Content-Type": "application/json"}
    res = requests.post(url, headers=headers, json={"text": text})

    if res.status_code == 401:
        _refresh_oauth2_token()  # リフレッシュして再試行
        headers["Authorization"] = f"Bearer {config.X_OAUTH2_TOKEN}"
        res = requests.post(url, headers=headers, json={"text": text})

    if not res.ok:
        raise RuntimeError(f"X API エラー {res.status_code}: {res.text}")
    return str(res.json()["data"]["id"])

7. 週次分析・自動改善(6_analyze.py / 7_improve.py)

毎週月曜9:00に過去7日間の投稿パフォーマンスをX APIで取得し、Claudeが改善提案を生成します。

# 分析項目
{
  "best_pattern": "最も効果的な投稿パターン",
  "best_hook_type": "最も効果的なフック手法",
  "best_hashtags": ["効果的なハッシュタグ"],
  "recommended_hashtags_next": ["次週推奨タグ"],
  "new_angles": [{"keyword": "...", "angle": "...", "hook_suggestion": "..."}],
  "morning_min": 422,  # 分単位 → 投稿時間帯も自動更新
  "morning_max": 457
}

改善提案は翌日の生成プロンプトに自動反映されます。


8. Windows タスクスケジューラー登録

# setup_tasks.py(抜粋)

# 30分ごとの繰り返しタスク(終了日なし)
cmd = (
    f'schtasks /Create /TN "XAuto_PostCheck" '
    f'/TR "{action}" '
    f'/SC MINUTE /MO 30 '  # /ET /K は付けない(当日限定になるバグ回避)
    f'/F'
)

ハマりポイント/ET 23:59 /K オプションを付けると終了時刻だけでなく終了日も当日に固定されてしまい、翌日以降タスクが動かなくなります。繰り返しタスクには /ET /K を付けないのが正解です。


ハマったポイントまとめ

問題 原因 解決策
import 1_research が失敗 Pythonは数字始まりファイルをimport不可 importlib.util で動的ロード
APIレート制限(429)で連続失敗 web_search失敗後すぐにフォールバックAPI呼び出し 失敗後60秒待機してからフォールバック
タスクが翌日以降動かない /ET 23:59 /K が終了日を当日に固定 /ET /K を削除して再登録
日本語ログが文字化け Windows端末がcp932、ログはUTF-8 FileHandlerにencoding="utf-8"、StreamHandlerはstderr
OAuth 2.0で投稿403エラー user_authパラメータのデフォルトがOAuth 1.0aを使おうとする requestsでX API v2に直接POSTする方式に変更
ShareCart以外の投稿にLINEリンク混入 生成プロンプトの指示が不明瞭 レビュー時にShareCart以外のリンクを自動削除するルールを追加

今後の改善予定

  • 週次分析データが蓄積されてから自動改善サイクルの効果を検証
  • リプライ・引用RTへの自動反応
  • トレンドハッシュタグのリアルタイム取得精度向上

まとめ

  • Claude API でリサーチ・生成・レビュー・改善の4役をこなす
  • 20/80ルールでサービス訴求と一般コンテンツのバランスを自動管理
  • 「スロット登録→投稿時生成」方式で常に最新トレンドの投稿を実現
  • Windows タスクスケジューラーだけでサーバーレスに完全自動稼働

サーバー不要でローカルPCだけで動く構成なので、個人開発者でもすぐに試せます。


使用サービス: Share Cart - LINEで買い物リストを家族・友人と共有

0
2
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
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?