レスポンシブ検索広告アセットレポート出力

更新履歴

2024/07/24 目次を追加


スクリプトの概要

Yahoo!広告スクリプトでは、レスポンシブ検索広告アセットレポートをアセット(タイトル、説明文)単位でGoogleスプレッドシートに自動で出力し、その結果をメールやSlackで通知することができます。
本機能をご利用いただくことで、アセットの掲載評価を確認する運用工数を削減することが可能です。

レスポンシブ検索広告アセットレポートについて

レスポンシブ検索広告アセットレポートについては、こちらのヘルプをご覧ください。

本ページでご紹介するスクリプトでは、以下の2パターンのレポートをGoogleスプレッドシートに出力可能です。

【レポートパターン】

パターン1:全項目を行追記する形式
レスポンシブ検索広告アセットレポート(アセット単体)の項目を、レポート取得日とともに行追記で出力します

パターン2:掲載評価を列追記する形式
レスポンシブ検索広告アセットレポート(アセット単体)の掲載評価を列追記で出力します

【各レポートで出力される項目】

本スクリプトでは、以下の項目をレポートに出力します。
レポートパターン1
(全項目を行追記する形式)
レポートパターン2
(掲載評価を列追記する形式)
レポート取得日
キャンペーンID
広告グループID
広告ID
キャンペーン名
広告グループ名
広告名
アセット
アセットタイプ
表示位置の固定
掲載評価
インプレッション数 出力されない
広告の配信ステータス 出力されない

ご利用の流れ

1.Yahoo!広告スクリプトとGoogleアカウントを連携します。

 詳しくはGoogleアカウントとの連携を確認してください。
 ※すでに連携済みの方は次のステップからご設定ください。

2.連携したGoogleアカウントにて、レポートを出力するGoogleスプレッドシートを新規作成してください。

 作成したスプレッドシートIDを確認してメモしておいてください。スクリプト内の定数の設定で利用します。
 ※GoogleスプレッドシートIDの取得方法についてはこちらをご覧ください。

3.新規作成したスプレッドシート内に、以下2つのシートを作成し、それぞれ任意のシート名をつけてください。スクリプト内の定数の設定で利用します。

 ・レポートパターン1を出力するためのシート(シート名の例「全項目を行追記」)
 ・レポートパターン2を出力するためのシート(シート名の例「掲載評価を列追記」)

4.管理画面上のスクリプト作成画面にて、本ページに後述のサンプルコードを設定してください。

 ※スクリプトの新規作成および編集の手順はこちらをご覧ください。

5.管理画面上にてスクリプトの設定を完了後、スクリプトの実行頻度の設定をしてください。

 本スクリプトでは週次(毎週 ●曜日 XX時)の設定が推奨です。
 ※スクリプトの実行頻度の設定手順はこちらをご覧ください。

サンプルコード内各定数のご説明

後述のサンプルコードにおける各定数の設定方法についてご説明いたします。

・スプレッドシートの指定
レポートを出力する対象のスプレッドシートを指定します。出力するレポートパターンにかかわらず必須です。
設定する定数 解説
const SPREAD_SHEET_ID = 'スプレッドシートID'; スプレッドシートIDを '(半角シングルクオーテーション)の間に記載

・レポートパターンの選択
スクリプト内の定数の設定によって、1度のスクリプトの実行で、2パターンとも出力/パターン1のみ出力/パターン2のみ出力、というように各レポートの出力有無の選択ができます。必要に応じてそれぞれの定数の設定をお願いします。
レポートパターン 設定する変数 解説
レポートパターン1
(全項目を行追記する形式)
const OUTPUT_LIST = true; レポート出力有無 (true : 出力する、false : 出力しない) を指定
const SHEET_NAME_LIST = 'レポートパターン1を出力する対象のシート名'; レポートパターン1を出力する対象のシート名を '(半角シングルクオーテーション)の間に記載
レポートパターン2
(提掲載評価を列追記する形式)
const OUTPUT_TREND = true; レポート出力有無 (true : 出力する、false : 出力しない) を指定
const SHEET_NAME_TREND = 'レポートパターン2を出力する対象のシート名'; レポートパターン2を出力する対象のシート名を '(半角シングルクオーテーション)の間に記載
const OUTPUT_ROW_LIMIT = 4; 追記する結果評価行の最大数です(任意の数を指定可能。理想4、最大8) 追加された列がこの数以上になると、古い列から削除されます

・出力対象とするアセットの条件を指定
アセットの数が多い場合は、出力対象とするアセットを絞るために、必要に応じてそれぞれの定数の設定をお願いします。
設定する定数 解説
const CAMPAIGN_IDS = [ ]; キャンペーンで出力対象を指定する場合のみ、キャンペーンIDを記載(例:['123456','234567'])
指定しない場合は空欄のまま(この場合は全キャンペーンが出力対象になります)
const HAS_IMPRESSIONS = true; インプレッションが発生したアセットのみ抽出する場合はtrueを指定
インプレッションなしも含める場合はfalseを指定

・実行結果のメール通知、Slack通知
実行結果をメールやSlackで通知する場合、定数の設定方法の詳細はこちらをご覧ください。

サンプルコード

下記のスクリプトを、「コピー」ボタンを押してコピーし、スクリプトの入力画面に貼り付けてください。(このとき、灰色のコメント部分は消さずに残しておいてください)
「サンプルコード内各定数のご説明」またはスクリプト内に記載の利用方法に沿って、設定が必要な定数を設定してください。

ご注意

特にアセットの数が多い場合は、出力対象のデータ量が多くなり、スクリプト実行後タイムアウトエラーになる可能性がございます。
その場合は定数の設定でキャンペーンID指定やインプレッション発生有無の条件付けをして、出力対象を絞っていただくことを推奨いたします。



/*
■スクリプト内容
レスポンシブ検索広告のアセットレポートを出力します。
(1) 全項目を行追記
スクリプト実行のたびに全項目を行追加で出力します。
ヘッダ行は「取得日、キャンペーンID、広告グループID、広告ID、キャンペーン名、広告グループ名、広告名、アセット、アセットタイプ、表示位置の固定、掲載評価、インプレッション数」
が初回に出力されます。
(2) 掲載評価を列追記
スクリプト実行のたびに掲載評価のみ列追加で出力します。
ヘッダ行は「キャンペーンID、広告グループID、広告ID、キャンペーン名、広告グループ名、広告名、アセット、アセットタイプ、表示位置の固定、広告配信状態」
が初回に出力されます。
■利用方法
1.当スクリプトを検索広告のスクリプトとしてください。
2.定数を以下のように設定してください。
■定数
・SPREAD_SHEET_ID  //スプレットシートIDを指定
・OUTPUT_LIST      //レポートパターン1全項目を行追記する形式のレポート出力有無(true:出力する、false:出力しない)
・SHEET_NAME_LIST //レポートパターン1全項目を行追記する形式のレポート出力をするスプレットシート名を指定
・OUTPUT_TREND     //レポートパターン2掲載評価を列追記する形式のレポート出力有無(true:出力する、false:出力しない)
・SHEET_NAME_TREND //レポートパターン2掲載評価を列追記する形式のレポート出力をするスプレットシート名を指定
・CAMPAIGN_IDS     //キャンペーンでフィルタする場合、キャンペーンIDを指定(例:['123456','234567'])、指定しない場合は全件となります
・HAS_IMPRESSIONS  //インプレッションが発生したアセットのみ抽出する場合はtrue、インプレッションなしも含める場合はfalse
・OUTPUT_ROW_LIMIT //レポートパターン2掲載評価を列追記する形式のレポートの追記する掲載評価列の最大数です。追加された列がこの数以上になると、古い列は削除されます。
・FLAG_MAIL        //結果をメール送信するならtrue、しないならfalse
・MAIL_TO          //メール送信先のYahoo! BusinessID
・MAIL_TITLE       //メールタイトル
・FLAG_SLACK       //結果をSlack送信するならtrue、しないならfalse
・SLACK_FETCH_URL    //SlackのWebhook URL
・SLACK_USER_NAME   //Slackのユーザー名
・SLACK_CHANNEL_NAME  //SlackのチャンネルID
■制限事項
・削除済のアセットも出力されます。
・追加・変更のあったアセットは、掲載評価推移が出力されるのは追加・変更された週の分からとなります。
*/
//設定が必要な定数
const SPREAD_SHEET_ID = 'スプレッドシートID';
const OUTPUT_LIST = true;
const SHEET_NAME_LIST = 'レポートパターン1を出力するスプレットシート名';
const OUTPUT_TREND = true;
const SHEET_NAME_TREND = 'レポートパターン2を出力するスプレットシート名';
const CAMPAIGN_IDS = [];
const HAS_IMPRESSIONS = true;
const OUTPUT_ROW_LIMIT = 4;//1ヶ月は基本4週なので初期値は4
const FLAG_MAIL = false;
const MAIL_TO = ['Yahoo! BusinessID'];
const MAIL_TITLE = '【Yahoo Ads Script】アセットレポート出力';
const FLAG_SLACK = false;
const SLACK_FETCH_URL = 'SlackのWebhook URL';
const SLACK_USER_NAME = 'Slackのユーザー名';
const SLACK_CHANNEL_NAME = 'SlackのチャンネルID';
//設定不要な定数
const accountId = AdsUtilities.getCurrentAccountId();
let TEXT_MESSAGE_ARRAY = [];
let globalErrorFlg = false;
const COL_REPORT = {
  CampaignId: 0,
  AdGroupId: 1,
  AdId: 2,
  CampaignName: 3,
  AdGroupName: 4,
  AdName: 5,
  AssetText: 6,
  AssetType: 7,
  PinedField: 8,
  Performance: 9,
  IMPS: 10
};
const COL_NO_TREND = {
  CampaignId: 0,
  AdGroupId: 1,
  AdId: 2,
  CampaignName: 3,
  AdGroupName: 4,
  AdName: 5,
  AssetText: 6,
  AssetType: 7,
  PinedField: 8,
  AdStatus: 9
};
const SEPARATER = ' ';//全角スペースは管理画面からは入らないので
const SS_LIST_HEADER = ['取得日', 'キャンペーンID', '広告グループID', '広告ID',
  'キャンペーン名', '広告グループ名', '広告名', 'アセット', 'アセットタイプ', '表示位置の固定', '掲載評価', 'インプレッション数'];
const SS_TREND_HEADER = ['キャンペーンID', '広告グループID', '広告ID',
  'キャンペーン名', '広告グループ名', '広告名', 'アセット', 'アセットタイプ', '表示位置の固定', '広告配信状態'];
function main() {
  try {
    if (!OUTPUT_LIST && !OUTPUT_TREND) {
      //ないとは思うが念のため
      logAndMessage('OUTPUT_LIST と OUTPUT_TREND が両方false(出力しない)になっています。どちらかをtrue(出力する)にしてください。');
      sendMailOrSlack();
      return;
    }
    //本日
    const todayString = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/M/d');
    //レポート取得
    let reportData = getReport();
    if (reportData.length == 0) {
      //ないとは思うが念のため
      logAndMessage('レポートの出力件数は0件でした。'
        + '\nスプレッドシートURL:https://docs.google.com/spreadsheets/d/' + SPREAD_SHEET_ID + '\n');
      sendMailOrSlack();
      return;
    }
    if (OUTPUT_LIST) {
      //毎週追記シートに出力
      outputSSList(reportData, todayString);
    }
    if (OUTPUT_TREND) {
      //掲載評価推移シートに出力
      outputSSTrend(reportData, todayString);
    }
    //通知
    logAndMessage('レポートの出力が完了しました。'
      + '\nスプレッドシートURL:https://docs.google.com/spreadsheets/d/' + SPREAD_SHEET_ID + '\n');
    sendMailOrSlack();
  } catch (error) {
    globalErrorFlg = true;
    logAndMessage('!!エラーが発生しています!!\n詳細はアカウントID:' + accountId + 'の管理画面のログをご確認ください。エラー内容:' + error
      + '\nスプレッドシートURL:https://docs.google.com/spreadsheets/d/' + SPREAD_SHEET_ID + '\n');
    sendMailOrSlack();
    throw error;//ログ上エラーにするため
  }
}
//レポート取得
function getReport() {
  let reportDefinition = {
    accountId: accountId,
    fields: ['CAMPAIGN_ID', 'ADGROUP_ID', 'AD_ID', 'CAMPAIGN_NAME', 'ADGROUP_NAME',
      'AD_NAME', 'ASSET_TEXT', 'ASSET_TYPE', 'PINNED_FIELD', 'PERFORMANCE', 'IMPS'],
    reportDateRangeType: 'LAST_7_DAYS',//本日を除く、過去7日間
    reportType: 'RESPONSIVE_ADS_FOR_SEARCH_ASSET',
    reportSkipColumnHeader: 'TRUE',//追記型なのでヘッダはなし
    reportSkipReportSummary: 'TRUE',//追記型なので小計行はなし
    reportIncludeDeleted: 'TRUE',//削除済のデータも出力
    sortFields: [ //広告内のIMPの降順。設定上限が5つなので同じIMPならアセットテキスト昇順に
      {
        field: "CAMPAIGN_ID",
        reportSortType: "ASC"
      },
      {
        field: "ADGROUP_ID",
        reportSortType: "ASC"
      },
      {
        field: "AD_ID",
        reportSortType: "ASC"
      },
      {
        field: "IMPS",
        reportSortType: "DESC"
      },
      {
        field: "ASSET_TEXT",
        reportSortType: "ASC"
      }
    ],
    filters: []
  };
  if (CAMPAIGN_IDS.length > 0) {
    reportDefinition.filters.push({
      field: 'CAMPAIGN_ID',
      filterOperator: 'IN',
      values: CAMPAIGN_IDS
    });
  }
  if (HAS_IMPRESSIONS) {
    reportDefinition.filters.push({
      field: 'IMPS',
      filterOperator: 'GREATER_THAN',
      values: ['0']
    });
  }
  const report = AdsUtilities.getSearchReport(reportDefinition).reports[0].rows;
  return report;
}
//毎週追記シートに出力
function outputSSList(reportData, todayString) {
  Logger.log('「' + SHEET_NAME_LIST + '」シートへの出力を開始します。');
  let sh = SpreadsheetApp.openById(SPREAD_SHEET_ID).getSheetByName(SHEET_NAME_LIST);
  //出力用にレポートデータに本日の日付を付与する
  let outputData = JSON.parse(JSON.stringify(reportData));//参照渡しだと元も変わるので新規obj作成
  outputData.forEach((array) => array.unshift(todayString));
  const lastRow = sh.getLastRow();//最終行
  if (lastRow == 0) {//0はじまり
    //初回はヘッダをつける
    outputData.unshift(SS_LIST_HEADER);
  }
  //スプシ出力
  sh.getRange(lastRow + 1, 1).setValues(outputData);
  logAndMessage(outputData.length + '行のデータを「' + SHEET_NAME_LIST + '」シートに出力しました。');
}
//掲載評価推移シートに出力
function outputSSTrend(reportData, todayString) {
  Logger.log('「' + SHEET_NAME_TREND + '」シートへの出力を開始します。');
  //既存データを取得
  let sh = SpreadsheetApp.openById(SPREAD_SHEET_ID).getSheetByName(SHEET_NAME_TREND);
  const kizonData = sh.getDataRange().getValues();
  //出力用データを生成
  let outputRowsArr = createOutputData(reportData, kizonData, todayString)
  //スプシ洗い替え
  sh.clear();
  sh.getRange(1, 1).setValues(outputRowsArr);
  logAndMessage(outputRowsArr.length - 1 + '行のデータを「' + SHEET_NAME_TREND + '」シートに出力しました。');//ヘッダ分引く
}
//既存レポートのMap作成
function createKizonMap(kizonReport) {
  let map = new Map();
  for (let i = 0; i < kizonReport.length; i++) {//ヘッダは飛ばす
    const row = kizonReport[i];
    const key = generateKey(row);
    map.set(key, row);//idxはずれるのでrowで
  }
  return map;
}
//出力用データ作成
function createOutputData(reportData, kizonData, todayString) {
  let outputRowsArr = [];
  const activeAdIds = createActiveAdIds();
  if (kizonData.length == 1) {//何もない配列としてlength1となる。データがある1行はヘッダなので問題なし
    //初回 
    let headerRow = Array.from(SS_TREND_HEADER);//元objに影響与えないよう再生成
    headerRow.push(todayString + '掲載評価');
    outputRowsArr.push(headerRow);
    let tempReportData = JSON.parse(JSON.stringify(reportData));//参照渡しだと元も変わるので新規obj作成
    for (let i = 0; i < tempReportData.length; i++) {
      const row = Array.from(tempReportData[i]);
      let outputRow = row.slice(COL_NO_TREND.CampaignId, -2);//配信状態列を差し込むのでIMPは消す
      outputRow.push(getStatusByAdId(row, activeAdIds));//配信状態を追加
      outputRow.push(row[COL_REPORT.Performance]);//掲載評価を追加
      outputRowsArr.push(outputRow);
    }
  } else {
    //2回目以降
    //比較用情報など生成
    const kizonMap = createKizonMap(kizonData);
    const addedColCnt = kizonData[0].length - Object.keys(COL_NO_TREND).length + 1;//現状ベースから何列追加になっているか
    const overColCnt = addedColCnt - OUTPUT_ROW_LIMIT;//現状上限を超過する列数
    const removeStartIdx = COL_NO_TREND.AdStatus + 1;//「配信状態」の次が最も古い掲載評価
    //ヘッダ分を最初に追加しておく
    let headerRow = Array.from(kizonData[0]);//既存のヘッダ.元objに影響与えないよう再生成
    headerRow.push(todayString + '掲載評価');//後ろにつける
    headerRow.splice(removeStartIdx, overColCnt);//上限超過した列は古いものから削除
    outputRowsArr.push(headerRow);
    //比較しながら出力用データ生成
    for (let i = 0; i < reportData.length; i++) {
      const newRow = reportData[i];
      const newKey = generateKey(newRow);
      //既存に行がある=掲載評価追加のみ。新規=行ごと追加
      let outputRow = kizonMap.has(newKey) ? kizonMap.get(newKey) : createNewRow(newRow, addedColCnt);
      //掲載評価を最後に調整
      outputRow.push(newRow[COL_REPORT.Performance]);
      outputRow.splice(COL_NO_TREND.AdStatus, 1, getStatusByAdId(newRow, activeAdIds));//配信状態列を更新
      outputRow.splice(removeStartIdx, overColCnt);//上限超過した列は古いものから削除
      outputRowsArr.push(outputRow);
    }
  }
  return outputRowsArr;
}
//{広告単位キー:配信状態}のMapを生成
function createActiveAdIds() {
  let counter = 0;
  let adValues = [];
  while (true) {
    const tempAds = Search.AdGroupAdService.get({
      accountId: accountId,
      numberResults: 10000,
      startIndex: counter * 10000 + 1,
      adTypes: ['RESPONSIVE_SEARCH_AD'],
      userStatuses: ['ACTIVE']//ACTIVEだけとってきて、ないものは停止判定
    }).rval;
    adValues = adValues.concat(tempAds.values);
    counter++;
    if (counter * 10000 >= tempAds.totalNumEntries) {
      break;
    }
  }
  let activeAdIds = [];
  for (let i = 0; i < adValues.length; i++) {
    const adGroupAd = adValues[i].adGroupAd;
    const adKey = adGroupAd.campaignId + SEPARATER + adGroupAd.adGroupId + SEPARATER + adGroupAd.adId;
    activeAdIds.push(adKey);
  }
  return activeAdIds;
}
//該当行の広告配信状態を取得
function getStatusByAdId(row, activeAdIds) {
  const rowKey = row[COL_NO_TREND.CampaignId] + SEPARATER + row[COL_NO_TREND.AdGroupId] +
    SEPARATER + row[COL_NO_TREND.AdId];
  //ACTIVEだけとってきたので、ないものは停止判定
  if (activeAdIds.includes(rowKey)) {
    return 'オン';
  } else {
    return 'オフ';
  }
}
//キー生成
function generateKey(row) {
  return row[COL_NO_TREND.CampaignId] + SEPARATER + row[COL_NO_TREND.AdGroupId] +
    SEPARATER + row[COL_NO_TREND.AdId] + SEPARATER + row[COL_NO_TREND.AssetType] +
    SEPARATER + row[COL_NO_TREND.AssetText];
}
//新しい行を生成
function createNewRow(newRow, addedColCnt) {
  let outputRow = Array.from(newRow.slice(COL_REPORT.CampaignId, -1));//IMPSは不要
  newRow.push('');//「配信状態」列は後で更新するので
  //既に列が追加されてる分をずらす(後で追加する1列分引く)
  for (let i = 0; i < addedColCnt - 1; i++) {
    outputRow.push('');
  }
  return outputRow;
}
//ログ、メールなどに流すテキスト
function logAndMessage(text) {
  Logger.log(text);
  TEXT_MESSAGE_ARRAY.push(text);
}
//メール、Slack、もしくはその両方に通知
function sendMailOrSlack() {
  //通知がfalseでもエラーフラグがtrueなら通知
  if (FLAG_MAIL || globalErrorFlg) {
    sendMail();
  }
  if (FLAG_SLACK) {
    sendSlack();
  }
}
//メール送信
function sendMail() {
  MailApp.sendEmail({
    to: MAIL_TO,
    subject: MAIL_TITLE,
    body: TEXT_MESSAGE_ARRAY.join('\n'),
  });
}
//Slackメッセージ送信
function sendSlack() {
  UrlFetchApp.fetch(SLACK_FETCH_URL,
    {
      method: 'POST',
      contentType: 'application/json',
      payload: {
        text: TEXT_MESSAGE_ARRAY.join('\n'),
        username: SLACK_USER_NAME,
        channel: SLACK_CHANNEL_NAME,
      }
    });
}