スプレッドシートを用いたキャンペーン/広告グループ/広告の配信設定(オン/オフ)変更

スクリプトの概要

Yahoo!広告スクリプトを用いてGoogleスプレッドシートと連携することにより、キャンペーン、広告グループ、広告の配信設定変更が可能となります。
本ページでは、Googleスプレッドシート上で1時間単位の配信開始日時と配信停止日時、名称(キャンペーン名、広告グループ名、広告名)を指定し、配信設定を切り替えるスクリプトを紹介します。
本スクリプトによる配信設定の切り替えが発生した場合は、メールとSlackでの通知も可能です。

※当スクリプトは検索広告・ディスプレイ広告共通版となっております。(設定されたアカウントが検索広告のものかディスプレイ広告のものか自動で判定されます)

ご利用の流れ

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

2.各エンティティ用のスプレッドシートのテンプレートをダウンロードします。
以下のテンプレートを開き、[ファイル] → [コピーを作成] を実行し、ご自身用のスプレッドシートを作成してください。
配信オンオフテンプレート
※広告用、広告グループ用、キャンペーン用の用途別にシートが分かれています

3.作成したスプレッドシートに設定内容を記載します。以下のとおり各項目の内容をご説明します。

項目 入力内容
キャンペーン名
広告グループ名
広告名
配信設定変更対象の名称を入力してください。
部分一致、または完全一致で対象を指定することが出来ます。
開始日付 配信設定を変更したい日付を入力してください。※YYYY/MM/DD形式
開始時間 配信設定を変更したい時間を入力してください。※HH:MM形式
開始時配信状態 変更したい配信設定を入力してください。
終了日付 配信設定を変更したい日付を入力してください。※YYYY/MM/DD形式
終了時間 配信設定を変更したい時間を入力してください。※HH:MM形式
終了時配信状態 変更したい配信設定を入力してください。
キャンペーンID 対象の広告または広告グループをキャンペーンIDで限定する場合に入力してください。
未入力の場合は、すべてのキャンペーンが対象になります。
広告グループID 対象の広告を広告グループIDで限定する場合に入力してください。
未入力の場合は、すべての広告グループが対象になります。
※終了日時未入力で開始日時だけでも動作します。
記載例

4.管理画面上のスクリプト作成画面にて、後述のサンプルコードを設定してください。
※スクリプトの新規作成および編集の手順はこちらをご覧ください。

5.管理画面上にてスクリプトの設定を完了後、スクリプトの実行頻度の設定をしてください。
本スクリプトでは「1時間ごと」に設定することを推奨いたします。
※スクリプトの実行頻度の設定手順はこちらをご覧ください。

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

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

・スプレッドシートの指定
スプレッドシートを指定します。

設定する定数 設定方法
const SPREAD_SHEET_ID = 'SPREAD_SHEET_ID'; スプレッドシートIDを '(半角シングルクオーテーション)の間に記載
const SHEET_NAME = 'SHEET_NAME'; シート名を '(半角シングルクオーテーション)の間に記載

・テストモードの指定
テストモードでは実際に配信設定の変更はされず動作の検証が行えます。
実際に配信設定を変更する時は、本番モードに変更してください。

設定する定数 設定方法
const TEST_EXECUTION = true; テストモードで実行するか記載
true:テストモード
false:本番モード
テストモードの場合、実行履歴のログに下図のように「テストモード」と記載され、対象のエンティティが一覧に記載されます。

・条件設定の指定
オンオフ対象のエンティティと、名称の抽出方法を指定してください。

設定する定数 設定方法
const ENTITY_TYPE = 'AD'; 対象のエンティティを記載
CAMPAIGN ・・・キャンペーン
ADGROUP ・・・広告グループ
AD ・・・広告
const MATCH_TYPE = 'BROAD'; 名称の抽出方法を記載
BROAD ・・・部分一致
EXACT ・・・完全一致

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

サンプルコード

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

/*
■スクリプト内容
スプレッドシートで指定した単語を含む名称のものを、指定開始日時に開始時配信状態にし、指定終了日時に終了配信状態にします。
■利用方法
1.当スクリプトを、検索広告またはディスプレイ広告のスクリプトとしてください。
2.各定数を以下のように設定してください。
■定数
・SPREAD_SHEET_ID:スプレッドシートID
・SPREAD_SHEET_NAME:スプレッドシート名
・TEST_EXECUTION:テスト実行時はtrue、本番実行時はfalse
・ENTITY_TYPE:実行するエンティティを設定。キャンペーン:CAMPAIGN、広告グループ:ADGROUP、広告:AD
・MATCH_TYPE:名称の検索方法を設定。完全一致:EXACT 部分一致:BROAD
*/
//設定が必要な定数
const SPREAD_SHEET_ID = 'SPREAD_SHEET_ID';
const SHEET_NAME = 'SHEET_NAME';
const TEST_EXECUTION = true; // テスト実行:true  本番実行:false
const ENTITY_TYPE = 'AD'; // CAMPAIGN ADGROUP AD
const MATCH_TYPE = 'BROAD'; //  完全一致:EXACT 部分一致:BROAD
const FLAG_SLACK = false;
const URL_FETCH_APP = 'SLACK_WEBHOOK_URL';
const FLAG_MAIL = true;
const MAIL_TO = ['Yahoo! JAPAN Business ID'];
const MAIL_TITLE = '配信設定オンオフスクリプト';
//設定が不要な定数(変更すると動かなくなります)
let PRODUCT_OBJ;
let MAX_NUMBER = 10000;
let TEXT_MESSAGE = '';
const accountId = AdsUtilities.getCurrentAccountId();
function main() {
  initServiceSetting();
  const sheetData = getSheetData();
  const operand =getSetOperand(sheetData);
  if(TEST_EXECUTION){
    Logger.log('テストモードで実行します');
    switchStatusText(operand);
  } else {
    switchStatus(operand);
  }
  validateAndSendMail();
  validateAndSendSlack();
}
function getSheetData(){
  const ss = validateAndGetSpreadsheet();
  const sh = validateAndGetSheet(ss);
  //スプレットシート情報を取得する
  let dataArray = sh.getDataRange().getValues();
  //もし2行以上の記載がなければ
  if (dataArray.length < 2) {
    throw new Error(SPREAD_SHEET_NAME + 'シートにオンオフの条件を記載してください');
  }
  const toDay = new Date(Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/MM/d H:00'));
  let sheetData = {
    'ACTIVE': [],
    'PAUSED': [],
  };
  for (let i = 1; i < dataArray.length; i++) {
    const row = createRowData(dataArray[i], toDay, i);
    if( row != null ){
      sheetData[row.targetStatus].push(row);
      Logger.log(i + '行目 名称:' +  row.targetName + ' 配信設定:' + ((row.targetStatus == 'ACTIVE') ? 'オン': 'オフ'));
    }
  }
  return sheetData;
}
function createRowData(dataRow, toDay, index){
  const targetName = dataRow[0];
  const campaignId = dataRow[7];
  const adGroupId = dataRow[8];
  if (targetName == '') return null;
  //開始日時と設定のチェック
  const startStatus = createSepRowData(dataRow[1], dataRow[2], dataRow[3], toDay, index);
  // 終了日時と設定をチェック
  const endStatus = createSepRowData(dataRow[4], dataRow[5], dataRow[6], toDay, index);
  // 有効な設定を反映
  const targetStatus = ( endStatus != null) ? endStatus : startStatus;
  return (targetStatus != null) ? {
    targetName: targetName,
    targetStatus: targetStatus,
    campaignId: campaignId,
    adGroupId: adGroupId,
  } : null;
}
function createSepRowData(date, time, status, toDay, index){
  if( date == '' && time == '' && status == '' ) return null;
  const targetStatus = isStatus(status, index);
  return isSameDate(toDay, date, time) ? targetStatus : null;
}
function isSameDate(toDay, date, time){
  const targetDate = new Date( date + ' ' + time );
  if(isNaN(targetDate.getDate())){
    throw new Error('日付の形式に間違いがあります。入力例 2024/04/01 12:00 実際に入力された値:' + date + ' ' + time);
  }
  return ( toDay.getFullYear() === targetDate.getFullYear() &&
      toDay.getMonth() === targetDate.getMonth() &&
      toDay.getDate() === targetDate.getDate() &&
      toDay.getHours() === targetDate.getHours() );
}
function getSetOperand(sheetData) {
  let operands = [];
  let num = 0;
  while (true) {
    const entitys = getDataFromGetService(num);
    for (let i = 0; i < entitys.values.length; i++) {
      const operand = getOperand(entitys.values[i]);
      if( chkName(operand, operand.userStatus, sheetData) ){
        operands.push(operand);
      }
    }
    num++;
    if (num * 10000 >= entitys.totalNumEntries) break;
  }
  return operands;
}
function chkName(operand, status, sheetData){
  const rows = ( status == 'ACTIVE' ) ? sheetData.ACTIVE : sheetData.PAUSED ;
  const name = getName(operand);
  for( const row of rows ){
    if( row.campaignId != '' && row.campaignId != operand.campaignId ) continue;
    if( row.adGroupId != '' && row.adGroupId != operand.adGroupId ) continue;
    switch (MATCH_TYPE){
      case 'BROAD':
        if( name.indexOf(row.targetName) > -1){
          return true;
        }
        break;
      case 'EXACT':
        if( name === row.targetName){
          return true;
        }
        break;
      default:
        throw new Error('MATCH_TYPEは、BROADまたはEXACTのいずれかを設定してください。');
    }
  }
  return false;
}
function getOperand( value ){
  let entity;
  let operand = {};
  switch (ENTITY_TYPE){
    case 'AD':
      entity = value.adGroupAd;
      operand.adId = entity.adId;
      operand.adGroupId = entity.adGroupId;
      operand.adName = entity.adName;
      break;
    case 'ADGROUP':
      entity = value.adGroup;
      operand.adGroupId = entity.adGroupId;
      operand.adGroupName = entity.adGroupName;
      break;
    case 'CAMPAIGN':
      entity = value.campaign;
      operand.campaignName = entity.campaignName;
      break;
  }
  operand.campaignId = entity.campaignId;
  operand.userStatus = ( entity.userStatus == 'ACTIVE' ) ? 'PAUSED':'ACTIVE';
  return operand;
}
function getDataFromGetService(num){
  let request = {
    accountId: accountId,
    numberResults: MAX_NUMBER,
    startIndex: num * MAX_NUMBER + 1,
  };
  return PRODUCT_OBJ.get(request).rval;
}
function switchStatus(operand){
  if(operand.length == 0){
    Logger.log('更新対象がありませんでした');
    return;
  }
  let num = 0;
  while (true) {
    const switchRes = PRODUCT_OBJ.set({
      accountId: accountId,
      operand: operand.slice(num * 2000, Math.min((num + 1) * 2000, operand.length)),
    }).rval;
    for (let i = 0; i < switchRes.values.length; i++) {
      if (switchRes.values[i].operationSucceeded) {
        const entity = operand[num * 2000 + i];
        const statusName = (entity.userStatus == 'ACTIVE')? 'オン' : 'オフ';
        logAndMessage(getName(entity) + 'を配信' + statusName + 'にしました');
      } else {
        logAndMessage(getName(entity) + 'の配信切替に失敗しました');
      }
    }
    num++;
    if (num * 2000 >= operand.length) break;
  }
}
function switchStatusText(operand){
  if(operand.length == 0){
    Logger.log('更新対象がありませんでした');
    return;
  }
  for (let i = 0; i < operand.length; i++) {
    const entity = operand[i];
    const status =  (entity.userStatus == 'ACTIVE') ? 'オン': 'オフ';
    logAndMessage(getName(entity) + 'を配信' + status + 'にしました');
  }
}
function initServiceSetting(){
  const productType = AdsUtilities.getProductType();
  if (productType == 'SEARCH') {
    PRODUCT_OBJ = getService(Search);
    Logger.log('検索広告のアカウントで実行します。対象は「' + getEntityName() + '」です');
  } else if (productType == 'DISPLAY') {
    PRODUCT_OBJ = getService(Display);
    if( ENTITY_TYPE == 'CAMPAIGN' ){
      MAX_NUMBER = 2000;
    }
    Logger.log('ディスプレイ広告のアカウントで実行します。対象は「' + getEntityName() + '」です');
  } else {
    throw new Error('検索広告またはディスプレイ広告のアカウントで実行してください。');
  }
}
function getService(ojb){
  switch (ENTITY_TYPE){
    case 'AD':
      return ojb.AdGroupAdService;
    case 'ADGROUP':
      return ojb.AdGroupService;
    case 'CAMPAIGN':
      return ojb.CampaignService;
    default:
      throw new Error('ENTITY_TYPEは、CAMPAIGN、ADGROUP、ADのいずれかを設定してください。');
  }
}
function isStatus(status, index){
  if (status == 'オン') {
    return 'ACTIVE';
  } else if (status == 'オフ') {
    return 'PAUSED';
  } else {
    throw new Error(index + '行目の配信設定に間違いがあります。 設定内容:' + status);
  }
}
function getName( operand ){
  switch (ENTITY_TYPE){
    case 'AD':
      return operand.adName;
    case 'ADGROUP':
      return operand.adGroupName;
    case 'CAMPAIGN':
      return operand.campaignName;
  }
}
function getEntityName(){
  switch (ENTITY_TYPE){
    case 'AD':
      return '広告';
    case 'ADGROUP':
      return '広告グループ';
    case 'CAMPAIGN':
      return 'キャンペーン';
  }
}
function validateAndGetSpreadsheet() {
  if (SPREAD_SHEET_ID === 'SPREAD_SHEET_ID' || SPREAD_SHEET_ID === '') {
    throw new Error('スプレッドシートIDを設定してください。');
  }
  try{
    return SpreadsheetApp.openById(SPREAD_SHEET_ID);
  } catch(e){
    throw new Error('スプレッドシートを開くことが出来ませんでした。スプレッドシートIDまたはスプレッドシートの権限が正しいか確認してください。' + e);
  }   
} 
function validateAndGetSheet(ss) {
  if (SHEET_NAME === '') {
    throw new Error('シート名を設定してください。');
  }
  const sh = ss.getSheetByName(SHEET_NAME);
  if(sh === null){
    throw new Error('シートが開けませんでした。シート名を確認してください。');
  }
  return sh;
}
function logAndMessage(text) {
  Logger.log(text);
  TEXT_MESSAGE += text + '\n';
}
function validateAndSendMail() {
  if (FLAG_MAIL && TEXT_MESSAGE.length > 0) {
    if (MAIL_TO.length < 1 || !MAIL_TO.every(str => typeof str === 'string' && /^[a-zA-Z0-9]+$/.test(str))) {
      throw new Error('Yahoo! JAPANビジネスIDを設定してください。メール送信前までに実行された処理は完了しています。');
    }
    MailApp.sendEmail({
      to: MAIL_TO,
      subject: MAIL_TITLE,
      body: TEXT_MESSAGE,
    });
  }
}
function validateAndSendSlack() {
  if (FLAG_SLACK && TEXT_MESSAGE.length > 0) {
    if (URL_FETCH_APP === 'SLACK_WEBHOOK_URL' || URL_FETCH_APP === '') {
      throw new Error('SlackのWebhook URLを設定してください。Slack送信前までに実行された処理は完了しています。');
    }
    UrlFetchApp.fetch(URL_FETCH_APP, {
      method: 'post',
      contentType: 'application/json',
      payload: JSON.stringify({
        text: TEXT_MESSAGE,
      }),
    });
  }
}