毎回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からGET・POST・PUT・DELETEなどの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を発行します。
- WordPress管理画面にログインします。
- 左メニューの「ユーザー」→「プロフィール」をクリックします。
- ページを下にスクロールすると「アプリケーションパスワード」セクションが表示されます。
- 「新しいアプリケーションパスワード名」の入力欄に任意の名前(例:「Python自動投稿」)を入力します。
- 「新しいアプリケーションパスワードを追加」ボタンをクリックします。
- スペース区切りの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レスポンスの中にcodeとmessageフィールドを含めて返します。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

コメント