WordPress REST API × Python で記事を自動投稿する完全ガイド【Application Password対応】

毎回WordPressの管理画面を開いて記事を貼り付ける作業、もう終わりにしませんか?Python × REST APIを使えば、記事の投稿・更新・カテゴリ設定まで数行のコードで完全自動化できます。スケジュール投稿・CSV一括投稿・外部ツール連携など、アイデア次第で自動化の幅は無限に広がります。この記事では、認証設定のつまずきポイントを丁寧に解説しながら、実際に動くPythonコードをフルセットでお届けします。

目次

フェーズ1:WordPress REST APIとPythonの相性が抜群な理由

WordPress REST APIとは何か

WordPress REST API(REST APIとは「Representational State Transfer」の略で、HTTPを使ってデータをやり取りするインターフェースのことです)とは、外部のプログラムやサービスからWordPressを操作できる仕組みのことです。WordPressが標準で用意しているこのAPIを使うと、ブラウザで管理画面を開かなくても、記事の投稿・更新・削除・カテゴリ管理といった操作をHTTPリクエスト一本で実行できます。

エンドポイント(APIへのアクセス先URL)の基本形は以下のとおりです。

https://あなたのサイト.com/wp-json/wp/v2/posts

このURLに対してPythonからGETPOSTPUTDELETEなどのHTTPメソッドを送ることで、記事一覧の取得から新規投稿・更新・削除まで自由自在に操作できます。WordPress 4.7以降は標準機能として組み込まれているため、プラグインなしで即利用可能です。

手動投稿との比較:管理画面 vs APIコール

管理画面での手動投稿は、ログイン→新規投稿→タイトル入力→本文貼り付け→カテゴリ選択→公開、という複数ステップが必要です。記事が1本なら問題ありませんが、10本・100本となると話は変わります。REST API経由なら、この一連の操作がPythonのコード数行に集約されます。

自動化できる主な操作は以下のとおりです。

  • 記事の新規投稿(下書き・公開・予約投稿)
  • 既存記事のタイトル・本文・ステータス更新
  • 記事の削除・ゴミ箱移動
  • カテゴリ・タグの作成・取得・紐付け
  • メディア(画像)のアップロードとアイキャッチ設定
  • ページ・カスタム投稿タイプの操作

よくあるつまずきポイントの全体像

WordPress REST APIとPythonの組み合わせで初心者がつまずく場所は、大きく3つに分類されます。

① 認証エラー(401 Unauthorized):Application Passwordの設定ミス、ユーザー名の誤り、スペースの扱いなど。フェーズ2で詳しく解説します。

② エンドポイントの誤り(404 Not Found):URLの末尾スラッシュ、パーマリンク設定が「プレーン」になっているとREST APIが機能しない場合があります。パーマリンク設定を「投稿名」などに変更しましょう。

③ パーミッション不足(403 Forbidden):使用しているWordPressユーザーの権限が不足している場合に発生します。記事投稿には「編集者」以上の権限が必要です。

これら3つのポイントを押さえておくだけで、実装時のトラブルの9割は回避できます。参考として、実際に自動投稿を試みた方の事例も参考になります。【Python】WordPressで記事の自動投稿できるようになる|仮想サーファー@さぼりエンジニアでは、つまずきポイントの実体験が詳しくまとめられています。

フェーズ2:認証設定の壁を突破する — Application Passwordの発行と確認

Application Passwordとは何か

Application Password(アプリケーションパスワード)とは、WordPress 5.6以降に標準搭載された、外部アプリケーション専用のパスワード機能です。通常のログインパスワードとは別に発行できるため、仮にAPIアクセス用のパスワードが漏洩しても、管理画面へのログインには使えません。セキュリティ上の分離ができる点が大きなメリットです。

また、Application Passwordはアプリケーションごとに複数発行でき、不要になったら個別に無効化できます。本番環境・テスト環境・外部ツールごとに別々のパスワードを発行しておくと管理が楽になります。

発行手順ステップバイステップ

以下の手順でApplication Passwordを発行します。

  1. WordPress管理画面にログインします。
  2. 左メニューの「ユーザー」→「プロフィール」をクリックします。
  3. ページを下にスクロールすると「アプリケーションパスワード」セクションが表示されます。
  4. 「新しいアプリケーションパスワード名」の入力欄に任意の名前(例:「Python自動投稿」)を入力します。
  5. 「新しいアプリケーションパスワードを追加」ボタンをクリックします。
  6. スペース区切りの24文字のパスワードが表示されます。この画面を閉じると二度と確認できないため、必ずコピーしてください。

⚠️ 注意:Application Passwordセクションが表示されない場合は、サイトがHTTPS(SSL)で動作していない可能性があります。ローカル環境ではHTTPSなしでも動作する場合がありますが、本番環境では必ずSSLを有効にしてください。

Basic認証の組み立て方

WordPress REST APIの認証には、ユーザー名とApplication PasswordをBase64エンコード(バイナリデータをテキストに変換する方式)して送る「Basic認証」を使います。Pythonのrequestsライブラリでは、session.auth = (ユーザー名, パスワード)と指定するだけで自動的にBase64エンコードとヘッダー設定を行ってくれるため、手動でエンコードする必要はありません。

Application Passwordに含まれるスペースは、requestsライブラリがそのまま処理してくれるため、スペースを除去する必要はありません。ただし、.envファイルから読み込む場合は引用符で囲まないようにしましょう。

よくある認証エラーと対処法

エラーコード 主な原因 対処法
401 Unauthorized ユーザー名またはApplication Passwordの誤り ユーザー名は「メールアドレス」ではなく「ログイン名」を使用する
403 Forbidden ユーザーの権限不足 ユーザーの権限を「編集者」または「管理者」に変更する
404 Not Found パーマリンク設定が「プレーン」 設定→パーマリンクを「投稿名」などに変更して保存
rest_cannot_create 投稿作成権限がない 使用ユーザーの権限を確認・昇格する

詳しい実装例はWordPressのREST APIを使ってPythonから自動投稿 | lifetechiaも参考になります。

フェーズ3:Pythonで実装する — 投稿・更新・カテゴリ管理のコード全解説

環境準備:必要なライブラリのインストール

まず、必要なPythonライブラリをインストールします。requestsはHTTP通信ライブラリ、python-dotenv.envファイルから環境変数を読み込むためのライブラリです。

pip install requests python-dotenv

認証情報をコードに直書きするのはセキュリティ上NGです。必ず.envファイルに分離し、.gitignoreに追加してGitリポジトリに含めないようにしましょう。

フェーズ3:実装手順

① .env ファイルの準備

プロジェクトルートに .env を作成し、WordPress の接続情報を記載します。Application Password は WordPress 管理画面の「ユーザー → プロフィール → アプリケーションパスワード」から生成してください。

WP_BASE_URL=https://example.com
WP_USER=your_username
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx

② 共通クライアント関数(認証ヘッダー生成)

Application Password を使った Basic 認証ヘッダーを生成し、requests.Session に設定する共通クライアント関数です。全スクリプトからインポートして使います。

# wp_client.py
import os
import requests
from dotenv import load_dotenv

load_dotenv()

BASE_URL = os.getenv("WP_BASE_URL").rstrip("/")
WP_USER = os.getenv("WP_USER")
WP_APP_PASSWORD = os.getenv("WP_APP_PASSWORD")

def get_wp_session() -> requests.Session:
    session = requests.Session()
    session.auth = (WP_USER, WP_APP_PASSWORD)
    session.headers.update({"Content-Type": "application/json"})
    return session

def api_url(endpoint: str) -> str:
    return f"{BASE_URL}/wp-json/wp/v2/{endpoint.lstrip('/')}"

③ 新規記事を投稿する(POST)

タイトル・本文・カテゴリ ID・公開ステータスを指定して新規記事を投稿します。成功すると投稿された記事の ID と URL を返します。

# post_article.py
from wp_client import get_wp_session, api_url

def create_post(title: str, content: str, category_ids: list[int], status: str = "publish") -> dict:
    session = get_wp_session()
    payload = {
        "title": title,
        "content": content,
        "categories": category_ids,
        "status": status,
    }
    response = session.post(api_url("posts"), json=payload)
    response.raise_for_status()
    data = response.json()
    return {"id": data["id"], "url": data["link"]}

if __name__ == "__main__":
    result = create_post(
        title="Python から自動投稿テスト",
        content="

これは REST API 経由で投稿した記事です。

", category_ids=[3], status="publish", ) print(f"投稿完了 → ID: {result['id']}, URL: {result['url']}")

④ 既存記事を更新する(PUT)

記事 ID を指定してタイトルと本文を更新します。変更したいフィールドだけ payload に含めれば部分更新も可能です。

# update_article.py
from wp_client import get_wp_session, api_url

def update_post(post_id: int, title: str = None, content: str = None) -> dict:
    session = get_wp_session()
    payload = {}
    if title is not None:
        payload["title"] = title
    if content is not None:
        payload["content"] = content

    response = session.put(api_url(f"posts/{post_id}"), json=payload)
    response.raise_for_status()
    data = response.json()
    return {"id": data["id"], "url": data["link"]}

if __name__ == "__main__":
    result = update_post(
        post_id=42,
        title="更新後のタイトル",
        content="

本文を更新しました。

", ) print(f"更新完了 → ID: {result['id']}, URL: {result['url']}")

⑤ カテゴリ一覧を取得して名前から ID を逆引きする(GET)

WordPress に登録されているカテゴリを全件取得し、カテゴリ名をキー・ID を値とする辞書を返します。CSV 一括投稿時にカテゴリ名から ID を解決するために使います。

# get_categories.py
from wp_client import get_wp_session, api_url

def fetch_category_map() -> dict[str, int]:
    session = get_wp_session()
    category_map = {}
    page = 1

    while True:
        response = session.get(api_url("categories"), params={"per_page": 100, "page": page})
        response.raise_for_status()
        data = response.json()
        if not data:
            break
        for cat in data:
            category_map[cat["name"]] = cat["id"]
        total_pages = int(response.headers.get("X-WP-TotalPages", 1))
        if page >= total_pages:
            break
        page += 1

    return category_map

if __name__ == "__main__":
    cat_map = fetch_category_map()
    for name, cid in cat_map.items():
        print(f"{name}: {cid}")

⑥ CSV から記事を一括投稿する(エラーハンドリング・ログ出力付き)

CSV の各行を読み込んで記事を順次投稿します。投稿結果は post_log.csv に記録され、エラーが発生した行はスキップして処理を継続します。CSV のフォーマットは title,content,categories,status(categories はカテゴリ名をセミコロン区切り)です。

# bulk_post.py
import csv
import logging
import time
from datetime import datetime
from wp_client import get_wp_session, api_url
from get_categories import fetch_category_map

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("bulk_post.log", encoding="utf-8"),
        logging.StreamHandler(),
    ],
)

def bulk_post_from_csv(input_csv: str, output_log: str = "post_log.csv") -> None:
    cat_map = fetch_category_map()
    results = []

    with open(input_csv, encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for i, row in enumerate(reader, start=1):
            title = row.get("title", "").strip()
            content = row.get("content", "").strip()
            status = row.get("status", "draft").strip()
            category_names = [c.strip() for c in row.get("categories", "").split(";") if c.strip()]
            category_ids = [cat_map[name] for name in category_names if name in cat_map]

            try:
                session = get_wp_session()
                payload = {
                    "title": title,
                    "content": content,
                    "categories": category_ids,
                    "status": status,
                }
                response = session.post(api_url("posts"), json=payload)
                response.raise_for_status()
                data = response.json()
                post_id = data["id"]
                post_url = data["link"]
                logging.info(f"[{i}] 投稿成功 → ID: {post_id}, URL: {post_url}")
                results.append({
                    "row": i,
                    "title": title,
                    "status": "success",
                    "post_id": post_id,
                    "url": post_url,
                    "timestamp": datetime.now().isoformat(),
                    "error": "",
                })
            except Exception as e:
                logging.error(f"[{i}] 投稿失敗 → タイトル: {title}, エラー: {e}")
                results.append({
                    "row": i,
                    "title": title,
                    "status": "error",
                    "post_id": "",
                    "url": "",
                    "timestamp": datetime.now().isoformat(),
                    "error": str(e),
                })

            time.sleep(1)  # APIへの負荷軽減のため1秒待機

    with open(output_log, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=["row", "title", "status", "post_id", "url", "timestamp", "error"])
        writer.writeheader()
        writer.writerows(results)

    logging.info(f"処理完了。ログを {output_log} に保存しました。")

if __name__ == "__main__":
    bulk_post_from_csv("articles.csv")

アイキャッチ画像をアップロードして記事に設定する(応用)

記事のアイキャッチ画像(featured_media)もAPIで設定できます。まず画像をメディアライブラリにアップロードし、返ってきたメディアIDを記事投稿時のfeatured_mediaフィールドに指定します。

# upload_media.py
import os
from wp_client import get_wp_session, api_url

def upload_image(image_path: str, alt_text: str = "") -> int:
    session = get_wp_session()
    # 画像アップロード時はContent-Typeを変更する
    session.headers.update({"Content-Type": None})

    filename = os.path.basename(image_path)
    with open(image_path, "rb") as f:
        response = session.post(
            api_url("media"),
            headers={
                "Content-Disposition": f'attachment; filename="{filename}"',
                "Content-Type": "image/jpeg",
            },
            data=f,
        )
    response.raise_for_status()
    media_id = response.json()["id"]

    # alt テキストを更新
    if alt_text:
        session.headers.update({"Content-Type": "application/json"})
        session.post(api_url(f"media/{media_id}"), json={"alt_text": alt_text})

    return media_id

アップロードしたメディアIDは、create_post関数のpayload"featured_media": media_idを追加することで記事に紐付けできます。

実装の参考として、[Python]WordPressの自動投稿をしてみた | まさまゆブログも合わせてチェックしてみてください。

フェーズ4:動作確認・エラーハンドリング・実運用への応用

レスポンスの確認方法

APIリクエストが成功したかどうかは、HTTPステータスコードとJSONレスポンスの2段階で確認します。

response = session.post(api_url("posts"), json=payload)

# ステータスコードで大まかな成否を判定
print(f"ステータスコード: {response.status_code}")
# 200番台:成功 / 400番台:クライアントエラー / 500番台:サーバーエラー

# JSONレスポンスの内容を確認
data = response.json()
print(f"記事ID: {data.get('id')}")
print(f"記事URL: {data.get('link')}")
print(f"ステータス: {data.get('status')}")

# エラー時はcodeとmessageが返ってくる
if "code" in data:
    print(f"エラーコード: {data['code']}")
    print(f"エラーメッセージ: {data['message']}")

APIエラーが発生した場合、WordPressはJSONレスポンスの中にcodemessageフィールドを含めて返します。response.raise_for_status()はHTTPエラーを例外として発生させますが、messageフィールドも合わせて確認するとデバッグが格段に楽になります。

エラーハンドリングの実装

try-exceptを使って通信エラーとAPIエラーを適切に捕捉する実装パターンを示します。

import requests
import logging

def safe_create_post(title: str, content: str) -> dict | None:
    try:
        session = get_wp_session()
        payload = {"title": title, "content": content, "status": "draft"}
        response = session.post(api_url("posts"), json=payload)

        # HTTPエラー(4xx・5xx)を例外として発生させる
        response.raise_for_status()
        return response.json()

    except requests.exceptions.ConnectionError as e:
        # ネットワーク接続エラー
        logging.error(f"接続エラー: サイトURLを確認してください。詳細: {e}")
        return None

    except requests.exceptions.Timeout as e:
        # タイムアウトエラー
        logging.error(f"タイムアウト: リクエストがタイムアウトしました。詳細: {e}")
        return None

    except requests.exceptions.HTTPError as e:
        # HTTPエラー(認証エラー・権限エラーなど)
        error_data = e.response.json() if e.response else {}
        logging.error(
            f"HTTPエラー {e.response.status_code}: "
            f"{error_data.get('code', 'unknown')} - {error_data.get('message', str(e))}"
        )
        return None

    except Exception as e:
        # その他の予期しないエラー
        logging.error(f"予期しないエラー: {e}")
        return None

実運用への応用例

CSVやMarkdownファイルからの一括投稿は、フェーズ3のbulk_post.pyで実装済みです。Markdownファイルからの投稿には、markdownライブラリを使ってHTMLに変換してからcontentフィールドに渡す方法が一般的です。

import markdown

with open("article.md", encoding="utf-8") as f:
    md_content = f.read()

html_content = markdown.markdown(md_content, extensions=["extra", "nl2br"])
create_post(title="Markdownから自動投稿", content=html_content, category_ids=[1])

cronでの定期実行(Linuxサーバーの場合):毎朝9時に一括投稿スクリプトを実行する設定例です。

# crontab -e で編集
0 9 * * * cd /path/to/project && python bulk_post.py >> /var/log/wp

この記事はハナ編集部が執筆しました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

はじめまして、「白米元気」と申します。

ノースキルで副業をスタートし、2ヶ月で月10万円を達成。
その後も毎日ChatGPTとにらめっこしながら、
「どうやったら仕組みで稼げるのか?」を考え続けてきました。

そんな中出会ったのが「LLM無職」です。
AIと仕組みを作り、AIに仕事をさせる。
副業や働き方そのものを実験していく——そんな挑戦をしています。

このブログでは、わたしのLLM無職への道のりの途中で
AIを活用した具体的な方法や工夫、日々の実践内容を紹介。
ときどき家族の話もまじえながら、
読んでくれた方が「なんかおもしろそう!」と思えるような、
リアルで実験的な情報をお届けしていきます。

コメント

コメントする

目次