はじめに
前回は、Codex CLIを使ってWordPress整理用のローカル環境を作りました。
準備したものは、主に以下です。
- Codex CLI
- Python仮想環境
.env- WordPressアプリケーションパスワード
AGENTS.md-
scripts/とdata/ディレクトリ
今回は、その続きです。
WordPress REST APIを使って、既存投稿を取得し、CSVとJSONにエクスポートします。
やりたいことはシンプルです。
WordPress管理画面で1記事ずつ見るのではなく、記事100本をCSVとして一覧化したい。
これができると、次のような作業が一気にやりやすくなります。
- カテゴリー分類
- タグ整理
- slugの見直し
- 記事タイトルの棚卸し
- 公開済み記事の一覧確認
- 重複記事や似た記事の確認
WordPressの記事が、管理画面上の「投稿一覧」から、手元で扱える「データ」になります。
今回作るもの
今回作るファイルは、主に2つです。
scripts/
wp_client.py
export_posts.py
役割は以下です。
wp_client.py
WordPress REST APIを叩くための共通クライアント
export_posts.py
WordPressの投稿を取得してCSV/JSONに出力するスクリプト
出力先は以下です。
data/
posts_export.csv
posts_export.json
前提
作業ディレクトリは以下です。
cd ~/wp-codex-tools
source .venv/bin/activate
.env には、以下のような情報が入っています。
WP_BASE_URL=https://www.example.com
WP_USERNAME=your_wordpress_username
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
実際には、自分のWordPressサイトのURL、ユーザー名、アプリケーションパスワードに置き換えます。
今回使うWordPress REST APIのベースURLは、以下の形になります。
https://www.example.com/wp-json/wp/v2
まずCodexに指示する
Codexを起動します。
codex
そして、以下のように指示しました。
このリポジトリで、WordPress REST APIを使って既存投稿を取得するPythonツールを作ってください。
前提:
- 認証情報は .env にあります。
- .env には WP_BASE_URL, WP_USERNAME, WP_APP_PASSWORD があります。
- WordPress REST API Base URL は WP_BASE_URL + /wp-json/wp/v2 です。
まず作るファイル:
- scripts/wp_client.py
- scripts/export_posts.py
実装内容:
1. scripts/wp_client.py
- .env を読み込む
- WP_BASE_URL, WP_USERNAME, WP_APP_PASSWORD を取得する
- Basic認証でWordPress REST APIを叩く
- 共通の get/post 関数を用意する
- 認証情報をログに出さない
2. scripts/export_posts.py
- /wp/v2/posts から投稿を取得する
- ページネーションに対応する
- --limit N で取得件数を制限できる
- --all で全件取得できる
- data/posts_export.csv と data/posts_export.json に保存する
取得項目:
- post_id
- title
- slug
- date
- status
- link
- excerpt_text
- content_text の先頭2000文字
- categories のID配列
- tags のID配列
注意:
- WordPressへの更新はまだ実装しない
- 削除系APIは実装しない
- .env の中身は絶対に表示しない
- WP_APP_PASSWORDをログに出さない
- APIエラー時はHTTPステータスとエラー概要だけ表示する
ポイントは、最初から「更新」はさせないことです。
今回は取得だけです。
AIエージェントに本番環境を触らせる場合、まずは読み取り専用の処理から始めるのが安全だと思っています。
wp_client.pyの役割
wp_client.py は、WordPress REST APIを叩くための共通部品です。
イメージとしては、以下のような役割です。
import os
import requests
from dotenv import load_dotenv
load_dotenv()
WP_BASE_URL = os.environ["WP_BASE_URL"].rstrip("/")
WP_USERNAME = os.environ["WP_USERNAME"]
WP_APP_PASSWORD = os.environ["WP_APP_PASSWORD"]
API_BASE = f"{WP_BASE_URL}/wp-json/wp/v2"
session = requests.Session()
session.auth = (WP_USERNAME, WP_APP_PASSWORD)
def get(path, params=None):
url = f"{API_BASE}{path}"
response = session.get(url, params=params, timeout=30)
response.raise_for_status()
return response
def post(path, json=None):
url = f"{API_BASE}{path}"
response = session.post(url, json=json, timeout=30)
response.raise_for_status()
return response
実際には、Codexにエラーハンドリングも含めて実装してもらいました。
ここで重要なのは、.env の値をログに出さないことです。
特に WP_APP_PASSWORD は絶対に表示しないようにします。
export_posts.pyで投稿を取得する
次に、export_posts.py で投稿一覧を取得します。
WordPress REST APIでは、投稿一覧は以下のエンドポイントから取得できます。
/wp-json/wp/v2/posts
ただし、投稿数が多い場合、一度に全部は取れません。
そのため、ページネーション対応が必要です。
イメージとしては、以下のようなパラメータを使います。
per_page=100
page=1
1ページ目、2ページ目、3ページ目……と順番に取得していき、記事がなくなるまで繰り返します。
Codexには、このページネーション処理も含めて実装してもらいました。
まず3件だけ取得する
いきなり全件取得する前に、まず3件だけ取得します。
python scripts/export_posts.py --limit 3
成功すると、以下のような出力になります。
Connected to WordPress.
Fetched 3 posts.
Saved: data/posts_export.csv
Saved: data/posts_export.json
この時点では、WordPress側には何も変更を加えていません。
取得して、手元に保存しているだけです。
出力ファイルを確認する
ファイルができたか確認します。
ls -lh data/posts_export.csv data/posts_export.json
CSVの先頭を見ます。
head -n 5 data/posts_export.csv
または、Pythonで中身を軽く確認します。
python - <<'PY'
import pandas as pd
df = pd.read_csv("data/posts_export.csv")
print(df.head())
print(f"件数: {len(df)}")
print(df.columns.tolist())
PY
想定している列は以下です。
post_id
title
slug
date
status
link
excerpt_text
content_text
categories
tags
これで、WordPressの記事がCSVとして手元に出てきました。
全件取得する
3件取得できたら、全件取得します。
python scripts/export_posts.py --all
100記事程度であれば、それほど時間はかかりません。
取得後に件数を確認します。
python - <<'PY'
import pandas as pd
df = pd.read_csv("data/posts_export.csv")
print(f"取得件数: {len(df)}")
print(df[["post_id", "title", "slug", "status"]].head(10))
PY
記事数が想定と合っていればOKです。
JSONも出力する理由
今回はCSVだけでなく、JSONも出力しました。
data/posts_export.csv
data/posts_export.json
CSVは、人間が見たり、Excelやスプレッドシートで確認したりするのに向いています。
一方、JSONは、後続のスクリプトで扱いやすいです。
たとえば、後でカテゴリー分類やslug整理をするときに、JSONの方が扱いやすい場面があります。
今回は両方出しておくことにしました。
HTML本文をテキスト化する
WordPress REST APIで取得した本文は、HTMLを含んでいます。
そのままだと、カテゴリー分類やタグ推定には少し扱いづらいです。
そこで、beautifulsoup4 を使ってHTMLからテキストを取り出します。
イメージは以下です。
from bs4 import BeautifulSoup
def html_to_text(html):
soup = BeautifulSoup(html or "", "html.parser")
return soup.get_text("\n", strip=True)
今回のCSVでは、本文すべてではなく、先頭2000文字程度を content_text として保存することにしました。
理由は、カテゴリー分類の材料としては、記事本文の冒頭がかなり重要だからです。
また、全本文をCSVに入れると、ファイルが重くなったり見づらくなったりします。
取得する項目
今回取得した項目は以下です。
post_id
WordPress上の投稿IDです。
後でカテゴリーやタグを反映するときに使います。
title
記事タイトルです。
分類やslug作成では、タイトルをかなり重視します。
slug
現在のslugです。
後で長すぎるslugや日本語slugを見直すために使います。
date
投稿日です。
記事の時系列を確認するために使います。
status
公開状態です。
publish、draft などが入ります。
今回は基本的に公開済み記事を対象にしていますが、状態を確認できるようにしておきます。
link
現在のURLです。
slug変更時に、旧URLと新URLを対応させるためにも使えます。
excerpt_text
抜粋です。
カテゴリー分類の材料になります。
content_text
本文テキストの先頭部分です。
タイトルだけでは判断できない記事を分類するために使います。
categories
現在ついているカテゴリーIDです。
既存分類を参考にするために保存します。
tags
現在ついているタグIDです。
既存タグも参考情報として保存します。
実際にCSV化して感じたこと
WordPress管理画面で記事を見るのと、CSVで記事一覧を見るのでは、かなり感覚が違いました。
管理画面では、どうしても1記事ずつ見ることになります。
でもCSVにすると、記事群全体を一覧できます。
たとえば、以下のようなことが見えてきます。
- slugがやたら長い記事
- 同じようなテーマの記事
- カテゴリーが未整理の記事
- タイトルの粒度がそろっていない記事
- 旅行記なのかノウハウなのか曖昧な記事
WordPressの記事が、急に「分析対象のデータ」になった感じがありました。
これはかなり大きいです。
管理画面で100記事をポチポチするのは大変ですが、CSVなら一気に俯瞰できます。
よくありそうなエラー
401 Unauthorized
認証に失敗しています。
確認することは以下です。
- WP_USERNAME が正しいか
- WP_APP_PASSWORD が正しいか
- アプリケーションパスワードを使っているか
- 通常のログインパスワードを入れていないか
WordPressのユーザー名がメールアドレスではない場合もあります。
その場合は、WordPress管理画面で実際のユーザー名を確認します。
403 Forbidden
ユーザー権限が足りない可能性があります。
投稿を取得するだけなら問題ないことも多いですが、下書きや非公開記事まで扱う場合は権限が必要です。
404 Not Found
WP_BASE_URL が間違っている可能性があります。
以下のURLにブラウザでアクセスして、REST APIが見えるか確認します。
https://www.example.com/wp-json/wp/v2/posts?per_page=1
公開投稿がJSONで返ってくれば、REST API自体は動いています。
CSVが文字化けする
日本語を扱う場合、CSVの文字コードに注意が必要です。
Excelで開くことも考えて、CSVは utf-8-sig で出力するようにしました。
df.to_csv(output_path, index=False, encoding="utf-8-sig")
この段階ではまだ更新しない
今回の記事でやったのは、あくまで取得だけです。
WordPressへの更新はしていません。
これは意図的です。
いきなりカテゴリーやタグを反映するのではなく、まず記事一覧を手元に持ってきます。
その上で、
- CSVを眺める
- 分類案を作る
- 人間が確認する
- dry-runする
- 少数だけ反映する
- 問題なければ全件反映する
という順番にします。
本番環境に対してAIエージェントを使うなら、この段階分けはかなり重要だと思います。
今回できたこと
今回できたことは以下です。
- WordPress REST APIで投稿一覧を取得した
- ページネーションに対応した
- 3件だけ取得して疎通確認した
- 全件取得してCSV/JSONに出力した
- タイトル、slug、本文、カテゴリー、タグを一覧化した
- WordPressの記事100本を「データ」として扱えるようにした
ここまでできると、次の作業が一気にやりやすくなります。
次回
次回は、この posts_export.csv を使って、記事100本のカテゴリー分類案を作ります。
やることは以下です。
- カテゴリーを7つに整理する
-
classify_posts.pyを作る - 記事ごとに
main_categoryを決める - タグ候補を3〜8個出す
-
confidenceを付ける -
categorized_posts.csvを出力する - 人間は
confidence=lowを中心に確認する
WordPressの記事がCSVになったことで、次は「100記事をどういう本棚に並べるか」を考えられるようになります。