1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【WordPress】XML加工でカスタム投稿を移行する

1
Posted at

はじめに

株式会社レコチョクで主にフロントエンド領域の開発をしている望月です。

今回、WordPressで構築されたコーポレートサイトのリニューアルに伴い、カスタム投稿の既存記事データの移行を行いました。

ちなみに、WordPressのデータ移行では、一般的に以下のような方法がよく用いられます。

  • プラグイン(All-in-One WP Migrationなど)を利用する方法
  • DBを直接コピーする方法
  • WordPressのエクスポート/インポート機能を利用する方法

今回は移行元・移行先ともにWordPressではあるものの、カスタム投稿のフィールド構造が一部変わっており、単純なエクスポート/インポートやプラグインでは対応できない状態でした。

そこで、WordPressのエクスポート機能で出力したXMLを移行先の構造に合わせて加工することでデータの移行を行いました。
本記事では、フィールド構造が異なるカスタム投稿の移行において実際に行った手順をまとめます。
同様にWordPressのリニューアルでデータ構造が変わってしまい、通常の移行方法では対応できない方の参考になれば幸いです。

状況

今回のデータ移行の具体的な状況は以下の通りです。

  • 移行対象の記事数が約1300件と大量にある
  • カスタム投稿のフィールド構造が変更されている
  • カスタムフィールドの作成にはACFを使用しており、フィールド構造変更に伴いフィールドキーも異なる
  • 記事内では画像やPDFなども掲載しており、メディアも含めて移行が必要

移行手順

大まかな流れは以下の通りです。

  1. WordPressのエクスポート機能で移行対象の記事、データ構造確認用の移行先テスト記事のXMLを出力
  2. 移行対象のXMLを移行先のデータ構造に合わせて加工
  3. 加工したXMLをインポート

1. 記事のXMLのエクスポート

移行元のWordPress管理画面から「ツール > エクスポート」を使用し、対象のカスタム投稿のデータをXML形式で出力します。
また、移行先のデータ構造把握のためテスト記事などを投稿し、移行元記事同様にデータをXML形式で出力します。

wordpress_xml_export.png

XML出力すると、カスタムフィールドのデータは以下のような構造になっています。

<!-- 記事ごとに<item>で囲ってある -->
<item>
  <title>記事タイトル</title>

<!-- カスタムフィールドは実際の値(article_detail)と管理用のフィールドキー(_article_detail)のセットになっている -->
  <wp:postmeta>
    <wp:meta_key>article_detail</wp:meta_key>
    <wp:meta_value><![CDATA[本文]]></wp:meta_value>
  </wp:postmeta>
  <wp:postmeta>
    <wp:meta_key>_article_detail</wp:meta_key>
    <wp:meta_value>field_xxxxx</wp:meta_value>
  </wp:postmeta>

</item>

ACFではカスタムフィールドの値に加え、対応するフィールドキー(field_xxxxx)もセットで管理されています。
このキーが一致していない場合、管理画面やフロントで正しく値が表示されないため、XML加工の際にはこのフィールドキーを移行先のものに合わせる必要があります。

2. 移行対象のXMLを、移行先のデータ構造に合わせて加工

移行元のXMLと移行先のXMLを照らし合わせ、それぞれ移行元のデータを移行先のデータ構造に合わせて変換するスクリプトを作成します。
今回スクリプトはPythonで作成し、XMLを扱うためのライブラリlxmlを使用しています。
実装イメージはこんな感じです。

# XMLを扱うためのライブラリ(lxml)のetree機能を読み込む
from lxml import etree as ET

# XMLファイルを読み込む
tree = ET.parse("input.xml")
root = tree.getroot()

# 移行先のXMLからACFフィールドキーを取得し、定義しておく
FIELD_NAME = "custom_field_name"
FIELD_KEY = "field_xxxxx"

# 各投稿(item)をループ
for item in root.findall("./channel/item"):

    # 投稿本文(post_content)を取得
    content = item.find("{http://purl.org/rss/1.0/modules/content/}encoded")

    # 本文がない場合はスキップ
    if content is None or not content.text:
        continue

    # ACFの値用postmetaを追加
    value_meta = ET.SubElement(item, "wp:postmeta")

    # フィールド名を設定
    value_key = ET.SubElement(value_meta, "wp:meta_key")
    value_key.text = ET.CDATA(FIELD_NAME)

    # フィールドの値として本文を設定
    value_data = ET.SubElement(value_meta, "wp:meta_value")
    value_data.text = ET.CDATA(content.text)

    # ACFのフィールドキー用postmetaを追加
    field_meta = ET.SubElement(item, "wp:postmeta")

    # 「_フィールド名」を設定
    field_key_name = ET.SubElement(field_meta, "wp:meta_key")
    field_key_name.text = ET.CDATA(f"_{FIELD_NAME}")

    # 移行先環境のフィールドキーを設定
    field_key_value = ET.SubElement(field_meta, "wp:meta_value")
    field_key_value.text = ET.CDATA(FIELD_KEY)

# 加工後のXMLを書き出し
tree.write("output.xml", encoding="utf-8", xml_declaration=True, pretty_print=True)

実際には移行データの構造に合わせて条件分岐や各種変換処理を追加して対応をしています。

3. 加工したXMLをインポート

加工したXMLを、移行先のWordPressでインポートします。
管理画面上「ツール > インポート」からWordPressインポーターを使用し、XMLをアップロードすることで、変換済みのデータをそのまま投入することができます。

はまったポイント

投稿IDの変更により画像が正しく表示されない

記事内ではWordPressにアップロードしている画像やPDFファイルを表示していたため、メディアのデータ移行も必要でした。

メディアのデータ移行は実体ファイルが格納されている/wp-content/uploads/配下の移行+XMLでの移行で可能です。しかしこの方法だと各メディアに割り振られている「投稿ID」が引き継がれず、新しいIDが再採番されます。

今回のケースでは記事内でメディアを使用する際にこの「投稿ID」によって該当のメディアを呼び出している箇所がありました。
そのため、移行に伴い「投稿ID」が変わってしまうと記事内で適切なメディアを呼び出すことができず、結果記事内で使用している画像が正しく表示されないという問題が発生しました。

そのため、移行元での投稿IDと移行先での投稿IDの対応表を作成し、
投稿記事のXMLを加工する際にこの対応表と照らし合わせながら、記事データの中で呼び出している投稿IDを差し替える処理を追加しました。

対応表は移行元と移行先のメディアのXMLからjson形式で出力するスクリプトを別途作成しました。
実装イメージはこんな感じです。

from lxml import etree as ET
import json
import os

# XMLファイルを読み込む
old_tree = ET.parse("old.xml")  # 移行元XML
new_tree = ET.parse("new.xml")  # 移行先XML

old_root = old_tree.getroot()
new_root = new_tree.getroot()

# URLやパスからファイル名だけを取得する
def get_filename(path):
    if not path:
        return ""
    return os.path.basename(path).lower()

# XMLから「attachment(メディア)」だけを抽出する
def extract_attachments(root):
    attachments = []

    for item in root.findall(".//item"):
        post_type = item.findtext("{http://wordpress.org/export/1.2/}post_type")

        # attachment(メディア)のみ対象
        if post_type != "attachment":
            continue

        post_id = item.findtext("{http://wordpress.org/export/1.2/}post_id")
        url = item.findtext("{http://wordpress.org/export/1.2/}attachment_url")

        attachments.append({
            "id": post_id,
            "filename": get_filename(url),
        })

    return attachments

# 移行元・先それぞれのメディア一覧を取得
old_attachments = extract_attachments(old_root)
new_attachments = extract_attachments(new_root)

# 移行先のメディアを検索しやすい形に変換
# (ファイル名 → 投稿ID)
new_map = {
    att["filename"]: att["id"]
    for att in new_attachments
}


# 移行元投稿ID → 移行先投稿IDの対応表を作成
id_map = {}

for old in old_attachments:
    filename = old["filename"]

    if filename in new_map:
        id_map[old["id"]] = new_map[filename]

# JSONとして出力
with open("attachment_map.json", "w", encoding="utf-8") as f:
    json.dump(id_map, f, ensure_ascii=False, indent=2)

これで出力されるJSONはこんな感じになります。

{
  "101": "205",
  "102": "206",
  "103": "210"
}

※キー(左側):移行元の投稿ID
※値(右側):移行先の投稿ID

まとめ

WordPressのデータ移行では、プラグインや標準機能で対応できるケースが多いですが、今回のようにフィールド構造が変更されている場合は、そのままの移行では対応できないことがあります。
今回はコストを抑えつつ、かつ独自のデータ構造に合わせて柔軟に対応するためにXMLを手元で加工する方法で移行を行いました。
フィールド構造が変わる場合のデータの移行を行う際の参考になれば幸いです。

一方で、XMLの構造理解やスクリプトの実装が必要になったり、移行元と移行先で大幅にデータ構造の違いがある場合は複雑化してしまいます。
状況に応じて、有料のプラグインを探すなどするのが良さそうです。

またリニューアルしたコーポレートサイトもよければ覗いてみてください!

この記事はRecoChoku Engineering Blogの記事を転載したものとなります 。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?