关键字标记器 - 单一账号

工具图标

对关键字进行大规模更改(例如停用效果不佳的关键字)可能非常繁琐。关键字标签器脚本会根据您定义的规则自动向关键字应用标签,从而简化此任务。应用标签后,您可以在 Google Ads 界面中轻松过滤关键字并应用所需的更改,也可以使用其他脚本对关键字进行更改。

下面列举了一些示例来说明如何使用此脚本:

  • 标记近期效果不佳的关键字。然后,您可以在 Google Ads 界面中过滤出这些关键字,并修改其出价,或暂停/移除它们。
  • 为包含与您的品牌相关联的专有名称(例如您的某件产品的名称)的关键字添加标签。稍后您可以使用此标签来细分关键字视图,以便更好地了解效果。
  • 标记您打算更改出价的关键字。然后,您可以在 Google Ads 界面中查看这些标签,再使用批量修改实际进行更改。
  • 用质量得分标记所有关键字。然后,为当前质量得分与之前的质量得分不一致(如原始标签中反映)的关键字添加标签,以便查看。
  • 以上各项的任意组合。

为新的关键字添加标签后,脚本会发送指向电子表格的电子邮件链接。

运作方式

在该脚本中,您可以使用简单的格式通过以下字段定义规则:

  • conditionsKeywordSelector 条件的可选列表。例如,这些条件可以根据效果统计信息选择关键字。
  • 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'
}

此规则将标记在过去 7 天内点击率低于 2%、平均每次点击费用高于 $1 的所有关键字,前提是关键字文字至少包含三个产品品牌名称中的一个。

通常情况下,您只需关注为已启用的关键字添加标签,以及位于已启用的广告组和广告系列中的关键字。该脚本允许您提供每个规则都会使用的全局条件,而不是在每条规则中指定这些条件:

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

该脚本的核心是一个函数,该函数接受规则,使用全局条件和规则专用条件构建 KeywordSelector,并将过滤条件应用于生成的每个关键字。如需了解完整实现,请参阅以下脚本中的 getKeywordsForRule 函数。

该脚本会发送一封包含电子表格链接的电子邮件,该电子表格列出了其标记的所有关键字。稍后,您可以登录 Google Ads 界面,看看是否要修改这些关键字,例如暂停、移除它们或修改其出价。

正在安排

是否安排脚本运行以及多久运行一次取决于您的标签目标。例如,如果您要标记效果不佳的关键字,则可能需要安排脚本每周甚至每天运行,以便快速找出所有效果不佳的关键字并采取相应的补救措施。另一方面,如果您使用该脚本来简化涉及为关键字添加标签的一次性工作,则无需安排该脚本定期运行。

初始设置

  • 使用下面的源代码创建新的 Google Ads 脚本。使用此电子表格模板的副本。
  • 更新 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.');
  }
}