別CMSで作成された4万件分の大量投稿をwordpressに引越しする
技術スタック wordpressPHP

別CMSで作成された4万件分の大量投稿をwordpressに引越しする

2020.11.23

こんにちはjunです。会社でとてつもない量のCMSのデータをwordpressに移行する計画がありました。色々と課題がある中なんとかwordpressにデータをマイグレーションして再構築できたので共有したいと思います。

データが4万件もあるのでプログラム的にwordpressを操作する必要がありました。日本語で検索してもなかなかヒットしなかったので、少し苦労。 しかしCLIがわかって、使う関数を把握すれば意外と簡単です。

引越しの背景から説明するので、「ささっと移行手順を見せろや!」という人は「wordpress側の構築概要」から見てください。

背景&データ移行の課題

2006年に構築されたサイトを弊社が受け持っており、その環境が古くなってきたので移行することになりました。PHP5、centos6というレガシーな環境で動いており非常に危なっかしい上に、ろくに保守もされてないのでページングとか表示もおかしい部分も出てきました。

移行するサイトはwordpressではない 別のCMSで構築されており、移行の際にはDBからデータを一回ダンプして加工してwordpressにマイグレーションをする必要がありました。 しかしそのデータは

  • 投稿4万件
  • ユーザー8200人
  • カテゴリー90件

というデータが膨大であり必然的にプログラム的にデータを移行する必要があります。とりあえず担当の方と移行するデータを精査しました。元々はコミュニティサイトとして使用していたのでユーザーが非常に多く、移行すべきアクティブなユーザーは100人程度だったのでユーザーはかなり減りました。しかし投稿は全部移行でした(泣

旧CMSでの「カテゴリー」はブログの種類に当てはまりました。ブログを管理、投稿する部署が異なっていることが判明し、投稿データにも categoruid の様に区別するカラムがありました。さらに言えばユーザー情報にも紐づいています笑。

投稿データは旧CMSで結び付けられたユーザーID、カテゴリーIDの関係性を維持しながら移行する必要があります。 さらに投稿データには

[img]http://~~~~~[/img]

という様なそのCMS独自のタグが存在したので、wordpressに移行する前に正規表現で置換する必要がありました。(今回はその解説はしません。別途の記事で)

まとめると

  • データ数が膨大。
  • 記事はカテゴリー、ユーザーとの関係性を維持する。
  • 記事データの独自記法をwordpress用に置換または削除する。

という課題がありました。

wordpress側の構築概要

今回の移行手順としては前準備に

  1. 旧CMSからデータをダンプ(mysql)してローカルに入れておく。
  2. データ構成をよく観察する。
  3. 必要なデータをSQLを用いて取得、JSONで取得
  4. JSONを元にPHPスクリプトでデータを加工

この様にデータの加工をしてwordpressに入れ込む準備をしました。加工済みデータJSONとwordpressの関数を用いてこれらのデータをwordpressに移行しました。

移行の特に厄介だったのが旧CMSでは部署ごとにカテゴリーという名前でブログ種が分けられていたことです。wordpressのカテゴリーとは別の概念です。さらに管理ユーザーもそのカテゴリーで区別されていました。

そこで今回は wordpressをマルチサイト構成にして構築しました。 wordpressには一つのwordpressシステムを用いて複数の異なるブログを構築する機能があります。詳しくはこちらの公式を参照。マルチサイトにすることで複数のブログに分け、さらにそのブログごとにユーザーの割当が可能になります。

引越し手順

手順としては以下の通りです。

  1. 旧CMSからデータをダンプ(mysql)してローカルに入れておく。
  2. データ構成をよく観察する。
  3. 必要なデータをSQLを用いて取得、JSONで取得
  4. JSONを元にPHPスクリプトでデータを加工
  5. wordpressプロジェクト内に上記のデータを移行、挿入用PHPスクリプトを作成
  6. マルチサイト 構成をONにしてブログネットワークを機械的に作成
  7. ユーザーを作成して適切なブログネットワークに割り当てる。
  8. 投稿データを対応するユーザーとブログネットワークに割り当てる。
  9. 画像などを移行する。(今回はやりません)
  10. 全てのブログネットワークに共通のテーマを設定する。

dockerで検証環境を構築

まずはローカルでの検証環境を作りましょう。失敗するとDBが結構汚れるのですぐにリセットできるdockerを用います。wordpress公式のdockerHubの通りにすれば簡単に構築できます。ディレクトリ構成は以下の通りです。

docker-wordpress/
|
|-scrips/
|-docker-compose.yml

scripts/ にはwordpressに挿入するためのPHPスクリプトを入れておくためのディレクトリです。最終的にこのwordpress dockerコンテナの中に入って、このスクリプトをコマンドで実行します。

docker-compose.yml は以下の通りです。(ほとんど公式と同じ。一部改修

docker-compose.yml
version: '3.1'

services:

  wordpress:
    image: wordpress
    restart: always
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb
    volumes:
      - ./scripts:/var/www/html/scripts

  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    volumes:
      - db_wp:/var/lib/mysql

volumes:
  ./scripts:
  db_wp:

DBは永続化して、そしてスクリプトもローカルで編集してすぐに実行できる様にボリュームにマウントしておきます。これで準備完了です。

jun@MacBook-Pro docker-wordpress % docker-compose up -d

ブラウザを開いてlocalhost:8080にアクセスするとwordpressのインストール画面が開きます。DBの設定などは済んでいるので、初期ユーザーの設定だけで終わります。

マルチサイトを機械的に作成

マルチサイトの有効化

それではまず旧CMSのカテゴリーにあたる、マルチサイトを機械的に作成しましょう。その前にwp-config.phpでやることがあります。以下の様なコードを追記してマルチサイト化を有効にします。

define('WP_ALLOW_MULTISITE', true);

有効にすると「ツール」から「サイトネットワークの設置」というメニューが出現します。これをクリックしてサイトネットワークの設定を行います。そして新しくコードを追記しろと言われるので以下の様にwp-config.php.htaccessに追記します。

wp-config.php
define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', false);
define('DOMAIN_CURRENT_SITE', 'localhost');
define('PATH_CURRENT_SITE', '/');
define('SITE_ID_CURRENT_SITE', 1);
define('BLOG_ID_CURRENT_SITE', 1);
.htaccess
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]

# add a trailing slash to /wp-admin
RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]

RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
RewriteRule . index.php [L]

なお、今回はドメイン別ではなくサブディレクトリ形式のマルチサイト とします。

サイト作成プログラム

移行元のデータはすでにJSONにしてあります。以下の様な構成とします。

article_category.json
[
  ...
  {
    "id":"6",
    "name":"サイトの名前",
    "sub_title":"サイトのキャッチフレーズ的なもの",
    "mailUser":"3::1543"
  },
  ...
]

旧CMSからはこの様になっており、mailUserがこのカテゴリー(ブログ)を管理するユーザーです。wordpressのマルチサイトを作るには、初期管理ユーザーとサイト名があればとりあえず作れます。

そして追加スクリプトは以下の通りです。

/scripts/create_site.php
<?php
require_once('../../wp-load.php');

$json = file_get_contents('/var/www/html/scripts/category/article_categpry.json');
$cats = json_decode($json,true);
$new_id = 2;
foreach($cats as $key=>&$val){
    wpmu_create_blog('localhost','blog'.$key,$val['name'],1,array('blogdescription'=>$val['sub_title']));
    
    if( is_wp_error( $return ) ) {
        print_r($return->get_error_message()."\n");
        continue;
    }
    
    $val['new_id']=$new_id;
    $new_id++;
}
file_put_contents("/var/www/html/scripts/category/registered_article_categpry.json", json_encode($cats,JSON_UNESCAPED_UNICODE));

wpmu_create_blog という関数を用いて作成します。wpmu_create_blog を使用するためには上記のコード二行目にある require_once('../../wp-load.php'); が必要になります。 wp-load.php を使用することでwordpress関数が使用できる様になります。引数は以下の様に取ります。

wpmu_create_blog(
  ブログのドメイン(必須),
  ブログのサブディレクトリ名(必須),
  サイト名(必須),
  管理ユーザーID(必須),
  そのほかの情報(配列),
)

上記スクリプトは非常に単純です。JSONにある旧CMSに登録されたブログカテゴリー分だけforeachで回して関数を実行しているだけです。

しかしブログカテゴリーは後でユーザーと投稿を挿入する際に必要となるので、 「wordpressでのブログIDと以前のブログカテゴリーIDを対応させる」様にしておきます。下記の様に工夫をしておきます。

$new_id = 2; // 新しいブログIDの最初の値
foreach($cats as $key=>&$val){ //参照渡しをしておく
    wpmu_create_blog('localhost','blog'.$key,$val['name'],1,array('blogdescription'=>$val['sub_title']));
    
    if( is_wp_error( $return ) ) {
        print_r($return->get_error_message()."\n");
        continue;
    }
    // エラーが起きなかったら new_id という新しいカラムと共にwordpressのブログIDを記録
    $val['new_id']=$new_id;
    $new_id++;
}

// wordpressと旧CMSとの関係性を記録したJSONを出力
file_put_contents("/var/www/html/scripts/category/registered_article_categpry.json", json_encode($cats,JSON_UNESCAPED_UNICODE));

すると新しく作成されたJSONを見ると

registered_article_category.json
[
...
  {
    "id":"6",
    "name":"サイトの名前",
    "sub_title":"サイトのキャッチフレーズ的なもの",
    "mailUser":"3::1543",
    "new_id":3
  }
...
]

new_idというカラムとwordpressでのブログIDが入りました。こうすることで 「旧CMSでのブログカテゴリーID 6のものはwordpressではブログID 3」という関係性を保存できます。

以上でブログカテゴリーの移行は終了しました。訳70サイトもあるのでこんな感じ↓になります笑。

スクリプトの実行方法

これらスクリプトはコマンドで実行します。dockerで管理しているので

docker exec -it {wordpressのコンテナ名} /bin/bash

この様にしてwordpressを立ち上げているコンテナに入って、コマンドを実行しにいきます。

root@0756d76ddde1:/var/www/html# 

コンテナに入るとこの様にターミナルが変化します。リモートサーバーにsshでログインしたみたいな感じです。そしてdockerを立ち上げるときに scripts/ ディレクトリをボリュームしているのでそこに移動します。

root@0756d76ddde1:/var/www/html# cd scripts
root@0756d76ddde1:/var/www/html/scripts# ls
create_site.php
article_category.json

root@0756d76ddde1:/var/www/html/scripts# php create_site.php

上記の様にphpファイルを指定することでスクリプトが実行されます。

ユーザーを機械的に作成、割り当て

ユーザーの追加

ではそれぞれのブログを作成したので次はユーザーを作成していきます。ユーザーは以下の様なJSONです。

user.json
[
    ...
    {
        "uid":"3",
        "loginname":"webmaster",
        "name":"お名前",
        "email":"example@example.com",
        "user_avatar":
        "thumbnail.jpg",
    }
    ...
]

groupid は旧CMSの権限グループです。そしてユーザー追加スクリプトは以下の様になります。

<?php
require_once('../../wp-load.php');

$json = file_get_contents('/var/www/html/scripts/user/user.json');
$uesrs = json_decode($json,true);

$new_id = 2;
foreach($uesrs as &$val){

    $role;
    switch($val['groupid']){
        case "1":
            $role = 'administrator';
        break;
       
        case "2":
            $role = 'contributor';
        break;

        case "3":
            $role = 'contributor';
        break;

        case "4":
            $role = 'administrator';
        break;

        case "5":
            $role = 'contributor';
        break;

        case "6":
            $role = 'administrator';
        break;

        case "7":
            $role = 'administrator';
        break;
    }

    $user = [
        'user_pass'=>'PASS_WORD',
        'user_login'=>$val['loginname'],
        'user_email'=>$val['email'],
        'display_name'=>$val['uid'],
        'role'=>$role,
    ];
    $return = wp_insert_user($user);
    
    if( is_wp_error( $return ) ) {
        print_r($return->get_error_message().':'.$val['loginname']."\n");
        continue;
    }
    $val['new_id']=$new_id;
    $new_id++;
}
file_put_contents("/var/www/html/scripts/user/registered_user.json", json_encode($uesrs,JSON_UNESCAPED_UNICODE));

ちょっと自分のためのコードがありますが、重要なのは wp_insert_user という関数です。引数は連想配列で入れます。以下のキー名で配列にします。

$user = [
        'user_pass'=>'PASS_WORD',       // パスワード名
        'user_login'=>$val['loginname'],// ログイン名(英数字でないといけない)
        'user_email'=>$val['email'],    // 登録アドレス
        'display_name'=>$val['uid'],    // 表示名(プロフ名)
        'role'=>$role,                  // 権限キー
    ];

権限キーは文字列で指定します。私のコードでは旧CMSのIDを対応させています。そしてブログの時の様に旧データと新データのIDを対応させる様にします。

...
   $return = wp_insert_user($user);

  if( is_wp_error( $return ) ) {
        print_r($return->get_error_message().':'.$val['loginname']."\n");
        continue;
    }
    $val['new_id']=$new_id;
    $new_id++;
}
file_put_contents("/var/www/html/scripts/user/registered_user.json", json_encode($uesrs,JSON_UNESCAPED_UNICODE));

ちなみにスクリプトを実行するときは is_wp_error( $return ) でエラーをキャッチできる様にしましょう。 なぜか私のデータには同じユーザーのデータがあったりなどで、「ユーザーがすでに登録されています」というエラーでIDがずれてしまうという事件がありました。そのためにキャッチできるスクリプトを入れておきましょう。

ユーザーをブログに当てはめ

ユーザーのスクリプトを実行するとユーザーが作成され、新旧のIDを対応させたユーザーJSONファイルができました。これとブログカテゴリーのデータを用いて各ブログを管理するユーザーを割り当てていきます。

add_user_blog.php
<?php
require_once('../../wp-load.php');

// wordpress user IDが入ったuserのファイル
$user_json = file_get_contents('/var/www/html/scripts/user/registered_user.json');
$uesrs = json_decode($user_json,true);

// wordpress blog IDが入ったブログカテゴリーのファイル
$cat_json = file_get_contents('/var/www/html/scripts/category/registered_article_categpry.json');
$cats = json_decode($cat_json,true);

foreach($cats as $cat_key=> $cat_val){
    $old_user_ids =explode('::',$cat_val['mailUser']);
    
    foreach($old_user_ids as $old_id){
        $user = array_values(array_filter($uesrs,function($ele) use($old_id) {
            return $ele['uid'] == $old_id && isset($ele['new_id']);
        }));

        if(!empty($user)){
            $new_userid = $user[0]['new_id'];
            $new_cat_id =intval($cat_val['new_id']);
            add_user_to_blog($new_cat_id,$new_userid,'administrator');
        }
    }
}

私のデータの場合、ユーザーが複数人いたのでforeachの中でさらにforeachしています。マルチサイト の特定のブログに対してユーザーを割り当てるためには add_user_to_blog を用います。

第一引数にユーザーID、第二引数に対象のブログID、第三には権限グループを指定することで簡単にブログに対してユーザーを割り当てられます。

投稿を機械的に流し込み

最後に投稿を流し込みます。私が使用したデータは以下の様なデータになっています。

article.json
[
...
    {
     "id":"162",
     "date":"2007-08-02",
     "category_id":"6",
     "uid":"1543",
     "title":"タイトル",
     "content":"ここにブログの内容がプレーンテキスト形式で入っています。"
    },
...
]

uidを元にユーザー(著者)と結び付け、category_idを元にどのブログに投稿するのかを指定します。以下の様なスクリプトを書きました。

insert_post.php
<?php
ini_set('memory_limit', '1024M');
require_once('../../wp-load.php');

// wordpress user IDが入ったuserのファイル
$user_json = file_get_contents('/var/www/html/scripts/user/registered_user.json');
$uesrs = json_decode($user_json,true);

// 旧CMSの投稿データ
$article_json = file_get_contents('/var/www/html/scripts/articles/article_replace.json');
$articles = json_decode($article_json,true);

// wordpress blog IDが入ったブログカテゴリーのファイル
$cat_json = file_get_contents('/var/www/html/scripts/category/registered_article_categpry.json');
$cats = json_decode($cat_json,true);

foreach($articles as $key => $a_val){
    $old_cat_id = $a_val['category_id'];

    if(empty($old_cat_id)) continue;

    $cat = array_values(array_filter($cats,function($ele) use($old_cat_id) {
        return $ele['id'] == $old_cat_id && isset($ele['new_id']);
    }));

    if(empty($cat)) continue;

    $new_cat_id = $cat[0]['new_id'];

    $old_user_id = $a_val['uid'];
    $user = array_values(array_filter($uesrs,function($ele) use($old_user_id) {
        return $ele['uid'] == $old_user_id && isset($ele['new_id']);
    }));

    $new_user_id = (empty($user))?1:$user[0]['new_id'];
    if(!get_user_by('id',intval($new_user_id))) continue;

    switch_to_blog($new_cat_id);
    $new_post = array(
        'post_title' => $a_val['title'],
        'post_content' => $a_val['content'],
        'post_status' => 'publish',
        'post_date' => date($a_val['date']),
        'post_author' => $new_user_id,
        'post_type' =>'post',
    );
    wp_insert_post($new_post);
    restore_current_blog();
}

4万件分のデータとなると非常にメモリを食うので ini_set でメモリ上限を上げてあります。

投稿データからブログカテゴリーIDとユーザーIDを新しいwordpresの方と紐づけ、 wp_insert_post を用いて記事を挿入します。 wp_insert_post  の気をつける点はデフォルトではblogid=1のブログに記事を作成するということです。

そのためコードに switch_to_blog(); を追加して挿入対象のブログを切り替えています。この関数を使用するときはセットで restore_current_blog(); を使います。

挿入先のブログを切り替えて、wp_insert_post を用いて投稿を挿入します。wp_insert_postは引数に連想配列を入れます。 対応するキー名が決まっているので間違えない様にしましょう。

そして同じ様にターミナルでこのPHPを実行すればwordpressに投稿データが入ります。ちなみに4万件は15分かかりました。投稿したデータはエディタで普通に編集できますが、クラシックモードでの編集となります。画像などもきちんとタグとパスが生きていればきちんとレンダーされます。

ミスってしまったら..

大量のデータを入れたのにミスってしまったらdockerをリセットしましょう。コンテナーを削除してDBのボリュームも削除します。

docker volume rm VOLUME_NAME

そしてまた docker-compose up -d を行うことで最初からやり直しができます。

意外と簡単でした

以上が旧CMSからwordpressにデータを挿入する方法です。wordpressは wp-load.php を読み込めばほとんどの関数を使用でき、ターミナルからも実行できます。 スクリプト自体も100行未満で思いつける簡単なものです。

もしプログラム的にwordpressを操作したい場合は日本語だと上手く出てこないので「wordpress how to ~~~ programmatically」と調べるといいです。私が調べたものですと以下の感じです。

  • wordpress how to create post programmatically
  • wordpress how to create user programmatically
  • wordpress how to set user role programmatically

ぶっちゃけ旧CMSからデータを引っ張ってきたり、適切に加工したり、構造を把握する方が大変でした。機会があればこのデータ移行の時に一番大変だった、正規表現により独自タグの置換も記事にしたいと思います。

Copyright © 2021 jun. All rights reserved.