Google Apps ScriptでWordPressの投稿記事を取得する【GAS】

WordPressの記事が500個を超えたことでメンテナンスが割と大変になってきたのと、GASから記事の内容をGeminiを使ってのリファクタリングをしてみたいと思い、その為にはGAS側から記事データを取得できなければならない

ということで、自身のWordPressのREST APIを利用して記事の中身のデータを取得し、取り敢えずドライブにファイル化をしてみようと思いました。今回はまず、記事一覧の取得と記事のTXT化を実現します。

今回利用する素材

自分の借りてるレンタルサーバーにインストールしたWordPressに対してREST APIを使って記事一覧と記事の内容(コード)を取得します。そのためにはWordPressでのREST APIが有効化しておく必要があります。通常デフォルトでは有効化されているので、無効化されてる場合には使うことが出来ません。

有効化されてるかどうかは以下のURLをブラウザで開いて記事一覧データがでてくれば、有効化されてるということになります。

https://WordPressをインストールしたパス/wp-json/wp/v2/posts

今回のテクニックを使えば記事内容のバックアップ取得という目的だけじゃなく、記事内容をGeminiを使ってリファインしたり、また文章校正をさせた後に書き戻すといった事が可能となるので、色々使い道があるテクニックになります。

また、記事の下書き投稿や更新投稿は以下のエントリーで扱っています。以下のエントリーではGeminiを使った文章校正や記事の生成も行わせているので、より高度な内容になっています。

Google Apps ScriptでWordPressの記事修正や投稿をする【GAS】

事前準備

ファイル格納先フォルダの用意

今回は、記事一覧を取得し、該当する記事レコードを選択状態で記事取得をすると、WordPressの記事編集画面における「コード」の内容を取得してテキスト化し、Google Driveに保存するという処理を作成しています。この時の格納先フォルダを作っておき、フォルダのIDを取得しておきましょう。

また、GASのコード内のグローバル設定にある「FOLDER_ID」にその値を書いて保存しておきます。このグローバル設定にはもう一つ、WordPressのトップ画面のURLを入れる場所があるので、WP_URLの変数にそのURLを記述しておきましょう。

図:folderidを取得して格納しておく

アプリケーションパスワード生成

WordPressのREST APIを叩く為には認証処理が必要です。そこで以下の手順でアプリケーションパスワードを生成しておきます。このパスワードは重要なものなので、流出しないように厳格に管理する必要があります。

  1. 自分のWordPressにログインする
  2. 左サイドバーより、ユーザー→プロフィールをクリックする
  3. 下の方にあるアプリケーションパスワードにて、パスワード名を入れてアプリケーションパスワードを追加ボタンをクリック
  4. パスワードが生成されるのでコピーする(このパスはこの時にしか表示されません)

ついでにログインするユーザー名もユーザー一覧から取得しておきましょう。

図:アプリケーションパスワードを生成する

スクリプトプロパティに格納

アプリケーションパスワードを利用してGASからログインし、REST APIを叩くことになるので、以下の内容をスクリプトプロパティに格納しておきます。

  • userid : WordPressに登録済みのユーザー名を入れておく
  • apppw : アプリケーションパスワードを格納しておく

図:スクリプトプロパティに格納

ソースコード

記事一覧を取得する

まずは記事一覧を取得する必要があります。スプシには「記事一覧シート」のみが存在しており、ここに一覧データを出力します。exportPostsToSheet関数を実行することで実現できます。

但し、WordPressは1回のリクエストで100記事文のデータしか返せないので、ページネーション処理を利用し、エラーコード400が返ってきた段階で最終ページまで取得したことになるため、ループでそのための処理を実装します。

記事内容は配列で一括で書き込みを行います。実際の記事一覧取得はgetAllPostsWithPagination関数が担当しています。

図:記事内容一覧を取得できました。

//公開記事一覧を取得して出力する関数
function exportPostsToSheet() {
  //UIを取得する
  const ui = SpreadsheetApp.getUi();

  try {
    //記事一覧をREST APIを叩いて取得する
    const allPosts = getAllPostsWithPagination();
    if (!allPosts || allPosts.length === 0) {
      Browser.msgBox('記事が見つかりませんでした。');
      return;
    }

    //シートを取得してくる
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("記事一覧")
    sheet.getRange("A2:D").clearContent();

    // データを書き込む
    const data = allPosts.map(post => [post.id, post.date, post.title, post.link]);
    sheet.getRange(2, 1, data.length, data[0].length).setValues(data);
    
    // 列幅を自動調整
    ui.alert(`完了: ${allPosts.length}件の記事をシートに出力しました。`);
  } catch (e) {
    ui.alert(`エラー: ${e.message}`);
  }
}

/**
 *【修正版】ページネーションを考慮して、WordPressの全記事一覧を取得します。
 * 400エラーを「最終ページ」として扱います。
 * @returns {Array<{id: number, date: string, title: string, link: string}>} 記事情報の配列
 */
function getAllPostsWithPagination() {
  const allPostsData = [];
  let page = 1;
  const perPage = 100; // 1回のリクエストで取得する件数(最大100)

  while (true) {
    //リクエストエンドポイント
    const endpoint = `${WP_URL}/wp-json/wp/v2/posts?_fields=id,date,title,link&per_page=${perPage}&page=${page}`;
    
    //ページの記事を取得する
    console.log(`- ページ ${page} の記事を取得中...`);
    const response = UrlFetchApp.fetch(endpoint, { 'muteHttpExceptions': true });
    const responseCode = response.getResponseCode();

    // ステータスコード400は「リクエストしたページが存在しない」ことを示すため、
    // これをエラーではなくループの終了条件として扱う
    if (responseCode === 400) {
      console.log(`- ページ ${page} が存在しないため、記事の取得を完了します。(ステータスコード: 400)`);
      break; // 正常な終了としてループを抜ける
    }

    // 400以外の200でないステータスコードは、本当のエラーとして処理する
    if (responseCode !== 200) {
      throw new Error(`記事一覧の取得に失敗しました (ページ: ${page})。ステータスコード: ${responseCode}`);
    }

    //記事一覧結果を取得
    const postsOnPage = JSON.parse(response.getContentText());

    // レスポンスが空配列の場合も、念のため終了条件として残しておく
    if (postsOnPage.length === 0) {
      console.log(`- 記事が0件のため、取得を完了します。`);
      break;
    }

    //配列にプッシュする
    allPostsData.push(...postsOnPage);
    page++;

    //ウェイトを1秒間掛ける
    Utilities.sleep(1000); 
  }
  
  console.log(`- 全ての記事の取得が完了しました。`);
  
  //結果を返す
  return allPostsData.map(post => ({
    id: post.id,
    date: new Date(post.date).toLocaleString('ja-JP'),
    title: post.title.rendered,
    link: post.link
  }));
}

記事をファイル化する

スプシ上の記事一覧のレコード上のセルをクリックしてから、メニューのプログラム→記事ファイル化を実行すると先頭の記事IDを元に、対象の記事のコードを取得後、TXT化してドライブに生成します。

createTxtFileFromPost関数がその関数となり、ここでアプリケーションパスワードを使って認証して取得しています。記事のタイトルがそのままファイル名となり、getPostContent関数が主体となって記事内容を取得します。

一括で全部ファイル化したいならば記事一覧をループで回せばよいだけですが、サーバー負荷が掛かるのでUtilities.sleepで1記事取得の度に1秒程度のウェイトを入れるのを忘れずに。

図:記事をテキスト化出来ました

/**
 *【実行関数②】
 * 現在選択している行の記事IDを元に、記事本文をTXTファイルとしてDriveに保存し、
 * ファイルIDをシートのE列に記録します。
 */
function createTxtFileFromPost() {
  //スプシのUIを取得
  const ui = SpreadsheetApp.getUi();

  if (FOLDER_ID === 'ここにフォルダIDを貼り付け' || FOLDER_ID === '') {
    ui.alert('エラー', 'スクリプト内の「FOLDER_ID」が設定されていません。保存先のGoogle DriveフォルダのIDを入力してください。', ui.ButtonSet.OK);
    return;
  }

  try {
    //スプレッドシートを取得する
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName("記事一覧");
    const activeRange = sheet.getActiveRange();
    const activeRow = activeRange.getRow();

    // A列から記事ID、C列から記事タイトルを取得
    const postId = sheet.getRange(activeRow, 1).getValue();
    const postTitle = sheet.getRange(activeRow, 3).getValue();

    // 記事IDが数値でなければ処理を中断
    if (typeof postId !== 'number' || postId <= 0) {
      ui.alert('処理中断', '選択された行のA列に有効な記事IDが見つかりません。記事IDが含まれる行を選択して実行してください。', ui.ButtonSet.OK);
      return;
    }

    //生成中メッセージ表示
    ss.toast(`記事「${postTitle}」の記事を取得してファイルを作成中です...`, '作業中', -1);

    //記事の本文を取得(コード)
    const rawContent = getPostContent(postId);

    //ファイル名をサニタイズ(ファイル名に使えない文字を削除)
    const fileName = `${postTitle.replace(/[\\/:"*?<>|]/g, '_')}.txt`;

    //指定されたフォルダにTXTファイルを作成
    const folder = DriveApp.getFolderById(FOLDER_ID);
    const file = folder.createFile(fileName, rawContent, MimeType.PLAIN_TEXT);
    const fileId = file.getId();

    //シートのE列にファイルIDを書き込む
    sheet.getRange(activeRow, 5).setValue(fileId);

    //終了メッセージ
    SpreadsheetApp.flush(); // 即時反映
    ss.toast(`記事「${postTitle}」のTXTファイルを作成し、E列にファイルIDを記録しました。`, '完了', 10);

  } catch (e) {
    console.log(e);
    ui.alert('エラー', `処理中にエラーが発生しました。\n\n詳細: ${e.message}`, ui.ButtonSet.OK);
  }
}

/**
 * 指定されたIDの記事内容(本文)を取得します。
 * @param {number} postId 取得したい記事のID
 * @returns {string} 記事の本文 (HTML形式)
 */
function getPostContent(postId) {
  if (!postId) {
    throw new Error('記事IDが指定されていません。');
  }

  // スクリプトプロパティから認証情報を取得
  const scriptProperties = PropertiesService.getScriptProperties();
  const username = scriptProperties.getProperty('userid');
  const appPassword = scriptProperties.getProperty('apppw');

  // 認証情報が設定されているか確認
  if (!username || !appPassword) {
    throw new Error('スクリプトプロパティに「userid」と「apppw」が設定されていません。メニューの「ファイル」>「プロジェクトのプロパティ」で設定を確認してください。');
  }

  // 生のコンテンツを取得するため、URLに ?context=edit を追加
  const endpoint = `${WP_URL}/wp-json/wp/v2/posts/${postId}?context=edit`;
  
  // Basic認証のためのヘッダーを作成
  const encodedAuth = Utilities.base64Encode(`${username}:${appPassword}`);
  const headers = {
    'Authorization': `Basic ${encodedAuth}`
  };

  //リクエストオプション
  const options = {
    'method': 'get',
    'headers': headers,
    'muteHttpExceptions': true
  };

  //HTTPリクエスト実行
  const response = UrlFetchApp.fetch(endpoint, options);
  const responseCode = response.getResponseCode();
  
  // 認証失敗のハンドリング
  if (responseCode === 401 || responseCode === 403) {
    throw new Error(`WordPressへの認証に失敗しました。スクリプトプロパティの「userid」と「apppw」が正しいか確認してください。(コード: ${responseCode})`);
  }

  if (responseCode !== 200) {
    throw new Error(`記事内容の取得に失敗しました (ID: ${postId})。ステータスコード: ${responseCode}`);
  }
  
  //記事の中身を取得する
  const json = JSON.parse(response.getContentText());
  
  // 「raw」(生データ)のコンテンツを返す
  return json.content.raw;
}

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)