投稿・固定ページのカスタムフィールドを、確認用CSVへ出力します。ID順に250記事ずつ取得し、同じキーに複数の値がある場合も、JSON配列として1セルに保持します。
CSVの構造と対象
列はpost_id・post_title・post_status・meta_key・values_jsonです。1記事1行ではなく1記事の1キーが1行です。先頭がアンダースコアの内部キーは除外します。ACFのフィールド定義・添付画像ファイル・タクソノミーは含まないため、完全バックアップや復元用ファイルにはなりません。
functions.phpへ追加する
子テーマのfunctions.phpをバックアップしてPHP領域へ追加します。先頭の<?phpは重ねません。PHPスニペット管理プラグインを使う場合はそちらだけに登録し、二重登録を避けてください。
<?php
function waza_cf_csv_cell($value) {
$value = (string) $value;
return preg_match('/^[\x00-\x20]*[=+@-]/', $value) ? "'" . $value : $value;
}
function waza_cf_csv_rows($post) {
$rows = [];
if (!$post instanceof WP_Post) return $rows;
foreach (get_post_meta($post->ID) as $key => $unused) {
$key = (string) $key;
if ($key === '' || $key[0] === '_') continue;
$json = wp_json_encode(get_post_meta($post->ID, $key, false), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) continue;
$rows[] = array_map('waza_cf_csv_cell', [$post->ID, $post->post_title, $post->post_status, $key, $json]);
}
return $rows;
}
function waza_cf_csv_batch($type, $after, $max = 0) {
global $wpdb;
if (!in_array($type, ['post','page'], true) || $after < 0 || $max < 0) return new WP_Error('invalid', '対象またはIDが不正です');
if (!$max) $max = (int) $wpdb->get_var($wpdb->prepare("SELECT MAX(ID) FROM {$wpdb->posts} WHERE post_type=%s", $type));
$ids = $wpdb->get_col($wpdb->prepare("SELECT ID FROM {$wpdb->posts} WHERE post_type=%s AND post_status IN ('publish','draft','pending','private') AND ID>%d AND ID<=%d ORDER BY ID ASC LIMIT 250", $type, $after, $max));
if ($wpdb->last_error) return new WP_Error('query', '投稿IDを取得できませんでした');
return ['ids'=>array_map('intval', $ids), 'max'=>$max, 'next'=>$ids ? (int) end($ids) : $after];
}
add_action('admin_menu', function () {
add_management_page('CF CSV出力', 'CF CSV出力', 'manage_options', 'waza-cf-csv', function () {
if (!current_user_can('manage_options')) return;
echo '<div class="wrap"><h1>CF CSV出力</h1><p>初回は両方のIDを0にします。続きはファイル名のnextとmaxを指定します。</p>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="waza_cf_csv">';
wp_nonce_field('waza_cf_csv');
echo '<p><label>対象 <select name="post_type"><option value="post">投稿</option><option value="page">固定ページ</option></select></label></p>';
echo '<p><label>前回のnext ID <input type="number" min="0" name="after" value="0" required></label></p>';
echo '<p><label>初回のmax ID <input type="number" min="0" name="max" value="0" required></label></p>';
submit_button('CSVをダウンロード');
echo '</form></div>';
});
});
add_action('admin_post_waza_cf_csv', function () {
if (!current_user_can('manage_options')) wp_die('権限がありません');
check_admin_referer('waza_cf_csv');
$type = isset($_POST['post_type']) && is_string($_POST['post_type']) ? wp_unslash($_POST['post_type']) : '';
$parse = function ($key) { return isset($_POST[$key]) && is_string($_POST[$key]) ? filter_var($_POST[$key], FILTER_VALIDATE_INT, ['options'=>['min_range'=>0]]) : false; };
$after = $parse('after'); $max = $parse('max');
if ($after === false || $max === false || ($after > 0 && $max === 0)) wp_die('続きの出力ではnextとmaxの両方を指定してください');
$batch = waza_cf_csv_batch($type, $after, $max);
if (is_wp_error($batch)) wp_die(esc_html($batch->get_error_message()));
if (!$batch['ids']) wp_die('この範囲に残りの対象記事はありません');
if (headers_sent()) wp_die('出力が始まっているためCSVを作成できません');
nocache_headers();
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="custom-fields-' . $type . '-after-' . $after . '-next-' . $batch['next'] . '-max-' . $batch['max'] . '.csv"');
$out = fopen('php://output', 'w');
if (!$out) wp_die('CSVを出力できません');
fwrite($out, "\xEF\xBB\xBF");
fputcsv($out, ['post_id','post_title','post_status','meta_key','values_json'], ',', '"', '');
foreach ($batch['ids'] as $id) {
if (!current_user_can('edit_post', $id)) continue;
foreach (waza_cf_csv_rows(get_post($id)) as $row) fputcsv($out, $row, ',', '"', '');
}
fclose($out);
exit;
});
ダウンロードと確認
管理画面の「ツール → CF CSV出力」で対象を選び、初回はnext IDとmax IDの両方を0にしてダウンロードします。
続きは直前のファイル名にあるnextと、初回のmaxを入力します。
たとえばcustom-fields-post-after-0-next-123-max-999.csvなら、次は123と999です。
CSVの行数ではなくファイル名のIDを使うため、内部キーしかない記事にも対応します。
次の取得範囲はnextより大きく、初回のmax以下のIDです。
取得済みの記事が途中で削除・対象外へ変わっても、ページ番号方式のように次の記事を飛ばしません。
初回より後に追加された記事はmaxの外へ置き、次の一巡で出力します。
ただし本文・値・状態を同一時点に固定する機能ではないため、完全な照合が必要なら出力中の更新を止めます。
終了表示は「この範囲に残りの対象記事はありません」です。
下書き・非公開記事の値も含まれ得るため、保存場所を限定してください。
日本語、カンマ、改行、複数値、配列、0を含むデータで確認します。値はJSONとして保存し、表計算ソフトで式になりやすいタイトルやキーの先頭にはアポストロフィを付けます。元値と完全一致する機械連携用CSVではなく、確認用の出力です。
1行を読み取る例と更新用CSVへの対応
| 列 | 例 | 意味 |
|---|---|---|
| post_id | 123(検証環境の実在IDに置換) | 元記事のID。別サイトの更新先IDではない |
| meta_key | card-title | 値の保存先の名前 |
| values_json | ["日本語, \"引用\"\n二行目"] | 1要素のJSON文字列配列。復号すると改行を含む文字列になる |
更新用データを作るときは、meta_keyで必要な行を選び、JSONを復号します。要素が1個で、その要素が文字列である場合だけ単一テキストの候補にします。複数要素や入れ子の配列を1つの文字列へ丸めると、保存構造を失います。
SCFのcard-titleへ読み戻す例では、復号した文字列をscf_card-title列にし、更新先の実在ID・投稿種別・公開状態を別途確認します。具体的な列と更新前後の比較はCSVでカスタムフィールドを更新する手順へ進んでください。
検証用投稿の出力から値を取り出し、別の検証用投稿へインポートした結果、日本語・引用符・カンマ・改行を保持しました。同名メタの複数値と入れ子の配列は変換対象から除外しました。今回はブラウザーでの投稿・固定ページのファイル取得と保存値との照合も確認しました。全件復元の確認ではありません。
ブラウザーで取得したCSVを保存値と照合する
| 確認対象 | 実際の結果 |
|---|---|
| 投稿と固定ページを選択してEnterで取得 | 別々のCSVファイルをブラウザーから保存 |
| 列と文字コード | 5列とUTF-8のBOMを確認 |
| 引用符・カンマ・改行を含む値 | values_jsonの復号後に保存メタと一致 |
| 内部キー | ACFのアンダースコア付き参照キーを除外 |
| 対象のない次ページ | 「このページに対象記事はありません」と表示 |
検証用の出力フォームから、投稿と固定ページをそれぞれ選んでCSVを取得しました。投稿の出力は9行、固定ページは1行でしたが、対象記事はそれぞれ4件と1件です。同じ投稿の複数キーが別行になるため、CSV行数を記事件数として扱わないでください。
復号すると引用符や改行が元の保存値に戻ることを確認しました。フォーム経由で保存した改行はCRLFで、CSVもその保存値を保持しています。CSVを更新データへ変換するときは、改行の表現と列の対応を明示して照合してください。
今回の試験は隔離環境の検証用投稿だけに対象を限定しています。全件出力の処理時間、途中で記事が追加された場合、複数回取得したファイルの一時点としての整合性は、この少件数の結果からは判断できません。

途中の削除と追加を含めたID分割の確認
| 条件 | 結果 |
|---|---|
| 503件から最初の250件を取得 | ID昇順で取得 |
| 取得済みの1件を対象外へ変更 | next IDを基準に、次の250件を取得 |
| 初回のmaxを固定して新規記事を追加 | 続きは既存の最後3件。新規記事は今回の範囲外 |
2026年10月3日、隔離したWordPress 7.1.2で、所有する検証投稿503件を使ってSQL取得結果を照合しました。
新しい入力欄とnonceの出力もPHPで確認しました。
HTTPでCSVを取得し、UTF-8のBOM・Content-Type・nextとmaxを含むファイル名まで照合しました。
CSV行のない記事でもファイル名のnextで続けられます。
実管理画面での操作は未確認です。
取得したCSVと対象記事を照合する
導入先では「ツール → CF CSV出力」を開きます。画面のURLはWordPress設置先のwp-admin/tools.php?page=waza-cf-csvです。
入力欄はnext IDとmax IDです。続きはCSVファイル名のnextを使い、初回のmaxを維持します。
取得したCSVごとにpost_idの重複をまとめ、記事ID数と並び順を照合してください。同じ投稿の複数キーが別行になるため、CSV行数は記事件数とは一致しません。
掲載したID方式の管理画面からのダウンロード、PC・スマホ表示、キーボード操作は未確認です。
JSONの外側の配列とフィールド内の配列を区別する
| 保存構造 | values_jsonの例 | 更新用CSVでの扱い |
|---|---|---|
| 同じキーに2行 | [“0″,””] | 単一テキストへまとめない |
| 配列を1行に保存 | [{“a”:{“b”:”C:\\work\\file”},”bool”:false,”integer”:0}] | 内側の型を保持して扱う |
| 文字列を1行に保存 | [“文字列”] | 定義を確認して1要素を取り出す |
values_jsonの一番外側の配列は、同じメタキーで保存された行をまとめたものです。
フィールドの値そのものが配列なら、その内側にも配列やオブジェクトが残ります。
CSVの行数やJSONの要素数を、そのまま更新する記事数として数えないでください。
単一テキストへ変換するのは、外側が1要素で、その要素が文字列のときだけです。
0は有効な文字列で、空文字列も更新する値になります。
外側が2要素以上、または内側が配列・オブジェクトの行は、この単一テキスト更新から外し、SCFからACFへの移行手順で保存構造を確認します。
隔離したWordPress 7.1.2で、同じキーに別々に保存した文字列0と空文字列、配列内の文字列パス・真偽値・整数を出力しました。
JSONの外側と内側の構造を保持し、CSVへ書き出して読み直したセルも一致しました。
取り込み先のフィールド定義を自動で変換する検証ではありません。
インポートとの違いと解除
この縦持ちCSVを一般のインポートプラグインへそのまま渡さないでください。読み戻すにはvalues_jsonの復号と、使用するプラグインの列仕様への変換が必要です。SCF/ACFの繰り返し構造の完全復元は保証しません。
関数を重複登録しないでください。
解除時は今回の三つの関数と二つのアクション登録をまとめて外します。
この機能は投稿メタを書き換えません。



