WordPressにCSVデータをインポートするために様々なプラグインがありますが、要件に応じた柔軟な対応は難しいものがあります。
例えば、「社内で管理している商品番号をキーにしたい」や「基幹システムから出力したCSVを毎日自動で取り込みたい」や「管理画面から『上書き禁止』設定をしたものはCSVインポートで上書かない」などなど。
こういう要件に関してはプラグインをカスタマイズをするよりも、自前でのCSVインポート処理を作ってしまうのが良いと思います。
そんなに難しくありませんし、とても柔軟に作ることが出来ます。
この記事では以下の流れで解説します。
比較的少量のデータや簡易な構成の場合は、上記の2の手順までで完結しますが、
CSVデータが増えた時のことを考えると4の手順まで実装しておくと安心かもしれません。
(4まで実装するとユーザーが使いやすいですし)
1.CSVファイルのアップロード処理
まずは、CSVファイルをアップロードする処理について書きます。
基幹システムなどでサーバーに置く場合はこの部分は不要ですね。
Points
アップロード用固定ページ作成(非公開設定)
CSVインポートする画面は管理者以外には使わせる必要はないので、非公開の固定ページを作成し、そこをCSVファイルをアップロードできる画面にします。
<input type=”file>でファイルをアップロードできますが、<form>に「enctype=”multipart/form-data”」を指定する必要があります。
1 2 3 4 5 | <form id="form" class="form_wrap" action="/csv_import_run/" method="POST" enctype="multipart/form-data"> <input type="file" name="csv_file"> <input type="hidden" name="key" value="【ランダムなキー】"> <input type="submit" value="csvインポート実行!!"> </form> |
key にランダムな文字列をセットし、後で照合することにより、この後作成するプログラムの不正な使用を防ぐことができます。
(アップロード用ページは非公開ページですのでここにkeyを書いちゃって大丈夫です)
アップロード処理について
<form>でファイルをアップロードしたら、それを受け取る処理を書く必要があります。
アップロードしたファイルの情報は変数「$_FILES」に格納されます。それを利用して例えば以下のように処理を書きます。
1 2 3 4 5 6 7 | if (is_uploaded_file($_FILES["csv_file"]["tmp_name"])) { if (move_uploaded_file($_FILES["csv_file"]["tmp_name"], dirname(__FILE__) . "/csv/" . $_FILES["csv_file"]["name"])) { $upload_message = $_FILES["csv_file"]["name"] . "をアップロードしました。"; } else { $upload_message = "ファイルをアップロードできません。"; } } |
(必要なら)ファイル自体をUTF-8に変換
CSVファイルをエクセルで作成することが多いと思いますが、エクセルで保存すると通常は文字コードが「Shift-JIS」 になります。
最近はエクセルでも「UTF-8」でCSV保存することが出来ますが、これにはまた別の罠があります。。。(UTF-8 BOMあり で保存される)
WPでの通常扱うことのできる文字コードは「UTF-8 BOMなし」ですので、上記の場合は文字コードを変更する必要があります。
『文字コードって何?』というクライアントもいますので、こちら側の処理で変更してあげたほうが良い場合が多いです。
1 2 | // ファイル自体をUTF-8に変換 system('nkf --overwrite -w ' . $filepath); |
インポート処理用PHPを呼び出して実行
インポート処理を直接書いても良いのですが、それ用のPHPを作って呼び出したほうが後々便利なので、以下のようにしてPHPを呼び出します。
1 2 | // 処理用PHPの呼び出し exec('php ' . __DIR__ . '/cgi/csv_import.php ' . $filepath . ' 【ランダムなキー】'); |
しかし、この書き方では、外部処理にしたことをあまり有効に活かせていません。
CSV処理が完了するまで、そのページは【処理中】となるからです。(時間のかかる処理ならブラウザの読み込み時間制限によりタイムアウトしてしまいます)
ですので、以下のように記述します。
1 2 | // 処理用PHPの呼び出し:非同期 : ログ追記 exec('php ' . __DIR__ . '/cgi/csv_import.php ' . $filepath . ' 【ランダムなキー】 >> ' . __DIR__ . '/cgi/csv_import.log &'); |
『PHPの非同期処理』などで調べると「【プログラム】 > /dev/null &」という書き方がけっこう出てきますが、ログを取る場合は上記の書き方が良いです。
これで、とりあえずCSVインポート処理用のプログラムを実行できました。
次の部分ではプログラムの中身について書いていきます。
2.インポート処理用のPHP作成
WordPressで直接制御しないPHPを読み込んでいますので、WPの関数などが使えるように、まずはWPを読み込んでやります。
1 | require_once ('【WPのルートディレクトリ】/wp-load.php'); |
このプログラムの中でどのような処理にするかは案件によって変わるわけですが、ポイントとケースをいくつか記載します。
Points
Cases
CSVファイルを読み込んで配列に入れる
CSVの取り込みには、「SplFileObject」を使うと便利です。(PHP5.1以上)
1 2 3 4 5 6 | $file = new SplFileObject($filepath); $file->setFlags(SplFileObject::READ_CSV); // ファイル取得 foreach ($file as $key => $line) { // 処理 } |
上記の「$line」という配列に、CSVの1行がさらに配列の形で入っています。それぞれのセルの値を取りたい場合は配列番号で指定できます。
分かりやすい配列の扱い方
しかし、配列番号で今後の処理を書くのは、パッと見て何の項目か分からなくなり不具合の温床ともなります。
そこで次のようにすると分かりやすいと思います。(これはただの提案です。もっと良いやり方があるかもしれません。)
CSVにはhead行をつけていると思いますので、このheadの項目名で扱えるととても便利です。
head行を読み込んでそれを逆配列(キーと値を入れ替えた配列)にしておき、それを用いて各セルの値を指定します。
「array_flip」という関数で可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | $file = new SplFileObject($filepath); $file->setFlags(SplFileObject::READ_CSV); $i = 0; // ファイル取得 foreach ($file as $key => $line) { // ヘッダを読込 if ( $i === 0 ) { $csv_heads = $line; $csv_heads_key = array_flip($csv_heads); // ←ここがポイント!! $head_count = count($csv_heads); $i ++; continue; } print $line[$csv_heads_key['station_name']]; // 函館 print $line[$csv_heads_key['add']]; // 北海道函館市若松町12-13 $i++; } |
ここから実際の処理を書いていくわけですが、ざっくりとよくあるケースの記述を書いていきます。
Case : カスタム投稿に投稿する
これが一番多いでしょうか。CSVの1行が1つのカスタム投稿になる、というものです。
投稿を追加するのは「wp_insert_post」というWordPressの関数を使いますが、この関数はただ追加するだけです。
CSVの中身を1度だけ入れる、というのならこれでポンポン入れていけば良いでしょうが、CSVで管理しているものを更新してそれを適宜インポートしたい、という場合はこのやり方ではだめです。
WordPressは投稿をIDで管理していますが、WordPressの持つIDとCSVの一意なidが一致することは意図的にそうしない限り、ないでしょう。
例えば商品を管理するCSVでは品番で一意に管理しているかもしれませんが、これをそのままWordPressのIDにすることは無理です。
ですので、CSVでの一意になる項目(複数のキーで一意になるのでも良い)で投稿を検索して、見つかったら更新(wp_update_post)し、見つからなかったら新規投稿(wp_insert_post)すれば良いわけです。
↓上記の「画像1」で1列目をスラッグとして一意に管理している場合の書き方(※スラッグで管理するのは実際はお勧めしません。WordPressの内部処理で思ったのと違うスラッグで投稿されることがあるからです)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | ... $args = array( 'post_type' => 'station', 'posts_per_page' => 1, 'post_status' => 'any', 'name' => 'station_cd' ); $the_query = new WP_Query($args); if($the_query->have_posts()): while($the_query->have_posts()):$the_query->the_post(); $target_post_id = $post->ID; // 更新処理 $update_post = array( 'ID' => $target_post_id, 'post_title' => $line[$csv_heads_key['station_name']], 'post_modified' => date('Y-m-d H:i:s') ); wp_update_post( $update_post ); endwhile; else: // 新規登録処理 $insert_post = array( 'post_title' => $line[$csv_heads_key['station_name']] ); $target_post_id = wp_insert_post( $insert_post ); endif; ... |
Case : カスタムフィールドに値をセットする
CSVでインポートしたい、なんて時にはだいたいカスタムフィールドに値を入れたいものです。
「update_post_meta」を使って入れることが出来ます。「update_post_meta」は「update」なので更新専用と一瞬思いますが、対象が存在しないときは「add_post_meta」というカスタムフィールドを追加する処理を内部で行ってくれます。ですので「update_post_meta」だけで完結出来ます。
1 2 3 4 5 | ... update_post_meta( $target_post_id, 'add', $line[$csv_heads_key['add']] ); update_post_meta( $target_post_id, 'lon', $line[$csv_heads_key['lon']] ); update_post_meta( $target_post_id, 'lat', $line[$csv_heads_key['lat']] ); ... |
単一のデータを入れる場合はこれで良いですが、複数データ(繰り返しカスタムフィールド)を入れたいときには、一度消してから追加する、という流れです。
例えば、CSVのセルにカンマ区切りで入っているデータを繰り返しフィールドの形でWPに入れたい場合は以下のように行います。
1 2 3 4 5 6 7 8 | ... delete_post_meta( $target_post_id, 'multi_meta_key' ); $multi_array = explode(',', $line[$csv_heads_key['multi_meta_key']]); foreach ( $multi_array as $multi_data ) { add_post_meta( $target_post_id, 'multi_meta_key', $multi_data ); } ... |
Case : タクソノミーをセットする
タクソノミーをセットしたいことも多々あると思います。
これも難しくありません。「wp_set_object_terms」を使うことができます。
1 2 3 4 | ... wp_set_object_terms( $target_post_id, $line[$csv_heads_key['pref_cd']], 'tax_pref', false ); // 最後の引数が false なら「上書き」、 true なら「追加」の処理になります。 ... |
ちなみに、タクソノミーにもメタ情報を持たせることが出来ます。
「update_term_meta」を使えば「update_post_meta」と同じような感覚でタクソノミーのメタ情報をセットできます。
補足
ここまででCSVの中身をWPに入れる処理はたいてい出来ると思います。
インポートしたメタ情報(カスタムフィールド)についての補足ですが、WPの初期状態では管理画面からは見えませんが情報としては確かに入っています。
管理画面で視覚的に見るためにはカスタムフィールド用のプラグインなどを利用することが出来るでしょう。
おすすめは「Smart Custom Fields」です。有名なプラグインで「Advanced Custom Fields」というのがありますが、繰り返しフィールドの値の保存の仕方がWordPressの標準の概念と異なっているので、「Smart Custom Fields」を使う方が良いです(Advanced Custom Fieldsは、WordPress自体が初期には繰り返しフィールドに対応していなかったので、独自のやり方で繰り返しフィールドに対応してくれていたのでこの形になったのですが)。
3.大量のデータや処理でメモリが足りなくなるケース対応
さて、CSVで管理しているデータはたいていそこそこ大容量です。だからこそCSVでインポートしたいと思うわけですが。
10,000行くらいのCSVをインポートする場合など、環境によっては処理が完了せずにタイムアウトしてしまうことがあります。またメモリ不足で落ちてしまうこともあります。
『10,000件くらいで!?』と思うかもしれませんが、WPの関数「wp_update_post」や「update_post_meta」「update_term_meta」などは既存データの確認や排他処理などを裏で行っているため結構処理が重たいのです。
そんなときには、1つのプロセスで処理するCSVの行数を制限して段階的に処理していけば大丈夫です。
PHPは「exec」関数で、linuxの標準コマンドを実行できます。それを利用して非同期にPHPを実行します。
1 2 3 | ... exec( 'php ' . __DIR__ . '/csv_import_run.php >> log_file &' ); ... |
これではただの非同期実行ですから、行数を制限していることにはなりません。
実行時に標準引数を付けてやることにより、開始行数を設定するようにしましょう。そして自分を再帰的に呼び出します。例えばこんな風に書けます。(セキュリティ的なことは考慮していません。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <?php $RUN_NUMBER = 1000; // 1プロセスで処理する件数 // 引数がない場合は処理しない if ( $argc < 1 ) { exit; } $filepath = $argv[1]; $start_row = $argv[2] ? $argv[2] : 1; $file = new SplFileObject($filepath); $file->setFlags(SplFileObject::READ_CSV); $i = 0; // ファイル取得 foreach ($file as $key => $line) { // ヘッダを読込 if ( $i === 0 ) { $csv_heads = $line; $csv_heads_key = array_flip($csv_heads); $head_count = count($csv_heads); $i ++; continue; } $i++; if ( $i < $start_row || $i > $start_row + $RUN_NUMBER ) { continue; } // ここで処理実行 // 実行した際にWPに登録したり更新した件数をカウントしておく $insert_count, $update_count if ( $insert_count > 0 || $update_count > 0 ) { exec( 'php ' . __DIR__ . '/csv_import_run.php ' . $filepath . ' ' . ( $start_row + $RUN_NUMBER + 1 ) . ' >> log_file &' ); } } ?> |
4.非同期処理で行う場合処理が完了したかどうか確認
上記の3の書き方で非同期処理を行った場合、ユーザー(管理者)としては処理が完了したかどうか分かりません。
いつ終わったのか、そもそも正常に終わったのか、気になることでしょう。
そこで、書き込みしているログファイルを管理画面で見ることが出来れば便利だ!と気づきました。
非公開のページを作り、そこでログファイルを読み込めば良いのです。
しかし、ログファイルも結構なサイズになります。それを全部読み込むのは処理的に大変です。
そういう場合は、最後の数十行だけを表示すれば良いのです。
linuxの標準コマンド「tail」を使うことにより簡単に実装出来ます。
1 2 3 4 5 | ... $log_file = __DIR__ . '/log_file'; exec('tail -30 ' . $log_file, $output); ... |
上記の「$output」に返ってきた値が入っていますので、それを適当に表示させてあげればOKです。
南本貴之