WordPressにカスタム投稿記事をできるだけ高速にインポートするという案件があったので、ハマりポイントを記す。
”高速に”と銘打ったがこれが最速であるとは思っていない。こうした方が速いよってのがあればぜひ教えてもらいたいところだ。
目次
仕様の策定
前提として、管理画面側ではなくWordPressの固定ページ内、つまりフロント側にインポートボタンを設置してあげる必要があった。よってWordPressのcsvインポートプラグインは使えない。
まず、csvから記事を投稿するという機能を噛み砕くと下記の機能に分けることができる。
- csvをアップロードする機能
- アップロードしたcsvから記事内容が格納された配列を生成する機能
- 配列から記事を投稿する機能
csvをアップロードし、そこから配列を生成するっていうのはありがちな話なので割愛する。工夫するとすれば、アップロードしたファイルは記事のインポートにのみ用いるものなのだから、インポート完了後、もしくは定期的に削除する処理をどっかに追加するべきってことくらいだろうか。
問題は記事投稿機能となってくるのだが、WordPressに管理画面外から記事を投稿する方法は下記のパターンが考えられる。
- wp_insert_post関数を使う
- WP-API経由で投稿する
- WP-CLIから投稿する
- 自前でSQL文を書く
結論としては、同サイト内から記事を投稿するだけであればwp_insert_postを使うのが無難だろう。当初この関数の存在に気付かず自前でSQL文を書いていたのだが、WordPress程熟成されたCMSに備わっている関数を超えるほどのパフォーマンスを出すことは難しいだろう。
最も高速に動作するのはおそらくWP-CLI経由。ただしWP-CLIが使える環境が必要、つまりインフラに依存してくるので実装する場合は要注意。
当然DBに対して直にcsvを流し込んだ方が高速なのだがそれはphpMyAdminとかを使える場合のお話。
完全に外部から投稿するのであればWP-API経由になるだろう。自前のアプリケーションから投稿する場合などはこの仕様を選択することになる。ただしあんまり高速で動作する気はしないしサイトに負荷をかけてしまうので要注意。
wp_insert_postの使い方
csvから配列を作成できたら、その配列を回して各行を記事として投稿していけばよい。
使い方自体は簡単なので公式リファレンスに譲るが、留意点としてはカスタムフィールドの投稿方法だ。
wp_insert_postは、記事の投稿に成功すると記事のidを返却してくれる。そのidに対して、add_post_metaを用いてカスタムフィールドを投稿するという手順だ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
foreach ($csv as $item) { $post_id = wp_insert_post(array( 'post_type' => 'custom', 'post_title' => $item[0], 'post_content' => $item[1], 'post_status' => 'publish', 'tax_input' => array( 'tax_name' => array( $item[2], $item[3] ) ), 'comment_status' => 'closed', 'ping_status' => 'closed', )); if ($post_id) { add_post_meta($post_id, 'custom_field', $item[4]); } } |
のような感じ。
注意点
文字コード
定番でハマる。エクセルのバージョンが2016以降であればUTF-8で保存することが可能だが、それ以前だと他フリーソフトを用いるなどの一手間が必要になるはず。
UTF-8で吐き出したcsvを用意することができればそれに超したことはないが、そうでない場合の対処法としては
- 読み込んだcsvファイル自体に対して文字コードの変換をかける
- csvを読み込み配列に格納したあとで、その配列に対してfor文またはforeach文内にて文字コードの変換処理を行う
の二通りが考えられる。計測してみたところ、前者の方がメモリ使用量は増えるものの処理速度は上がることが観測された。配列一つ一つに対して文字コードの変換を行う処理を挟むということを考えれば当然の結果だろう。しかし、だからと言って前者が必ずしも優れているとは断言し難い。csvファイルが十分に大きかった場合、メモリ使用量の上限に引っかかるケースが想定される。これはcsvファイルの容量に左右される(項目数がめちゃくちゃ多いなど)ため実際に試してみてどの仕様が相応しいか試してみた方がよい。
前者の読み込んだcsvをまるっと文字コード変換するのはこんな感じ。
1 2 |
$data = file_get_contents($file_path); $data = mb_convert_encoding($data, 'UTF-8', 'sjis-win');//文字化け対策 |
邪魔な文字達をなんとかする
BOM
エクセルで出力したcsvにはBOMが付いている。これを無視しないと一行空行が入ってしまう。BOMにより挿入される文字を無視する処理を挟む必要がある。
ハイフンっぽい文字の表記揺れと戦う
ハイフンと打って変換してみると分かるが、ハイフンっぽい見た目をした文字は結構ある。下記リンクを参考とし、お客様から送られてきたデータに含まれていた謎のハイフンっぽいなにかをプラスして全部まとめて半角ハイフンに丸めた。
https://qiita.com/ryounagaoka/items/4cf5191d1a2763667add
空行を無視する
最後に謎の空行が入ってしまうことがあるので、空行は無視するようにする。
上記3点に対処するとこんな感じになった。
1 2 3 4 5 6 7 8 9 10 |
$csv = []; //csvから配列に foreach ($file as $line) { $line = preg_replace('/^\xEF\xBB\xBF/', '', $line); //BOM削除 $line = preg_replace ('/[\x{30FC}\x{2010}-\x{2015}\x{2212}\x{FF70}\x{FF0D}]/u', '-', $line);//ハイフンっぽいもの置換 //空以外であれば配列に格納 if ($line[0] !== "") { $csv[] = $line; } } |
まだまだ想定外の文字が入り込むパターンありそうだが、とりあえず今回はこれでなんとかなった。
CSRF対策
記事のインポートに加えて記事の一括削除機能も作成したのだが、これを外部から叩かれてはたまったものではない。
削除ボタンが存在するページにてトークンを発行しマッチしなければ拒否という処理を挟むことで対処した。トークンの生成方法は多々ありそうなので下記は一例。
送信する側
1 2 3 4 5 6 7 8 9 10 |
<?php //トークンを生成 session_start(); $token = sha1(uniqid(mt_rand(), true)); $_SESSION['token'] = $token; ?> <form action="delete.php" method="post"> <input type="hidden" name="token" value="<?php echo $token;?>"> <button id="delete" class="" type="submit">削除</button> </form> |
受信側
1 2 3 4 5 6 7 |
session_start(); if (empty($_SESSION['token']) || ($_SESSION['token'] !== $_POST['token'])) { echo 'ダメよ~ダメダメ'; exit; } else { //なんかする } |
照合順序
WordPressの検索機能に限らず、半角全角カタカナひらがなを同一視し検索したいという要望は多々想定される。その際、DBのカラムの照合順序をutf8_unicode_ciにすれば全部同一視してヒットするようになる。
例えば「きたがわ」「キタガワ」「キタガワ」どれでもヒットする。キタガワさんが好きです。
SQL文内にcollate utf8_unicode_ciを追加するのがベストであるが、それが難しい場合は素直にデータベース内で該当カラムの照合順序を変更すれば良い。
速くしようと頑張ってみる
トランザクション処理
SQLは都度インサートするよりトランザクションを用いて一括インサートする方が圧倒的に早い。wp_insert_postを用いる場合でもこれは同様。下記を参照し一括コミットするようにすることでかなりの高速化が望める。bulk insertとかってググると色々出てくるので調べてみるといい。
https://wordpress.stackexchange.com/questions/102349/faster-way-to-wp-insert-post-add-post-meta-in-bulk
forかforeachか
結論として、出来上がった配列を扱うのであれば基本的にはforeachが最速…だと思う。for文はカウントをプラスするなどの処理を挟むことを考えれば不利そうな感じはするだろう。
この辺は先人たちが検証したデータを参照されたい。同一処理であっても書き方によって処理速度が変わることを意識しながらコードを書こう。
必要以上に変数を作らない
コードのある箇所にて何を行っているのか分かりやすくするために、何か処理を挟むごとに変数を定義したくなる。が、果たしてそれは本当に必要なのか見直してみると良い。変数が増えれば増えるだけメモリの使用量は増えていくはずだ。例えば
1 2 3 |
$str = 'ホゲ';//全角カタカナに変換する文字列 $converted_str = mb_convert_kana($str);//全角カタカナに変換 echo $converted_str;//出力 |
みたいなヤツ。確かに初学者にとっては順を追いやすいが、それ一旦変数に格納する意味ある?という感じは伝わるだろう。
比較演算子は厳格に
PHPの判定は往々にしてクソ理解が難しいって話はさておき、処理速度の観点からも比較演算子は厳格なものを用いるべきだ。
1 2 3 |
if ($hoge == $piyo) {} //じゃなくて if ($hoge === $piyo) {} |
型の変換を挟む分、前者の方が遅くなる。
まだまだ意識するべきことは考えられるが、csvインポート機能の話から脱線しすぎたので一旦シメとする。読みやすさ・メンテナンス性・速度・メモリ使用量…これらはあるいはトレードオフな関係にあり何かを犠牲にしなくてはならない場合もあるだろう。こういった細かい点までしっかり意識してできる限り「美しい」コードを書きたいものだ。