自分のWordPressで学ぶPHP講座(AIの変更を判定する)
区分3 差分を判断する★★WP-CLI(wp というコマンド)は、管理画面を開かずに、ターミナルからWordPressを操作する道具です。
その中の wp eval-file は、WordPressを読み込んだ状態で、指定したPHPファイルを1回だけ実行します。つまり、その場限りのPHPスクリプトを、WordPressの関数が全部使える状態で走らせる命令です。
functions.php が「アクセスのたびに動く常設の部品」だとすると、eval-file のスクリプトは「一回だけ行う作業」です。 フックには登録されず、走らせた瞬間だけ動きます。
固定ページや投稿の本文は、データベース(WordPressが記事を保存している場所)に入っています。 スクリプトはそれを読み出して、PHPで文字を置き換えて、書き戻します。管理画面から1ページずつ直す作業の、自動版です。
| 何のことか | |
|---|---|
| 下見(dry-run) | 書き換えず、「どの記事の何か所が変わるか」だけを表示する実行 |
| 本番(apply) | 実際に書き換える実行 |
| 控え | 書き換える前の本文を、別のファイルに保存した物 |
| リビジョン | WordPressが自動で残す、記事の過去の版。設定で数や有無が変わる |
スクリプトを読む時は、まず先頭の数行を見ます。ここで「下見か本番か」の仕組みが決まるからです。 例にするのは、お知らせの文言を置き換えるスクリプトです(実物から店舗や料金の文言だけを例の文字列に替えています)。
<?php
// usage: wp eval-file replace_notice.php [apply]
$apply = in_array('apply', $args ?? [], true);
走らせ方は2通りです。
wp eval-file replace_notice.php ← 下見(何も書き換えない)
wp eval-file replace_notice.php apply ← 本番(書き換える)
$args は、eval-file が渡す、コマンドの後ろに並べた言葉の一覧(配列)です。
in_array('apply', $args ?? [], true) は、「その中に apply があるか」を調べる式です。?? [] は、$args が無い時に空の配列で代用する書き方で、true は厳密に比べる指定です。結果の true/false が $apply に入ります。
このスクリプトの安全性は、$apply が true の時だけ書き込む作りになっているかで決まります。
後ろで見るように、書き込みの処理が if ( $apply ... ) の中だけに入っていれば、何も付けずに走らせた時は、画面に表示するだけで終わります。
対象の決め方は、この種のスクリプトで2通りに分かれます。
1つ目は、記事のIDを固定で並べる形です。
$ids = [11,12,13,14,15];
foreach ($ids as $id) {
$p = get_post($id);
...
}
get_post($id) は、そのIDの記事を取る関数です。IDは記事ごとにサイトが振った番号で、同じ番号でも、別のサイトでは別の記事を指します。
どのサイトで走らせるのか、そのサイトのIDなのかは、必ず確かめます。複数のサイトを運用していると、ここが取り違えの原因になります。
2つ目は、データベースに問い合わせて、条件に合う記事を探す形です。
global $wpdb;
$ids = $wpdb->get_col("SELECT ID FROM {$wpdb->posts}
WHERE post_status='publish' AND post_type IN ('post','page')
AND (post_content LIKE '%旧文言%A%' OR post_content LIKE '%旧文言:A%')");
読み方を順に説明します。
$wpdb はWordPressのデータベース担当。get_col は「問い合わせ結果の1列目を、配列で返す」命令{$wpdb->posts} は記事のテーブルの名前(サイトの設定で前に文字が付くので、直接書かずこう書く)SELECT ID FROM ... WHERE ... は「条件に合う記事のIDを選ぶ」という SQL(データベースへの問い合わせの言葉)post_status='publish' は公開中のもの、post_type IN ('post','page') は投稿か固定ページLIKE '%旧文言%A%' の % は「任意の文字が何文字でも入る」の意味。「旧文言」と「A」を含む記事、という条件になるこの問い合わせは、SQLの全文が固定の文字だけでできていて、外から来た値が混ざっていません。
もし外から来た値を混ぜる場合は、$wpdb->prepare() で包むのが決まりです(No.07の「入る側を整える」と同じ考え方)。SQLに、整えていない外の値が混ざっていないかを確かめます。
下見の表示を読む時は、取れたIDの数が、想像していた数と合っているかを見ます。多すぎれば条件が広すぎ、0なら条件が合っていません。
置換は、「元の文字 => 新しい文字」の表を作って、順に当てはめる形が多いです。
$rules = [
'営業時間は10時から18時です。定休日は月曜です。' => $NEW,
'営業時間は10時から18時です。' => $NEW,
];
foreach ($rules as $from => $to) {
$n = substr_count($c, $from);
if ($n) { $c = str_replace($from, $to, $c); }
}
str_replace($from, $to, $c):$c の中の $from を全部 $to に置き換えるsubstr_count($c, $from):$c の中に $from が何回あるかを数える。数えた数は、下見の表示に使うforeach ($rules as $from => $to):表を1行ずつ取り出して当てはめる(No.04)ここで見落としやすいのが、上から順に当てはめるという点です。 上の例は、2行目の文が、1行目の文の前半と同じです。もし順番が逆だと、先に短い方が置き換わって、後ろの「定休日は月曜です。」だけが取り残され、不自然な文章になります。 長い文を上に、短い文を下に。順番が、AIが書いた並びで合っているかを見ます。
もう1つの注意は、置換の連続です。 1本目のスクリプトの「新しい文言」が、2本目のスクリプトの「元の文言」になっていることがあります。
1本目:A => B
2本目:B => C
この場合、1本目を走らせずに2本目を走らせると、何も変わりません。逆に2本目を2回走らせても、2回目は変わりません(もう B が無いからです)。 どの順番で、何回走らせたかを、記録に残してください。
置き換えた文字を、記事に戻す部分です。
if ($apply && $c !== $orig) {
file_put_contents($backup_dir."/$id.html", $orig);
$r = wp_update_post(['ID'=>$id, 'post_content'=>wp_slash($c)], true);
echo is_wp_error($r) ? " ERROR ".$r->get_error_message()."\n" : " updated\n";
}
条件の $apply && $c !== $orig は、「本番で、かつ中身が実際に変わった時だけ」という意味です。
変わらなかった記事は、触りません。
順に読むと、次のとおりです。
file_put_contents(...):元の本文($orig)をファイルとして保存する。これが控え。書き換えよりも前に行っているwp_update_post(...):記事を更新する関数。第2引数を true にしておくと、失敗した時にエラーの物が返るwp_slash($c):本文の中のバックスラッシュなどを、そのまま保存するための下ごしらえ。wp_update_post は受け取った値から1段階バックスラッシュを取り除くので、先に付けておく決まりになっているis_wp_error($r):返ってきた物がエラーかを確かめて、ERROR か updated を表示する控えを書く行が、wp_update_post より前にあるか。 これが逆だと、書き換えたあとで、元の本文が失われます。
もう1つ、控えのファイル名を見ます。
$id.html のように、ID だけで名前が決まっていると、同じ記事に2回目の本番をした時に、1回目の控え(本当の元の本文)が、2回目の控え(1回目の結果)で上書きされます。ID に日付やスクリプト名を足してあれば、そのような事故は防げます。
控えの保存先も確認します。 公開フォルダの外(ホームの直下など)に保存されているか。本文に入っているものによっては、公開フォルダに置くと誰でも見られる恐れがあります。
本番を走らせたあとは、表示されたログ(updated の行)の数が、下見で出た記事数と同じかを数えます。 そのうえで、1〜2本の記事を、実際にブラウザで開いて、置き換わった文を目で見ます。
WordPressは、wp_update_post で記事を更新すると、設定が有効ならリビジョンも残します。控えのファイルに加えて、管理画面の記事の編集画面からも、前の版に戻せる場合があります。ただし、リビジョンの数や有無は設定で変わるので、控えのファイルを頼りにします。
○ AIに頼む時の言い方の例:「置換スクリプトは、引数なしで下見、apply で本番にして。控えは書き換えの前に、日付付きの名前で公開フォルダの外へ。下見の出力は、記事ID・記事名・置換した数にして」
○ 下見の結果で「NO CHANGE」と出る記事は、条件か置換表が合っていません。その記事を対象から外すか、表を直してから本番にします
○ よくある勘違い:「下見で問題が無かったから、本番も大丈夫」→ 下見は書き込みの処理を通らないので、控えの保存先や書き込みの失敗は、本番まで分かりません。最初は1本だけで本番を試すのも手です
○ 関係する単元:No.06(控えと戻し方)/No.07(SQLに外の値を混ぜない)/No.11(5つの問い)
本文の一括置換は、引数なしが下見・apply が本番か、控えが書き換えの前か、対象のIDがこのサイトのものか、の3点を見る。置換表は長い文を上に。
問1 AIが本文置換のスクリプトを出してきました。wp_update_post の呼び出しが、$apply の条件の外にあり、いつでも実行される書き方になっています。どう判定しますか?
問2 置換スクリプトの中で、file_put_contents による控えの保存が、wp_update_post の後ろの行にありました。何が問題ですか?
問3 置換表の1行目が「営業時間は10時から18時です。」で、2行目が「営業時間は10時から18時です。定休日は月曜です。」でした。何が起きますか?
問4 AIが出したSQLに post_content LIKE '%旧文言%' とあります。この % の意味として正しいのは?
出典:WP-CLI 公式「wp eval-file」(https://developer.wordpress.org/cli/commands/eval-file/)/WordPress公式 wp_update_post(https://developer.wordpress.org/reference/functions/wp_update_post/)/wp_slash/get_post/is_wp_error の各リファレンス/wpdb クラス(https://developer.wordpress.org/reference/classes/wpdb/)/PHP公式 str_replace(https://www.php.net/manual/ja/function.str-replace.php)/substr_count/in_array の各ページ/自分のサーバーのホームに置いた置換スクリプトとその控え。