ウェブ制作

サイト制作のちょっとしたティップスまとめ

当サイトのリンクには広告が含まれています。

CSVをXML(WXR)に変換して、WordPress純正インポートツールで記事を一括登録した話

CSVをWordPressのXML(WXR)形式に変換し、WordPress純正のインポートツールを使って記事を一括登録した話の備忘録。プラグインや、自前でCSVインポーターを自作するのではなく、インポート処理そのものは純正ツールを使用する点がポイント。アイデアとして参考になれば幸いです。

ウェブ制作

公開日:2026/02/25

CSVをWordPressのXML(WXR)形式に変換し、WordPress純正のインポートツールを使って記事を一括登録した話の備忘録です。

プラグインや、自前でCSVインポーターを実装する方法とは違った、「純正ツールを活用する」シンプルな1つのアイデアとして参考になれば幸いです。

事の経緯

キッカケは「WordPressにCSVから記事を一括で登録したい」という依頼。

調べればいくらでもプラグインやティップスがあるだろうと思いきや、これといった定番の方法が意外と見当たらない。プラグインを探しても、極端に古かったり、カスタム投稿やカスタムフィールドの対応が有料だったりする。かつ現在も保守されているものは海外製のものばかり。しかも結構高い。。

ならば自前でCSVインポーターを作るしかないですが、調べながらのトライ&エラーになりそう。しかし今回はとにかく急ぎの要求だったため、そんな時間もありませんでした。これは困った。

あぁー、WordPress純正の『インポートツール』が、CSV形式に対応してくれていればなぁ~

とグチグチ嘆いていたのですが、いやちょっと待てよ、と。

CSVの中身をXML(WXR)に当てはめて作れば良いんじゃね?

CSVファイルは、一定のルールに基づいて整形されたデータ

であれば、投稿タイプ・タクソノミー・カスタムフィールドなど、インポート先の“さえちゃんと定義したXML(WXR)を用意すれば、あとは純正インポートツールがよしなに登録してくれるのでは?

これならインポート処理そのものを自前で実装する必要はありません。

そこで一旦WordPressから記事データをエクスポートし、XML(WXR)の形式を解析。CSVのデータがitemのどの箇所に当てはまるのか、1つ1つ値を書き換えてインポート……という検証を重ねたところ、今回のケースでは要件がシンプルだったこともあり、最終的には問題なく全データをインポートする事ができました!

ただ、インポートしたいCSVのデータが300件近くもあり、手動で全件分itemデータを書いて用意するなんて絶対に嫌だったので、CSVからXML(WXR)に変換するコンバーターを、取り急ぎPHPで試作しました。その際、実際に行った作業を共有します。

ただし、本コードの内容は あくまで基礎的な「たたき台」と考えてください。
CSVデータは「整形済みの信頼できるデータ」であることを前提としており、バリデーションや異常系処理は最低限になっています。

CSVの用意・整形

まずCSVのサンプルがこちら。

説明の便宜上、ここでは物件系のCSVデータと仮定します。以下のような形式で用意しました。

import_data.csv(例)

titlecategoryareacontentcf_attentionpost_date
物件Amansioncity-center,chiyoda都心での生活が便利な物件です!2025/3/15 15:02
物件B1storysuburban-center,shinjuku広々とした空間で快適な暮らしが可能です。専用フォームよりお問い合わせください。2025/2/11 10:00
物件C2storiesother-area,taito静かな環境で落ち着いた暮らしができる物件です。2024/12/31 17:05
import_data.csv

CSVの作成は、Excelではなく専用ツールの方がおすすめです。僕はSmoothCSVで作りました。

文字コードは UTF-8(BOMなし) に設定。

CSVの仕様

  • 1行目:カラム定義(WordPress側の対応項目)
  • 2行目以降:実際に登録されるデータ

カラムの説明

1行目:カラム定義(WordPress側の対応項目)

  • title : 記事タイトル
  • category : カテゴリー
  • area : タクソノミー
  • content : 記事本文
  • cf_attention : カスタムフィールド(※ACFの利用を想定)
  • post_date : 公開日(YYYY-MM-DD HH:MM)

2行目以降:実際に登録されるデータの例

  • 物件B : 記事タイトル
  • 1story : カテゴリー「category」のターム ※カンマ区切りで複数指定可
  • suburban-center,shinjuku : タクソノミー「area」のターム ※カンマ区切りで複数指定可
  • 広々とした空間で…以下略 : 記事本文
  • 専用フォームより…以下略 : カスタムフィールド「cf_attention」の本文
  • 2025/2/11 10:00 : 公開日

ちなみに記事本文ですが、今回は単なるテキストデータなので、インポート後はクラシックブロックとして登録されます。Gutenbergのブロック構造をそのままCSVに書いた場合は、インポート後も最初からブロックとして認識されます。

変換用ファイル:convert.php

次に、CSVをXML(WXR)形式へ変換するために、用意したPHPスクリプトがこちら。

PHPのバージョンは8.3で確認しました。

XML(WXR)の形式は、WordPress6.3.1で確認しました。

<?php

// CSV ファイルパス
$csv_file = __DIR__ . '/import_data.csv';

// 投稿タイプ
$post_type = 'post';

// 登録ユーザー名:ユーザー名を指定。指定しない場合、インポート実行時のユーザーになる
$post_author = '';

// インポートを許可するカスタムフィールドのキーを定義(CSVのキーと一致させる)
// ※カンマ区切りで複数登録可
$allowed_custom_fields = [
    'cf_attention',
];


/* -------------------------------------------------
    共通関数
---------------------------------------------------- */

// タイトルからスラッグ生成
function sanitize_title_simple($title) {
    $slug = mb_strtolower($title, 'UTF-8');
    $slug = preg_replace('/[^\p{L}\p{N}]+/u', '-', $slug);
    return trim($slug, '-');
}

// XMLエスケープ
function esc_xml($str) {
    return htmlspecialchars($str, ENT_XML1 | ENT_QUOTES, 'UTF-8');
}

// パースエラー防止
function cdata_safe($str) {
    return str_replace(']]>', ']]]]><![CDATA[>', $str);
}

// 日付フォーマット変換(ローカル + GMT)
function convert_date_for_wxr($date) {
    $timestamp = strtotime($date);
    if (!$timestamp) {
        return ['', ''];
    }

    $local = date('Y-m-d H:i:00', $timestamp);
    $gmt   = gmdate('Y-m-d H:i:00', $timestamp);

    return [$local, $gmt];
}


/* -------------------------------------------------
    CSV 読み込み
---------------------------------------------------- */

if (!file_exists($csv_file)) {
    exit('CSVファイルが見つかりません');
}

$csv = [];
if (($handle = fopen($csv_file, 'r')) !== false) {
    while (($row = fgetcsv($handle)) !== false) {
        $csv[] = $row;
    }
    fclose($handle);
}
$headers = array_shift($csv); // 1行目をヘッダーとして取得

/* -------------------------------------------------
    XML ヘッダー(WXR)
---------------------------------------------------- */
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0"
  xmlns:excerpt="http://wordpress.org/export/1.2/excerpt/"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:wfw="http://wellformedweb.org/CommentAPI/"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:wp="http://wordpress.org/export/1.2/"
>
<channel>
  <title>Post Import</title>
  <language>ja</language>
  <wp:wxr_version>1.2</wp:wxr_version>

XML;

/* -------------------------------------------------
    投稿データ生成
---------------------------------------------------- */
foreach ($csv as $row) {

    // CSV 行を連想配列に変換
    $data = array_combine($headers, $row);
    if ($data === false) {
        continue;
    }

    // 基本設定
    $title        = trim($data['title']); // タイトル
    $content      = $data['content']; // コンテンツ(本文)

    list($post_date_local, $post_date_gmt) =
        convert_date_for_wxr(trim($data['post_date'])); // 公開日

    // category(複数対応) カテゴリー ※カンマ区切りから配列作成
    $categories = [];
    if (!empty($data['category'])) {
        foreach (explode(',', $data['category']) as $cat) {
            $cat = trim($cat);
            if ($cat !== '') {
                $categories[] = $cat;
            }
        }
    }

    // area(複数対応) タクソノミー ※カンマ区切りから配列作成
    $area_slugs = [];
    if (!empty($data['area'])) {
        foreach (explode(',', $data['area']) as $slug) {
            $slug = trim($slug);
            if ($slug !== '') {
                $area_slugs[] = $slug;
            }
        }
    }

    /* -----------------------------
        <item> 出力
    -------------------------------- */
    $xml .= "  <item>\n";
    $xml .= '    <title>' . esc_xml($title) . "</title>\n";
    $xml .= '    <dc:creator><![CDATA[' . cdata_safe($post_author) . "]]></dc:creator>\n";
    $xml .= '    <wp:post_type>' . esc_xml($post_type) . "</wp:post_type>\n";
    $xml .= "    <wp:status>publish</wp:status>\n";
    $xml .= '    <wp:post_name>' . esc_xml(sanitize_title_simple($title)) . "</wp:post_name>\n";

    // 投稿日(必須)
    if ($post_date_local) {
        $xml .= '    <wp:post_date>' . esc_xml($post_date_local) . "</wp:post_date>\n";
        $xml .= '    <wp:post_date_gmt>' . esc_xml($post_date_gmt) . "</wp:post_date_gmt>\n";
    }

    // 本文 ※Gutenberg形式以外はクラシックブロックとして登録される
    $xml .= '    <content:encoded><![CDATA[' . cdata_safe($content) . ']]></content:encoded>' . "\n";


    /* -----------------------------
        タクソノミー
    -------------------------------- */
    // category(複数)
    foreach ($categories as $cat) {
        $xml .= '    <category domain="category" nicename="' . esc_xml($cat) . '"><![CDATA[' . cdata_safe($cat) . "]]></category>\n";
    }

    // area(複数)
    foreach ($area_slugs as $slug) {
        $xml .= '    <category domain="area" nicename="' . esc_xml($slug) . '"><![CDATA[' . cdata_safe($slug) . "]]></category>\n";
    }


    /* -----------------------------
        カスタムフィールド
    -------------------------------- */
    foreach ($allowed_custom_fields as $meta_key) {
        // CSVデータにそのキーが存在し、かつ値が空でないことを確認
        if (isset($data[$meta_key]) && $data[$meta_key] !== '') {
            $meta_value = trim($data[$meta_key]);
            $xml .= "    <wp:postmeta>\n";
            $xml .= '      <wp:meta_key><![CDATA[' . cdata_safe($meta_key) . "]]></wp:meta_key>\n";
            $xml .= '      <wp:meta_value><![CDATA[' . cdata_safe($meta_value) . "]]></wp:meta_value>\n";
            $xml .= "    </wp:postmeta>\n";
        }
    }

    $xml .= "  </item>\n\n";
}


/* -------------------------------------------------
    XML フッター & ファイル出力
---------------------------------------------------- */
$xml .= "</channel>\n</rss>";

$output_file = __DIR__ . '/wordpress_import.xml'; // 出力ファイルパス

header('Content-Type: text/html; charset=UTF-8');

// ファイルに書き込み
if (file_put_contents($output_file, $xml)) {
    // 成功メッセージをブラウザに表示
    echo "XMLファイルが正常に出力されました。<br>";
    echo "ファイル名: " . htmlspecialchars($output_file, ENT_QUOTES, 'UTF-8');
} else {
    // 失敗メッセージをブラウザに表示
    http_response_code(500); // サーバーエラー
    echo "エラー: XMLファイルの出力に失敗しました。ディレクトリの書き込み権限を確認してください。";
}

/*

// ※万が一保存できない場合はpreタグで出力してxmlファイルに直接コピペ
$xml .= "</channel>\n</rss>";
header('Content-Type: text/html; charset=UTF-8');
echo '<pre style="white-space: pre-wrap;">' . htmlspecialchars($xml, ENT_QUOTES, 'UTF-8') . '</pre>';

*/

今回作成したコードのうち、主な設定項目は以下の通りです。

CSVファイルパス

$csv_file = DIR . '/import_data.csv';

読み込むCSVファイルを指定します。

投稿タイプ

$post_type = 'post';

登録先の投稿タイプを指定します。
カスタム投稿タイプの場合は、ここを書き換えます。

登録ユーザー名

$post_author = '';

記事の投稿者となる WordPressのユーザー名(user_login) を指定します。
空の場合は、インポート実行時のユーザーが投稿者になります。

インポートを許可するカスタムフィールド

$allowed_custom_fields = [
'cf_attention',
];

CSVからインポートを許可するカスタムフィールドのキーを定義します。
CSVのキーと一致させることで、

  • タイプミスによる不要なカスタムフィールド生成
  • 意図しないメタデータの混入

を簡易的に防止しています。

XML出力ファイル

$output_file = DIR . '/wordpress_import.xml';

変換後のXMLファイル名と保存場所を指定します。デフォルトは同じ場所を指定。

convert.phpにブラウザでアクセス

作成したconvert.phpを、ローカル環境やステージングなどPHPが実行できる非公開環境にアップします。convert.phpにブラウザからアクセスすると、CSVを元にXMLファイルが生成されます

ブラウザでconvert.phpにアクセス

xmlファイルが生成される

<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0"
  xmlns:excerpt="http://wordpress.org/export/1.2/excerpt/"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:wfw="http://wellformedweb.org/CommentAPI/"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:wp="http://wordpress.org/export/1.2/"
>
<channel>
  <title>Post Import</title>
  <language>ja</language>
  <wp:wxr_version>1.2</wp:wxr_version>
  <item>
    <title>物件B</title>
    <dc:creator><![CDATA[]]></dc:creator>
    <wp:post_type>post</wp:post_type>
    <wp:status>publish</wp:status>
    <wp:post_name>物件b</wp:post_name>
    <wp:post_date>2025-02-11 10:00:00</wp:post_date>
    <wp:post_date_gmt>2025-02-11 01:00:00</wp:post_date_gmt>
    <content:encoded><![CDATA[広々とした空間で快適な暮らしが可能です。]]></content:encoded>
    <category domain="category" nicename="1story"><![CDATA[1story]]></category>
    <category domain="area" nicename="suburban-center"><![CDATA[suburban-center]]></category>
    <category domain="area" nicename="shinjuku"><![CDATA[shinjuku]]></category>
    <wp:postmeta>
      <wp:meta_key><![CDATA[cf_attention]]></wp:meta_key>
      <wp:meta_value><![CDATA[専用フォームよりお問い合わせください。]]></wp:meta_value>
    </wp:postmeta>
  </item>

</channel>
</rss>

生成されたXMLの例(抜粋)

XMLファイルが問題なく生成されたことを確認したら、生成されたファイルを保存します。

なお、今回のように300件程度であればまず問題ありませんが、メモリに全XMLを溜めるので、件数や本文のサイズが多すぎると、エラーになる可能性があります。その場合は、CSV を分割するなどして小分けに処理してください。WordPressにインポートする時も同じです。

また、convert.php は用が済み次第、とっととサーバーから削除します。ツールを置きっぱなしにしてはいけない

番外編:もし書き込みに失敗する場合はコピペ作戦に切り替え

header('Content-Type: text/html; charset=UTF-8');
echo '<pre style="white-space: pre-wrap;">' . htmlspecialchars($xml, ENT_QUOTES, 'UTF-8') . '</pre>';

もしサーバーの書き込み権限の問題でXMLファイルを生成できない場合は、コード末尾に用意しているXML フッター & ファイル出力以降の処理をコメントアウトの部分と入れ替えてください。preタグでXMLを出力する処理に切り替わります。

出力されたXMLを、コードエディターにコピー&ペースト

HTML上に出力されたXMLを、コードエディターにコピー&ペーストしてUTF-8形式で保存してください。

ブラウザのツールバーから「名前を付けて保存」はNGです。エスケープされた文字列を、そのままXMLとして保存してしまうのでダメです。

WordPress側の準備

XMLファイルを用意したら、WordPress側の準備をします。

今回のCSV例の場合、必要な確認項目は以下になります。

  • 指定した投稿タイプが存在すること
  • 指定したタクソノミーが存在すること
  • タームが事前に作成されていること
  • 指定したカスタムフィールドが存在すること
  • post(投稿)
  • category(カテゴリー)、area(エリア)
  • suburban-center(副都心エリア)、shinjuku(新宿区)……etc
  • cf_attention(注意事項)

特にスペルミスに注意です。

今回のCSVを例にあげると、カテゴリーの他に、
タクソノミー「area」とタームも事前に作成して準備しておく

カスタムフィールド「cf_attention」も事前に設置

インポートの実行

必要な確認項目を全てクリアしたら、実際にインポート作業に入ります。

WordPress管理画面から、

「ツール」>「インポーターの実行」>「WordPress のインポート」

を選択し、「ファイルを選択」>「ファイルをアップロードしてインポート」から、XMLファイルを指定します。

いきなり全件インポートするのではなく、まずは最小限の数行に絞った簡易版のCSVを用意し、それを使ってテストすることを強く推奨します。

なお、投稿者を指定していない場合、インポート時に、「作成者をインポートできませんでした」というメッセージが表示されますが、最終的にインポート実行時のユーザーが投稿者になるので、動作上は問題ありません。

存在しないユーザーが勝手に作成される事故を防ぐ意味では、むしろこのメッセージが出た方が安全とも言える……かも(多すぎて鬱陶しいけど)。

最後に「実行」ボタンをクリックします。

インポート完了

無事にインポートが完了すると、投稿が作成され、タクソノミー(ターム)が紐づいています。カスタムフィールドにも値が入っていることが確認できます。

まとめ

以上、CSVをXML(WXR)形式に変換し、WordPress純正のインポートツールを使って記事を一括登録する方法でした。

XMLに変換するという一手間はありますが、インポート処理そのものを自前で実装する必要は無く、純正ツールでインポートできるという点は大きなメリットだと思います。

ただし実際の運用では、サイトの仕様に応じてインポート項目の拡張・調整が必要です。

本記事の内容はあくまで「たたき台」 として、参考になれば幸いです。使用・改造については自己責任でお願いします🙇‍♂️

上に戻る