自分のWordPressで学ぶPHP講座(AIの変更を判定する)

区分3 差分を判断する★★

No.08 本文を一括置換するスクリプトの判定

固定ページや投稿の本文を、何十本もまとめて書き換えるスクリプトを、AIが出してくることがあります。出る頻度は少ないものの、間違えると何本もの本文が一度に壊れるので、判定の見る所を決めておく単元です。区分3の最後で、さらっと全体を通します。
目次
  1. 第0章 前提知識
  2. 第1章 スクリプトの骨組み
  3. 第2章 置換の中身
  4. 確認問題

第0章 前提知識

① WP-CLI と eval-file :WordPressをコマンドで操作する道具と、PHPファイルを走らせる命令

WP-CLI(wp というコマンド)は、管理画面を開かずに、ターミナルからWordPressを操作する道具です。 その中の wp eval-file は、WordPressを読み込んだ状態で、指定したPHPファイルを1回だけ実行します。つまり、その場限りのPHPスクリプトを、WordPressの関数が全部使える状態で走らせる命令です。

functions.php が「アクセスのたびに動く常設の部品」だとすると、eval-file のスクリプトは「一回だけ行う作業」です。 フックには登録されず、走らせた瞬間だけ動きます。

② 本文の置換 :投稿の中身の文字を、別の文字に置き換えること

固定ページや投稿の本文は、データベース(WordPressが記事を保存している場所)に入っています。 スクリプトはそれを読み出して、PHPで文字を置き換えて、書き戻します。管理画面から1ページずつ直す作業の、自動版です。

③ 似た言葉との違い

何のことか
下見(dry-run) 書き換えず、「どの記事の何か所が変わるか」だけを表示する実行
本番(apply) 実際に書き換える実行
控え 書き換える前の本文を、別のファイルに保存した物
リビジョン WordPressが自動で残す、記事の過去の版。設定で数や有無が変わる

第1章 スクリプトの骨組み

① 下見と本番を引数で分ける 必ず覚える

スクリプトを読む時は、まず先頭の数行を見ます。ここで「下見か本番か」の仕組みが決まるからです。 例にするのは、お知らせの文言を置き換えるスクリプトです(実物から店舗や料金の文言だけを例の文字列に替えています)。

<?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%')");

読み方を順に説明します。

この問い合わせは、SQLの全文が固定の文字だけでできていて、外から来た値が混ざっていません。 もし外から来た値を混ぜる場合は、$wpdb->prepare() で包むのが決まりです(No.07の「入る側を整える」と同じ考え方)。SQLに、整えていない外の値が混ざっていないかを確かめます。

下見の表示を読む時は、取れたIDの数が、想像していた数と合っているかを見ます。多すぎれば条件が広すぎ、0なら条件が合っていません。

第2章 置換の中身

① 置き換えの表と、その順番 必ず覚える

置換は、「元の文字 => 新しい文字」の表を作って、順に当てはめる形が多いです。

$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); }
}

ここで見落としやすいのが、上から順に当てはめるという点です。 上の例は、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 は、「本番で、かつ中身が実際に変わった時だけ」という意味です。 変わらなかった記事は、触りません。

順に読むと、次のとおりです。

  1. file_put_contents(...):元の本文($orig)をファイルとして保存する。これが控え。書き換えよりも前に行っている
  2. wp_update_post(...):記事を更新する関数。第2引数を true にしておくと、失敗した時にエラーの物が返る
  3. wp_slash($c):本文の中のバックスラッシュなどを、そのまま保存するための下ごしらえ。wp_update_post は受け取った値から1段階バックスラッシュを取り除くので、先に付けておく決まりになっている
  4. 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 の各ページ/自分のサーバーのホームに置いた置換スクリプトとその控え。