Keyword Labeler - Single Account

ツールアイコン

パフォーマンスの低いキーワードを無効にするなど、キーワードに大規模な変更を加えるのは大変な作業です。キーワード ラベラー スクリプトは、定義したルールに基づいてキーワードに自動的にラベルを適用することで、このタスクを簡素化します。ラベルを適用したら、Google 広告管理画面でキーワードを簡単にフィルタして、必要な変更を適用できます。また、別のスクリプトを使用してキーワードに変更を加えられます。

このスクリプトの使用例を次に示します。

  • 成果の低いキーワードにラベルを付けます。その後、Google 広告の管理画面でそれらのキーワードをフィルタし、入札単価を変更したり、一時停止または削除したりできます。
  • 商品の名前など、ブランドに関連する固有名を含むキーワードにラベルを付けます。後でこのラベルを使用してキーワードビューを分割し、掲載結果を詳しく分析できます。
  • 入札単価を変更するキーワードにラベルを付けます。その後、Google 広告管理画面でラベルを確認してから、一括編集で変更を加えられます。
  • すべてのキーワードに品質スコアのラベルを付けます。その後、現在の品質スコアが(元のラベルに反映された)以前の品質スコアと一致しないキーワードにラベルを付けて、確認できるようにします。
  • 上記の任意の組み合わせ。

このスクリプトは、新しいキーワードにラベルが付けられると、スプレッドシートへのリンクをメールで送信します。

仕組み

このスクリプトでは、次のフィールドを含む簡単な形式でルールを定義できます。

  • conditions: KeywordSelector 条件のリスト(省略可)。これらの条件により、たとえば、掲載結果の統計情報に基づいてキーワードを選択できます。
  • dateRange: 統計情報フィールドに基づいて選択する際に使用する KeywordSelector の期間(省略可)。省略した場合は、スクリプトのデフォルトの期間が使用されます。
  • filter: キーワードがルールに一致するかどうかをさらに定義するために、取得後に各キーワードに適用するオプションの関数。このフィルタを使用すると、conditions では表現できない特定のキーワード フィールドに対して、より複雑なロジックを表現できます。
  • labelName: ルールに一致し、まだそのラベルを持たないキーワードに適用する必須のラベル名。

ブランドに関連する成果の低いキーワードを特定してラベル付けするためのルールの例を次に示します。

{
  conditions: [
    'Ctr < 0.02',
    'AverageCpc > 1',
  ],
  dateRange: 'LAST_7_DAYS',
  filter: function(keyword) {
    const brands = ['Product A', 'Product B', 'Product C'];
    const text = keyword.getText();
    for (const i = 0; i < brands.length; i++) {
      if (text.indexOf(brand[i]) >= 0) {
        return true;
      }
    }
    return false;
  },
  labelName: 'Underperforming Branded'
}

このルールでは、キーワードのテキストに 3 つの商品ブランド名のうち少なくとも 1 つが含まれている場合、過去 7 日間のクリック率が 2% 未満、平均クリック単価が 1 ドルを超えるすべてのキーワードにラベルを付けます。

通常は、それ自体が有効な広告グループとキャンペーンに含まれる有効なキーワードにのみラベル付けします。このスクリプトでは、各ルールにこれらの条件を指定する代わりに、すべてのルールで使用されるグローバル条件を指定できます。

GLOBAL_CONDITIONS: [
  'campaign.status = ENABLED',
  'ad_group.status = ENABLED',
  'ad_group_criterion.status = ENABLED'
]

このスクリプトの中核は、ルールを取得し、グローバル条件とルール固有の条件を使用して KeywordSelector を作成し、結果として得られる各キーワードにフィルタを適用する関数です。完全な実装については、以下のスクリプトの getKeywordsForRule 関数をご覧ください。

このスクリプトは、ラベルを付けたすべてのキーワードをリストしたスプレッドシートへのリンクメールを送信します。後で Google 広告の管理画面にログインして、一時停止、削除、入札単価の変更など、キーワードの変更が必要かどうかを確認できます。

スケジュール設定

スクリプトを実行するかどうかと、その頻度は、ラベル付けの目標によって異なります。たとえば、パフォーマンスの低いキーワードにラベルを付ける場合は、パフォーマンスの低いキーワードをすばやく特定して修正できるように、スクリプトを毎週または毎日実行するスケジュールを設定できます。一方、スクリプトを使用してキーワードのラベル付けを伴う 1 回限りの作業を容易にする場合は、スクリプトを定期的に実行するようにスケジュールする必要はありません。

設定

  • 以下のソースコードを使って新しい Google 広告スクリプトを作成します。このテンプレート スプレッドシートのコピーを使用します。
  • RULES 配列を更新して、ラベル付けルールを表します。以下のソースコードは、ブランドに関連する成果の低いキーワードと、成果の低い一般的なキーワードにラベルを付ける例を示しています。
  • スクリプトの SPREADSHEET_URLRECIPIENT_EMAILS を更新します。

ソースコード

// Copyright 2015, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @name Keyword Labeler
 *
 * @overview The Keyword Labeler script labels keywords based on rules that
 *     you define. For example, you can create a rule to label keywords that
 *     are underperforming. Later, you can filter for this label in Google Ads
 *     to decide whether to pause or remove those keywords. Rules don't have
 *     to be based solely on a keyword's performance. They can also be based
 *     on properties of a keyword such as its text or match type. For example,
 *     you could define "branded" keywords as those containing proper nouns
 *     associated with your brand, then label those keywords based on
 *     different performance thresholds versus "non-branded" keywords.
 *     Finally, the script sends an email linking to a spreadsheet when new
 *     keywords have been labeled. See
 *     https://developers.google.com/google-ads/scripts/docs/solutions/labels
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 2.0
 *
 * @changelog
 * - version 2.0
 *   - Updated to use new Google Ads scripts features.
 * - version 1.1.2
 *   - Added validation for external spreadsheet setup.
 * - version 1.1.1
 *   - Improvements to time zone handling.
 * - version 1.1
 *   - Modified to allow generic rules and labeling.
 * - version 1.0
 *   - Released initial version.
 */

const CONFIG = {
  // URL of the spreadsheet template.
  // This should be a copy of https://goo.gl/opviBb.
  SPREADSHEET_URL: 'YOUR_SPREADSHEET_URL',

  // Array of addresses to be alerted via email if labels are applied.
  RECIPIENT_EMAILS: [
    'YOUR_EMAIL_HERE'
  ],

  // Selector conditions to apply for all rules.
  GLOBAL_CONDITIONS: [
    'campaign.status = ENABLED',
    'ad_group.status = ENABLED',
    'ad_group_criterion.status = ENABLED'
  ],

  // Default date range over which statistics fields are retrieved.
  // Used when fetching keywords if a rule doesn't specify a date range.
  DEFAULT_DATE_RANGE: 'LAST_7_DAYS'
};

/**
 * Defines the rules by which keywords will be labeled.
 * The labelName field is required. Other fields may be null.
 * @type {Array.<{
 *     conditions: Array.<string>,
 *     dateRange: string,
 *     filter: function(Object): boolean,
 *     labelName: string,
 *   }>
 * }
 */
const RULES = [
  {
    conditions: [
      'metrics.ctr < 0.02',
      'metrics.average_cpc > 1',
    ],
    filter: function(keyword) {
      const brands = ['Product A', 'Product B', 'Product C'];
      const text = keyword.getText();
      for (const brand of brands) {
        if(text.indexOf(brand) >= 0) {
          return true;
        }
      }
      return false;
    },
    labelName: 'Underperforming Branded'
  },

  {
    conditions: [
      'metrics.ctr < 0.01',
      'metrics.average_cpc > 2',
    ],
    labelName: 'Underperforming'
  }
];

function main() {
  validateEmailAddresses();
  const results = processAccount();
  processResults(results);
}

/**
 * Processes the rules on the current account.
 *
 * @return {Array.<Object>} An array of changes made, each having
 *     a customerId, campaign name, ad group name, label name,
 *     and keyword text that the label was applied to.
 */
function processAccount() {
  ensureAccountLabels();
  const changes = applyLabels();

  return changes;
}

/**
 * Processes the results of the script.
 *
 * @param {Array.<Object>} changes An array of changes made, each having
 *     a customerId, campaign name, ad group name, label name,
 *     and keyword text that the label was applied to.
 */
function processResults(changes) {
  if (changes.length > 0) {
    const spreadsheetUrl = saveToSpreadsheet(changes, CONFIG.RECIPIENT_EMAILS);
    sendEmail(spreadsheetUrl, CONFIG.RECIPIENT_EMAILS);
  } else {
    console.log('No labels were applied.');
  }
}

/**
 * Retrieves the names of all labels in the account.
 *
 * @return {Array.<string>} An array of label names.
 */
function getAccountLabelNames() {
  const labelNames = [];

  for (const label of AdsApp.labels()) {
    labelNames.push(label.getName());
  }

  return labelNames;
}

/**
 * Checks that the account has a label for each rule and
 * creates the rule's label if it does not already exist.
 * Throws an exception if a rule does not have a labelName.
 */
function ensureAccountLabels() {
  const labelNames = getAccountLabelNames();

  for (var i = 0; i < RULES.length; i++) {
    const labelName = RULES[i].labelName;

    if (!labelName) {
      throw `Missing labelName for rule #${i}`;
    }

    if (labelNames.indexOf(labelName) == -1) {
      AdsApp.createLabel(labelName);
      labelNames.push(labelName);
    }
  }
}

/**
 * Retrieves the keywords in an account satisfying a rule
 * and that do not already have the rule's label.
 *
 * @param {Object} rule An element of the RULES array.
 * @return {Array.<Object>} An array of keywords.
 */
function getKeywordsForRule(rule) {
  let selector = AdsApp.keywords();
  let includedLabel;

  // Add global conditions.
  for (const globalCondition of CONFIG.GLOBAL_CONDITIONS) {
    selector = selector.withCondition(globalCondition);
  }

  // Add selector conditions for this rule.
  if (rule.conditions) {
    for (const ruleCondition of rule.conditions) {
      selector = selector.withCondition(ruleCondition);
    }
  }

  if(rule.labelName){
    includedLabel = AdsApp.labels()
        .withCondition(`label.name = '${rule.labelName}'`)
        .get();
    if(includedLabel.hasNext()){
      includedLabel=includedLabel.next();
    }
  }

  // Exclude keywords that already have the label.
  selector.withCondition(`LabelNames CONTAINS_NONE ["${rule.labelName}"]`);

  // Add a date range.
  selector = selector.forDateRange(rule.dateRange || CONFIG.DEFAULT_DATE_RANGE);

  // Get the keywords.
  const iterator = selector.get();
  const keywords = [];

  // Check filter conditions for this rule.
  for (const keyword of iterator) {
    if (!rule.filter || rule.filter(keyword)) {
      keywords.push(keyword);
    }
  }

  return keywords;
}

/**
 * For each rule, determines the keywords matching the rule and which
 * need to have a label newly applied, and applies it.
 *
 * @return {Array.<Object>} An array of changes made, each having
 *     a customerId, campaign name, ad group name, label name,
 *     and keyword text that the label was applied to.
 */
function applyLabels() {
  const changes = [];
  const customerId = AdsApp.currentAccount().getCustomerId();

  for (const rule of RULES) {
    const keywords = getKeywordsForRule(rule);
    const labelName = rule.labelName;

    for (const keyword of keywords) {

      keyword.applyLabel(labelName);

      changes.push({
        customerId: customerId,
        campaignName: keyword.getCampaign().getName(),
        adGroupName: keyword.getAdGroup().getName(),
        labelName: labelName,
        keywordText: keyword.getText(),
      });
    }
  }

  return changes;
}

/**
 * Outputs a list of applied labels to a new spreadsheet and gives editor access
 * to a list of provided emails.
 *
 * @param {Array.<Object>} changes An array of changes made, each having
 *     a customerId, campaign name, ad group name, label name,
 *     and keyword text that the label was applied to.
 * @param {Array.<Object>} emails An array of email addresses.
 * @return {string} The URL of the spreadsheet.
 */
function saveToSpreadsheet(changes, emails) {
  const template = validateAndGetSpreadsheet(CONFIG.SPREADSHEET_URL);
  const spreadsheet = template.copy('Keyword Labels Applied');

  // Make sure the spreadsheet is using the account's timezone.
  spreadsheet.setSpreadsheetTimeZone(AdsApp.currentAccount().getTimeZone());

  console.log(`Saving changes to spreadsheet at ${spreadsheet.getUrl()}`);

  const headers = spreadsheet.getRangeByName('Headers');
  const outputRange = headers.offset(1, 0, changes.length);

  const outputValues = [];
  for (const change of changes) {
    outputValues.push([
      change.customerId,
      change.campaignName,
      change.adGroupName,
      change.keywordText,
      change.labelName
    ]);
  }
  outputRange.setValues(outputValues);

  spreadsheet.getRangeByName('RunDate').setValue(new Date());

  for (const email of emails) {
    spreadsheet.addEditor(email);
  }

  return spreadsheet.getUrl();
}

/**
 * Sends an email to a list of email addresses with a link to a spreadsheet.
 *
 * @param {string} spreadsheetUrl The URL of the spreadsheet.
 * @param {Array.<Object>} emails An array of email addresses.
 */
function sendEmail(spreadsheetUrl, emails) {
  MailApp.sendEmail(emails.join(','), 'Keywords Newly Labeled',
      `Keywords have been newly labeled in your` +
      `Google Ads account(s). See ` +
      `${spreadsheetUrl} for details.`);
}

/**
 * DO NOT EDIT ANYTHING BELOW THIS LINE.
 * Please modify your spreadsheet URL and email addresses at the top of the file
 * only.
 */

/**
 * Validates the provided spreadsheet URL and email address
 * to make sure that they're set up properly. Throws a descriptive error message
 * if validation fails.
 *
 * @param {string} spreadsheeturl The URL of the spreadsheet to open.
 * @return {Spreadsheet} The spreadsheet object itself, fetched from the URL.
 * @throws {Error} If the spreadsheet URL or email hasn't been set
 */
function validateAndGetSpreadsheet(spreadsheeturl) {
  if (spreadsheeturl == 'YOUR_SPREADSHEET_URL') {
    throw new Error('Please specify a valid Spreadsheet URL. You can find' +
        ' a link to a template in the associated guide for this script.');
  }
  return SpreadsheetApp.openByUrl(spreadsheeturl);
}

/**
 * Validates the provided email address to make sure it's not the default.
 * Throws a descriptive error message if validation fails.
 *
 * @throws {Error} If the list of email addresses is still the default
 */
function validateEmailAddresses() {
  if (CONFIG.RECIPIENT_EMAILS &&
      CONFIG.RECIPIENT_EMAILS[0] == 'YOUR_EMAIL_HERE') {
    throw new Error('Please specify a valid email address.');
  }
}