CUEBiC TEC BLOG

キュービックTECチームの技術ネタを投稿しております。

REST APIを使ってカスタムフィールドを一括更新してみた

最近WordPressよりGASと戯れる時間が長くなってきているmikihoです。

前回、RESTAPIを使って記事一覧を作りましたが、今回はそれの発展系。
WordPressからスプレッドシートの連携ではなく、スプレッドシートからWordPressへの情報更新を実現させていきます。

調べてみると「投稿する」「更新する」など、単発記事への更新はそこそこありますが、今回目指すのは一括での情報更新です。
なければ作ればいいのということで、更新のためのコードを参考にしながらGASと向き合いました。

CSVデータでも生成させた方が早いのではないか?という疑念は常にありましたが、今回はREST APIを用いた方法を検証してみます。

リストを作るところは前回やったのでいいとしましょう。
気になる方は前回の記事をチェックしてくださいね。

cuebic.hatenablog.com

やってみた

事前準備

今回は情報の更新ということで、WordPress内でアプリケーションパスワードを取得する必要があります。
このアプリケーションパスワードはユーザーに紐づいて発行されるものなので、今回の目的上既存投稿の編集権限を持つアカウントでパスワード発行をしてください。

管理画面からプロフィールに移動して、下の方にパスワードの発行場所があるのでそこからパスワードを発行してください。

WordPressのアプリケーションパスワード発行場所

保存したパスワードと、パスワードが紐づいたアカウントのユーザーIDはスプレッドシートスクリプト プロパティに保存しておきましょう。
名前はなんでもいいんですが今回はapi_keyとapi_userとしておきます。

スプレッドシートのスクリプトプロパティ保存場所

※弊社の場合外部からアクセスできないように制限しているために、一旦このような形での運用しております。
悪用されないようにユーザーIDとパスワードが外部に漏れないように注意してください。

そして次に、今回はカスタムフィールドの情報を含むので、次にfunctions.phpにてregister_rest_fieldで定義させる必要があるので、そこら辺を追加していきます。

<?php
add_action( 'rest_api_init',  function() {
    register_rest_field(
        'post',        // カスタムフィールドを利用しているpost_type名
        'post_meta',   // rest-apiに追加するキー
        array(
            'get_callback'  => function(  $object, $field_name, $request  ) {
                // 出力したいカスタムフィールドのキーをここで定義
                array_push($meta_fields ,'カスタムフィールド名'); 
                $meta = array();
                foreach ( $meta_fields as $field ) {
                    $meta[ $field ] = get_post_meta( $object[ 'id' ], $field, false );
                }
                return $meta;
            },
            'update_callback' => function(  $value, $post, $field_name) {
                if (!$value) {return;}
                foreach($value as $key => $data){
                if(is_array($data)){
                    foreach($data as $record){
                        update_post_meta($post->ID, $key, $record);
                    }
                }else{
                    update_post_meta($post->ID, $key, $data);
                }
            }
            },
                'schema'=> null,
            )
        );
    });
?>

事前準備はこれだけ。
ここからGASのコードを交えて解説していきます。

とりあえず全部更新させてみた

GAS側の処理としてはこんな感じ。

var siteUrl = '更新したいWPサイトURL';  
var Key = PropertiesService.getScriptProperties().getProperty('api_key');
var User = PropertiesService.getScriptProperties().getProperty('api_user');
var sheetArticleList = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シートの名前');
function postReport() {
  var lastRow = sheetArticleList.getLastRow();  
  for(var i=2;i < lastRow + 1;i++){ //1行目は項目名なので、2行目からスタート
    var headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + Utilities.base64Encode(User + ":" + Key)
    };
    //post_idとtitleの取得
  var postId = sheetArticleList.getRange(i,2).getValue();
  var title = sheetArticleList.getRange(i,3).getValue();
  if(postId){
    var post_meta = {};
    var index = 4;//更新したいカスタムフィールドの位置
        post_meta['カスタムフィールド名'] = sheetArticleList.getRange(i,index).getValue();
    var arguments = { 
            'title': title,
        post_meta
      };
    var apiUrl = siteUrl + '/wp-json/wp/v2/post_type名/'+ postId;
      var options = {
        'method': 'POST',
        'muteHttpExceptions': true,
        'headers': headers,
        'payload': JSON.stringify(arguments)
        };
        var response = UrlFetchApp.fetch(apiUrl, options);
        var responseJson = JSON.parse(response.getContentText());
        
        JSON.parse(UrlFetchApp.fetch(apiUrl, options));
    }
  }
}

これは例なのでわかりやすくタイトルとテストのカスタムフィールドのみ更新が行えるコードになってます。
※タイトルやカスタムフィールドの値を取得するための数値は適宜環境ごとに変更してください。

参考: motoki-design.co.jp

実際に処理を走らせたんですが…
基本全記事に対してRESTAPIへ更新処理を走らせる必要があるので、非常に時間がかかる…!

テスト実装していたメディアには160を超えるデータがあったので30分程度の時間がかかりました。
データが増えればtimeoutするという、とんでもない代物となり却下。

mikihoは 力尽きた...

なんて茶番が頭によぎった途端、死んでしまうとは情けないという言葉に叩き起こされたのでもうちょっと改良していきます。

更新したものだけ抽出してみた

最初からそうしろという話ではありましたが、更新したデータだけにすれば負荷軽くなるじゃん!ということでコードをかなり書き換え。

少々原始的な方法を使いながら更新データを抽出して更新処理をするという方法に変えてみました。

まず、更新されたデータの収められた行が何行目なのか、を判別させるためのコードを書きます。
こんな感じ。

function showStatus(){
  var mySheet = SpreadsheetApp.getActiveSheet(); //シートを取得
  var myCell = mySheet.getActiveCell(); //アクティブセルを取得
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('更新データを貯めたいシート名');
  if(myCell.getColumn() === 3 || myCell.getColumn() > 4){ //アクティブセルが更新対象フィールドかの判別
   var lastRow = sheet.getRange('A:A').getValues().filter(String).length;
   sheet.getRange(lastRow+1,1).setValue(myCell.getRow());
  }
}

そう、更新データというタブを増やして、そこに更新行だけひたすらためていくためのシートを作ったんです。
今回は特定のフィールドの更新しかさせないために、特定列の更新が入ったタイミングでのみ更新行を書き出すようにif文を仕込みました。

あとはこの関数を、シート更新時にトリガーを設定しておくだけ。
これで更新した行数が自動でシートに書き込まれていきます。

さて、次に最初に作ったコードをこんな感じに変えていきます。

var siteUrl = '更新したいWPサイトURL';  
var Key = PropertiesService.getScriptProperties().getProperty('api_key');
var User = PropertiesService.getScriptProperties().getProperty('api_user');
var sheetArticleList = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シートの名前');
function postReport() {
  //重複した更新データの削除
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('更新データを貯めたシート名');
  var range = sheet.getDataRange();
  range.removeDuplicates();
  //更新するデータ行の取得
  var values = sheet.getDataRange().getValues();
  for(i=0;i < values.length;i++){
    var headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + Utilities.base64Encode(User + ":" + Key)
    };
    var postId = sheetArticleList.getRange(values[i],2).getValue();
    var title = sheetArticleList.getRange(values[i],3).getValue();
    if(postId){
      var post_meta = {};
      post_meta['カスタムフィールド名'] = sheetArticleList.getRange(i,index).getValue();
    var arguments = { 
     'title': title,
      post_meta
      };
    var apiUrl = siteUrl + '/wp-json/wp/v2/post_type名/'+ postId;
      var options = {
        'method': 'POST',
        'muteHttpExceptions': true,
        'headers': headers,
        'payload': JSON.stringify(arguments)
        };
        var response = UrlFetchApp.fetch(apiUrl, options);
        var responseJson = JSON.parse(response.getContentText());
        
        JSON.parse(UrlFetchApp.fetch(apiUrl, options));
    }
    }
    //更新したデータを貯めたシートをクリアする
    sheet.clear();
  }

更新データのシートは同じ行が更新されるとその都度追加してしまうので、最初のタイミングで同じデータが入ってるものは削除してまとめます。
重複したデータが無くなった状態の内容を改めて取得。
あとは、データ数分ループを回して更新処理を走らせるだけ

こんな感じの処理にすることによって、更新データ数を制限するので、最初の処理よりかは遥かに軽くなりました。

更新したいデータにのみチェックを入れてもらうという案もあったものの、そもそも一括で更新して工数を減らしたい、というのにわざわざ手順を一つ増やす意味があるのか?
と、自問自答した結果更新データも自動で抽出する形にしてみました。

もう少し上手いやり方はあると思うのですが、期限的に今はとにかくアウトプットを優先ということで、この仕様で納品いたしました。

やってみて

記事の本文はともかく、カスタムフィールドを多用していると意外といちいち編集画面に入って、更新して…という手間がかかって気づいたら工数がかさむということが運用側はそこそこあるようでした。
ですが、調べてみてもスプレッドシートから記事を投稿・更新するというのは情報が色々あるんですが一括はなかったです。

おそらくそこまでやりたいならCSVファイルでの一括更新がプラグインなどで手軽に実装できるので必要性がなかったのでしょう。(多分)

作ってみた感想としてはCSVファイルに落とし込んだほうが早いのでは…?というところはありました。
既存のものは手軽だし、どのプラグインを選んだかに左右されるもののある程度セキュリティ担保もされていますし...
とはいえ、今回のやり方を使えばファイルを落としてきてアップロードするという手間は省けます。

かといってCSVより今回のやり方の方がよかったとは言い切れません。
ケースバイケースとしか言え無さそうだと感じました。
ダウンロードしてきたCSVファイルの取り扱いの運用のめんどくささを考えるとシートから直接更新の方に多少のメリットがありそう、とは思いましたが。
運用体制も含めて、どちらが最適なのかのかを検討する必要がありそうです。