mcpField
G
google-ads-scripts

google-ads-scripts

github.com/cognyai营销与增长11149 次安装2 周前更新
代码仓库

关于 google-ads-scripts

google-ads-scripts 是面向 Google Ads 脚本编写者的 JavaScript 参考技能,涵盖 AdsApp、GAQL、常见自动化模式及 Google Sheets 集成,并提供编写、调试和优化脚本的指导。

主要功能

  1. 01讲解 AdsApp 对象模型与选择器
  2. 02提供 GAQL 查询模式
  3. 03涵盖账户与 MCC 自动化
  4. 04展示 Google Sheets 和邮件集成
  5. 05给出错误处理与安全实践

适用场景

  1. 01编写预算节奏监控脚本
  2. 02挖掘搜索词并生成否定关键词
  3. 03跟踪质量得分并写入 Sheets
  4. 04排查脚本错误或优化执行

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/cognyai/claude-code-marketing-skills --skill google-ads-scripts

安装后在 Claude Code 中输入/google-ads-scripts确认已加载。

详细介绍

Google Ads Scripts 参考

Google Ads Scripts 完整参考:Google Ads 中的 JavaScript 自动化、AdsApp 对象模型、选择器和迭代器、GAQL 查询、包含可运行代码的常见脚本模式、Google Sheets 集成、MCC 并行处理以及最佳实践。

完整文档:https://cogny.com/docs/google-ads-scripts

用法

/google-ads-scripts                          # Full overview
/google-ads-scripts budget pacing            # Budget pacing monitor script
/google-ads-scripts negative keywords        # Search term negative keyword miner
/google-ads-scripts quality score tracker    # Quality score logging to Sheets
/google-ads-scripts MCC parallel             # MCC parallel processing pattern
/google-ads-scripts selectors                # Selector and iterator patterns
/google-ads-scripts GAQL                     # GAQL queries in scripts
/google-ads-scripts anomaly detection        # Anomaly alerting script

说明

你是 Google Ads Scripts 专家。请使用此参考帮助用户编写、调试和优化 Google Ads Scripts。提供完整、可运行的代码,并采用恰当的错误处理和最佳实践。

用户提问时,请查找下方相关章节,并提供准确、可执行的答案及可直接使用的 JavaScript 代码。

如果用户提供了具体主题作为参数,请重点介绍该领域。否则,请概述功能和常见模式。

关键原则:

  • 在会修改账户的脚本中始终加入 DRY_RUN 标志
  • 执行更改前先记录日志
  • 使用 try/catch 处理错误
  • 对复杂查询优先使用 GAQL(Google Ads Query Language)
  • 批量写入 Google Sheets 以提升性能
  • 提醒用户执行时限为 30 分钟(MCC 为 60 分钟)

概述

Google Ads Scripts 可让你使用 JavaScript 以编程方式控制 Google Ads。脚本直接在 Google Ads 网页界面中运行,无需外部服务、API 密钥或 OAuth。

主要功能:

  • 读取和修改广告系列、广告组、广告、关键字和附加信息
  • 使用 GAQL(Google Ads Query Language)查询效果数据
  • 集成 Google Sheets 以创建信息中心和记录日志
  • 通过 MailApp 发送电子邮件提醒
  • 通过 UrlFetchApp 发起 HTTP 请求
  • 按计划运行(每小时、每天、每周、每月)

单账户脚本与 MCC 脚本:

功能单账户MCC 脚本
范围一个账户MCC 下的所有账户
入口点main()使用 AdsManagerApp 的 main()
执行时限30 分钟60 分钟
并行执行否是,executeInParallel()

**语言:**JavaScript ES5,并支持部分 ES6。可使用 let、const、箭头函数和模板字面量。不支持 async/await、import/export 和 class。

AdsApp 对象模型

AdsApp 对象是所有单账户操作的根对象。

实体层级
AdsApp
  +-- campaigns()
  |     +-- adGroups()
  |     |     +-- ads()
  |     |     +-- keywords()
  |     |     +-- audiences()
  |     +-- extensions()
  +-- adGroups()        (account-level shortcut)
  +-- ads()             (account-level shortcut)
  +-- keywords()        (account-level shortcut)
  +-- negativeKeywords()
  +-- shoppingCampaigns()
  +-- videoCampaigns()
  +-- labels()
  +-- budgets()
  +-- biddingStrategies()
常见实体方法
// Status
entity.isEnabled()
entity.isPaused()
entity.isRemoved()
entity.enable()
entity.pause()
entity.remove()

// Naming
entity.getName()
entity.setName('New Name')

// Stats (requires date range)
entity.getStatsFor('LAST_30_DAYS')
entity.getStatsFor('20250101', '20250131')

// Stats object methods
var stats = entity.getStatsFor('LAST_30_DAYS');
stats.getImpressions()
stats.getClicks()
stats.getCtr()
stats.getAverageCpc()
stats.getCost()
stats.getConversions()
stats.getConversionRate()

选择器和迭代器

每个实体集合都采用选择器-迭代器模式,这是每个脚本中最基本的数据访问模式。

选择器方法
var keywords = AdsApp.keywords()
  .withCondition('Status = ENABLED')
  .withCondition('CampaignStatus = ENABLED')
  .withCondition('AdGroupStatus = ENABLED')
  .withCondition('Ctr < 0.01')
  .forDateRange('LAST_30_DAYS')
  .orderBy('Impressions DESC')
  .withLimit(100)
  .get();

.withCondition(condition) — 筛选实体。运算符:=、!=、>、<、>=、<=、CONTAINS、DOES_NOT_CONTAIN、STARTS_WITH、CONTAINS_IGNORE_CASE、REGEXP_MATCH、IN []。

.withCondition('Name CONTAINS "brand"')
.withCondition('Name REGEXP_MATCH "^(buy|shop|order).*"')
.withCondition('QualityScore > 5')
.withCondition('LabelNames CONTAINS_ANY ["Priority", "Monitor"]')
.withCondition('CampaignName IN ["Search - Brand", "Search - Generic"]')

.forDateRange(dateRange) — 基于指标的条件必须使用此方法。预定义范围:TODAY、YESTERDAY、LAST_7_DAYS、THIS_WEEK_SUN_TODAY、THIS_WEEK_MON_TODAY、LAST_WEEK、LAST_14_DAYS、LAST_30_DAYS、LAST_BUSINESS_WEEK、LAST_WEEK_SUN_SAT、THIS_MONTH、LAST_MONTH、ALL_TIME。

// Custom date range
.forDateRange('20250101', '20250131')

.orderBy(orderSpec) — 排序结果(ASC / DESC)。

.withLimit(limit) — 限制结果数量。

迭代器模式
var iterator = AdsApp.keywords()
  .withCondition('Status = ENABLED')
  .forDateRange('LAST_7_DAYS')
  .withCondition('Impressions > 100')
  .get();

while (iterator.hasNext()) {
  var keyword = iterator.next();
  var stats = keyword.getStatsFor('LAST_7_DAYS');
  Logger.log(keyword.getText() + ': ' + stats.getClicks() + ' clicks');
}

脚本中的 GAQL

Google Ads Query Language 可让你在脚本中以类似 SQL 的方式访问完整的 Google Ads API。

AdsApp.search(query)

返回一个迭代器,其中包含作为 JavaScript 对象的结果行。

function main() {
  var query = "SELECT " +
    "campaign.name, " +
    "campaign.status, " +
    "metrics.impressions, " +
    "metrics.clicks, " +
    "metrics.cost_micros, " +
    "metrics.conversions " +
    "FROM campaign " +
    "WHERE campaign.status = 'ENABLED' " +
    "AND segments.date DURING LAST_30_DAYS " +
    "ORDER BY metrics.cost_micros DESC";

  var results = AdsApp.search(query);

  while (results.hasNext()) {
    var row = results.next();
    Logger.log(row.campaign.name +
      ' | Cost: $' + (row.metrics.costMicros / 1000000).toFixed(2) +
      ' | Conversions: ' + row.metrics.conversions);
  }
}
AdsApp.report(query)

返回具有导出功能的报告对象,最适合批量数据处理和 Sheets 集成。

var report = AdsApp.report(
  "SELECT " +
  "ad_group_criterion.keyword.text, " +
  "ad_group_criterion.quality_info.quality_score, " +
  "metrics.impressions, " +
  "metrics.clicks, " +
  "metrics.conversions " +
  "FROM keyword_view " +
  "WHERE campaign.status = 'ENABLED' " +
  "AND segments.date DURING LAST_30_DAYS"
);

// Export directly to Google Sheets
var sheet = SpreadsheetApp.openByUrl('YOUR_SHEET_URL').getActiveSheet();
report.exportToSheet(sheet);
Search 与 Report 的比较
功能AdsApp.search()AdsApp.report()
返回类型行迭代器报告对象
字段访问row.campaign.name行迭代器或 exportToSheet()
导出到表格手动内置 exportToSheet()
最适用于逐行处理批量导出、信息中心

常见脚本模式

1. 暂停效果不佳的关键字
function main() {
  var CONFIG = {
    MIN_QUALITY_SCORE: 4,
    MIN_CTR: 0.005,
    MIN_IMPRESSIONS: 100,
    DATE_RANGE: 'LAST_30_DAYS',
    DRY_RUN: true
  };

  var query = "SELECT " +
    "ad_group_criterion.keyword.text, " +
    "ad_group_criterion.keyword.match_type, " +
    "ad_group_criterion.quality_info.quality_score, " +
    "ad_group.name, " +
    "campaign.name, " +
    "metrics.impressions, " +
    "metrics.clicks, " +
    "metrics.ctr, " +
    "metrics.cost_micros, " +
    "metrics.conversions " +
    "FROM keyword_view " +
    "WHERE campaign.status = 'ENABLED' " +
    "AND ad_group.status = 'ENABLED' " +
    "AND ad_group_criterion.status = 'ENABLED' " +
    "AND metrics.impressions > " + CONFIG.MIN_IMPRESSIONS + " " +
    "AND segments.date DURING " + CONFIG.DATE_RANGE;

  var results = AdsApp.search(query);
  var pauseCount = 0;

  while (results.hasNext()) {
    var row = results.next();
    var qs = row.adGroupCriterion.qualityInfo.qualityScore;
    var ctr = row.metrics.ctr;

    if (qs < CONFIG.MIN_QUALITY_SCORE || ctr < CONFIG.MIN_CTR) {
      Logger.log('PAUSE: "' + row.adGroupCriterion.keyword.text + '"' +
        ' | QS: ' + qs + ' | CTR: ' + (ctr * 100).toFixed(2) + '%' +
        ' | Campaign: ' + row.campaign.name);

      if (!CONFIG.DRY_RUN) {
        var keywords = AdsApp.keywords()
          .withCondition('Text = "' + row.adGroupCriterion.keyword.text + '"')
          .withCondition('AdGroupName = "' + row.adGroup.name + '"')
          .withCondition('CampaignName = "' + row.campaign.name + '"')
          .get();
        if (keywords.hasNext()) keywords.next().pause();
      }
      pauseCount++;
    }
  }

  Logger.log('Total keywords to pause: ' + pauseCount);
  if (CONFIG.DRY_RUN) Logger.log('DRY RUN — no changes applied.');
}
2. 预算消耗进度监控
function main() {
  var CONFIG = {
    MONTHLY_BUDGET: 10000,
    ALERT_THRESHOLD_OVER: 0.15,
    ALERT_THRESHOLD_UNDER: 0.20,
    ALERT_EMAIL: '[email protected]',
    CAMPAIGN_LABEL: 'Budget-Monitor'
  };

  var today = new Date();
  var daysInMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
  var dayOfMonth = today.getDate();
  var expectedSpend = (CONFIG.MONTHLY_BUDGET / daysInMonth) * dayOfMonth;

  var firstOfMonth = Utilities.formatDate(
    new Date(today.getFullYear(), today.getMonth(), 1), 'UTC', 'yyyyMMdd');
  var todayStr = Utilities.formatDate(today, 'UTC', 'yyyyMMdd');

  var campaigns = AdsApp.campaigns()
    .withCondition('Status = ENABLED')
    .withCondition('LabelNames CONTAINS_ANY ["' + CONFIG.CAMPAIGN_LABEL + '"]')
    .forDateRange(firstOfMonth, todayStr)
    .get();

  var totalSpend = 0;
  var campaignDetails = [];

  while (campaigns.hasNext()) {
    var campaign = campaigns.next();
    var stats = campaign.getStatsFor(firstOfMonth, todayStr);
    var spend = stats.getCost();
    totalSpend += spend;
    campaignDetails.push({
      name: campaign.getName(),
      spend: spend,
      conversions: stats.getConversions()
    });
  }

  var paceRatio = totalSpend / expectedSpend;
  var projectedSpend = (totalSpend / dayOfMonth) * daysInMonth;
  var status = 'ON PACE';

  if (paceRatio > (1 + CONFIG.ALERT_THRESHOLD_OVER)) status = 'OVERSPENDING';
  else if (paceRatio < (1 - CONFIG.ALERT_THRESHOLD_UNDER)) status = 'UNDERSPENDING';

  Logger.log('Day ' + dayOfMonth + '/' + daysInMonth +
    ' | Expected: $' + expectedSpend.toFixed(2) +
    ' | Actual: $' + totalSpend.toFixed(2) +
    ' | Projected: $' + projectedSpend.toFixed(2) +
    ' | Status: ' + status);

  if (status !== 'ON PACE') {
    var body = 'Budget Pacing Alert\n\nStatus: ' + status +
      '\nDay ' + dayOfMonth + ' of ' + daysInMonth +
      '\nMonthly budget: $' + CONFIG.MONTHLY_BUDGET.toFixed(2) +
      '\nExpected spend: $' + expectedSpend.toFixed(2) +
      '\nActual spend: $' + totalSpend.toFixed(2) +
      '\nProjected: $' + projectedSpend.toFixed(2);

    MailApp.sendEmail(CONFIG.ALERT_EMAIL, '[Google Ads] Budget Alert: ' + status, body);
  }
}
3. 搜索字词否定关键词挖掘工具
function main() {
  var CONFIG = {
    MIN_COST: 50,
    MAX_CONVERSIONS: 0,
    MIN_CLICKS: 10,
    DATE_RANGE: 'LAST_30_DAYS',
    NEGATIVE_MATCH_TYPE: 'EXACT',
    DRY_RUN: true,
    LOG_SHEET_URL: ''
  };

  var query = "SELECT " +
    "search_term_view.search_term, " +
    "campaign.name, " +
    "ad_group.name, " +
    "metrics.impressions, " +
    "metrics.clicks, " +
    "metrics.cost_micros, " +
    "metrics.conversions " +
    "FROM search_term_view " +
    "WHERE campaign.status = 'ENABLED' " +
    "AND metrics.cost_micros > " + (CONFIG.MIN_COST * 1000000) + " " +
    "AND metrics.clicks > " + CONFIG.MIN_CLICKS + " " +
    "AND metrics.conversions <= " + CONFIG.MAX_CONVERSIONS + " " +
    "AND segments.date DURING " + CONFIG.DATE_RANGE;

  var results = AdsApp.search(query);
  var negatives = [];

  while (results.hasNext()) {
    var row = results.next();
    var term = row.searchTermView.searchTerm;
    var cost = row.metrics.costMicros / 1000000;

    negatives.push({ term: term, campaign: row.campaign.name, cost: cost });

    Logger.log('NEGATIVE: "' + term + '" | Cost: $' + cost.toFixed(2) +
      ' | Clicks: ' + row.metrics.clicks + ' | Campaign: ' + row.campaign.name);

    if (!CONFIG.DRY_RUN) {
      var campaigns = AdsApp.campaigns()
        .withCondition('Name = "' + row.campaign.name + '"').get();
      if (campaigns.hasNext()) {
        var campaign = campaigns.next();
        if (CONFIG.NEGATIVE_MATCH_TYPE === 'EXACT') {
          campaign.createNegativeKeyword('[' + term + ']');
        } else if (CONFIG.NEGATIVE_MATCH_TYPE === 'PHRASE') {
          campaign.createNegativeKeyword('"' + term + '"');
        } else {
          campaign.createNegativeKeyword(term);
        }
      }
    }
  }

  Logger.log('Total negative keyword candidates: ' + negatives.length);
}
4. 失效 URL 检查器
function main() {
  var CONFIG = {
    ALERT_EMAIL: '[email protected]',
    PAUSE_BROKEN: false,
    MIN_IMPRESSIONS: 10,
    DATE_RANGE: 'LAST_7_DAYS'
  };

  var brokenUrls = [];

  var ads = AdsApp.ads()
    .withCondition('Status = ENABLED')
    .withCondition('CampaignStatus = ENABLED')
    .withCondition('AdGroupStatus = ENABLED')
    .forDateRange(CONFIG.DATE_RANGE)
    .withCondition('Impressions > ' + CONFIG.MIN_IMPRESSIONS)
    .get();

  while (ads.hasNext()) {
    var ad = ads.next();
    var url = ad.urls().getFinalUrl();
    if (url) {
      var status = checkUrl(url);
      if (status !== 200) {
        brokenUrls.push({
          campaign: ad.getCampaign().getName(),
          adGroup: ad.getAdGroup().getName(),
          url: url,
          httpStatus: status
        });
        Logger.log('BROKEN [' + status + ']: ' + url);
        if (CONFIG.PAUSE_BROKEN) ad.pause();
      }
    }
  }

  if (brokenUrls.length > 0 && CONFIG.ALERT_EMAIL) {
    var body = 'Broken URL Report\n\n';
    for (var i = 0; i < brokenUrls.length; i++) {
      body += 'HTTP ' + brokenUrls[i].httpStatus + ' | ' +
        brokenUrls[i].campaign + ' | ' + brokenUrls[i].url + '\n';
    }
    MailApp.sendEmail(CONFIG.ALERT_EMAIL,
      '[Google Ads] ' + brokenUrls.length + ' Broken URLs', body);
  }
}

function checkUrl(url) {
  try {
    var response = UrlFetchApp.fetch(url, {
      muteHttpExceptions: true,
      followRedirects: true,
      validateHttpsCertificates: false
    });
    return response.getResponseCode();
  } catch (e) {
    return 0;
  }
}
5. 按一天中的时段调整出价(分时段投放)
function main() {
  var CONFIG = {
    DATE_RANGE: 'LAST_30_DAYS',
    MAX_BID_MODIFIER: 0.30,
    MIN_BID_MODIFIER: -0.50,
    MIN_CONVERSIONS: 5
  };

  var query = "SELECT " +
    "campaign.name, " +
    "segments.hour, " +
    "metrics.impressions, " +
    "metrics.clicks, " +
    "metrics.conversions, " +
    "metrics.cost_micros " +
    "FROM campaign " +
    "WHERE campaign.status = 'ENABLED' " +
    "AND segments.date DURING " + CONFIG.DATE_RANGE;

  var results = AdsApp.search(query);
  var hourlyData = {};

  while (results.hasNext()) {
    var row = results.next();
    var name = row.campaign.name;
    var hour = row.segments.hour;

    if (!hourlyData[name]) hourlyData[name] = {};
    if (!hourlyData[name][hour]) {
      hourlyData[name][hour] = { clicks: 0, conversions: 0, cost: 0 };
    }
    hourlyData[name][hour].clicks += row.metrics.clicks;
    hourlyData[name][hour].conversions += row.metrics.conversions;
    hourlyData[name][hour].cost += row.metrics.costMicros / 1000000;
  }

  for (var campaignName in hourlyData) {
    var hours = hourlyData[campaignName];
    var totalConv = 0, totalClicks = 0;
    for (var h in hours) {
      totalConv += hours[h].conversions;
      totalClicks += hours[h].clicks;
    }
    var avgCvr = totalClicks > 0 ? totalConv / totalClicks : 0;

    Logger.log('\n' + campaignName + ' | Avg CVR: ' + (avgCvr * 100).toFixed(2) + '%');

    for (var hour = 0; hour < 24; hour++) {
      var data = hours[hour] || { clicks: 0, conversions: 0 };
      var hourCvr = data.clicks > 0 ? data.conversions / data.clicks : 0;
      var modifier = 0;

      if (avgCvr > 0 && data.conversions >= CONFIG.MIN_CONVERSIONS) {
        modifier = (hourCvr / avgCvr) - 1;
        modifier = Math.max(CONFIG.MIN_BID_MODIFIER, Math.min(CONFIG.MAX_BID_MODIFIER, modifier));
      }

      Logger.log('  Hour ' + hour + ' | Conv: ' + data.conversions +
        ' | Modifier: ' + (modifier > 0 ? '+' : '') + (modifier * 100).toFixed(0) + '%');
    }
  }
}
6. 质量得分跟踪器

将质量得分历史记录到 Google Sheets(Google Ads 不会原生保留质量得分历史记录)。

function main() {
  var CONFIG = {
    SHEET_URL: 'YOUR_GOOGLE_SHEET_URL',
    SHEET_NAME: 'QS History',
    MIN_IMPRESSIONS: 50
  };

  var sheet = SpreadsheetApp.openByUrl(CONFIG.SHEET_URL)
    .getSheetByName(CONFIG.SHEET_NAME);

  if (sheet.getLastRow() === 0) {
    sheet.appendRow([
      'Date', 'Campaign', 'Ad Group', 'Keyword', 'Match Type',
      'Quality Score', 'Expected CTR', 'Ad Relevance', 'Landing Page Exp',
      'Impressions', 'Clicks', 'Cost', 'Conversions'
    ]);
  }

  var today = Utilities.formatDate(new Date(), 'UTC', 'yyyy-MM-dd');

  var query = "SELECT " +
    "campaign.name, ad_group.name, " +
    "ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, " +
    "ad_group_criterion.quality_info.quality_score, " +
    "ad_group_criterion.quality_info.search_predicted_ctr, " +
    "ad_group_criterion.quality_info.creative_quality_score, " +
    "ad_group_criterion.quality_info.post_click_quality_score, " +
    "metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions " +
    "FROM keyword_view " +
    "WHERE campaign.status = 'ENABLED' " +
    "AND ad_group.status = 'ENABLED' " +
    "AND ad_group_criterion.status = 'ENABLED' " +
    "AND metrics.impressions > " + CONFIG.MIN_IMPRESSIONS + " " +
    "AND segments.date DURING LAST_7_DAYS";

  var results = AdsApp.search(query);
  var rows = [];

  while (results.hasNext()) {
    var row = results.next();
    var qi = row.adGroupCriterion.qualityInfo;
    rows.push([
      today, row.campaign.name, row.adGroup.name,
      row.adGroupCriterion.keyword.text, row.adGroupCriterion.keyword.matchType,
      qi.qualityScore || '', qi.searchPredictedCtr || '',
      qi.creativeQualityScore || '', qi.postClickQualityScore || '',
      row.metrics.impressions, row.metrics.clicks,
      (row.metrics.costMicros / 1000000).toFixed(2), row.metrics.conversions
    ]);
  }

  if (rows.length > 0) {
    sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
  }
  Logger.log('Logged ' + rows.length + ' keywords');
}
7. 广告文案测试(RSA 效果)
function main() {
  var CONFIG = {
    MIN_IMPRESSIONS: 500,
    DATE_RANGE: 'LAST_30_DAYS',
    WINNER_CTR_LIFT: 0.20,
    LOSER_CTR_DROP: -0.20,
    ALERT_EMAIL: '[email protected]'
  };

  var query = "SELECT " +
    "campaign.name, ad_group.name, ad_group_ad.ad.id, " +
    "ad_group_ad.ad_strength, " +
    "metrics.impressions, metrics.clicks, metrics.ctr, " +
    "metrics.conversions, metrics.cost_micros " +
    "FROM ad_group_ad " +
    "WHERE campaign.status = 'ENABLED' AND ad_group.status = 'ENABLED' " +
    "AND ad_group_ad.status = 'ENABLED' " +
    "AND ad_group_ad.ad.type = 'RESPONSIVE_SEARCH_AD' " +
    "AND metrics.impressions > " + CONFIG.MIN_IMPRESSIONS + " " +
    "AND segments.date DURING " + CONFIG.DATE_RANGE;

  var results = AdsApp.search(query);
  var adGroups = {};

  while (results.hasNext()) {
    var row = results.next();
    var agKey = row.campaign.name + ' > ' + row.adGroup.name;
    if (!adGroups[agKey]) adGroups[agKey] = [];
    adGroups[agKey].push({
      adId: row.adGroupAd.ad.id,
      strength: row.adGroupAd.adStrength,
      impressions: row.metrics.impressions,
      clicks: row.metrics.clicks,
      ctr: row.metrics.ctr,
      conversions: row.metrics.conversions
    });
  }

  var winners = [], losers = [];

  for (var agKey in adGroups) {
    var ads = adGroups[agKey];
    if (ads.length < 2) continue;

    var totalClicks = 0, totalImpr = 0;
    for (var i = 0; i < ads.length; i++) {
      totalClicks += ads[i].clicks;
      totalImpr += ads[i].impressions;
    }
    var avgCtr = totalImpr > 0 ? totalClicks / totalImpr : 0;

    for (var j = 0; j < ads.length; j++) {
      var ctrLift = avgCtr > 0 ? (ads[j].ctr - avgCtr) / avgCtr : 0;
      if (ctrLift >= CONFIG.WINNER_CTR_LIFT) winners.push({ adGroup: agKey, ad: ads[j], lift: ctrLift });
      else if (ctrLift <= CONFIG.LOSER_CTR_DROP) losers.push({ adGroup: agKey, ad: ads[j], lift: ctrLift });
    }
  }

  Logger.log('Winners: ' + winners.length + ' | Losers: ' + losers.length);
}
8. 按 ROAS 分配预算
function main() {
  var CONFIG = {
    TOTAL_DAILY_BUDGET: 500,
    MIN_BUDGET: 20,
    MAX_BUDGET: 200,
    DATE_RANGE: 'LAST_14_DAYS',
    CAMPAIGN_LABEL: 'Auto-Budget',
    MIN_CONVERSIONS: 3,
    DRY_RUN: true
  };

  var campaigns = AdsApp.campaigns()
    .withCondition('Status = ENABLED')
    .withCondition('LabelNames CONTAINS_ANY ["' + CONFIG.CAMPAIGN_LABEL + '"]')
    .forDateRange(CONFIG.DATE_RANGE)
    .get();

  var campaignData = [];
  while (campaigns.hasNext()) {
    var campaign = campaigns.next();
    var stats = campaign.getStatsFor(CONFIG.DATE_RANGE);
    var cost = stats.getCost();
    var convValue = stats.getConversionValue();

    campaignData.push({
      campaign: campaign,
      name: campaign.getName(),
      cost: cost,
      roas: cost > 0 ? convValue / cost : 0,
      conversions: stats.getConversions(),
      currentBudget: campaign.getBudget().getAmount()
    });
  }

  campaignData.sort(function(a, b) { return b.roas - a.roas; });

  var totalRoas = 0;
  for (var i = 0; i < campaignData.length; i++) {
    if (campaignData[i].conversions >= CONFIG.MIN_CONVERSIONS) totalRoas += campaignData[i].roas;
  }

  for (var j = 0; j < campaignData.length; j++) {
    var c = campaignData[j];
    var newBudget = (c.conversions >= CONFIG.MIN_CONVERSIONS && totalRoas > 0)
      ? (c.roas / totalRoas) * CONFIG.TOTAL_DAILY_BUDGET
      : CONFIG.MIN_BUDGET;

    newBudget = Math.max(CONFIG.MIN_BUDGET, Math.min(CONFIG.MAX_BUDGET, Math.round(newBudget * 100) / 100));

    Logger.log(c.name + ' | ROAS: ' + c.roas.toFixed(2) +
      ' | Current: $' + c.currentBudget.toFixed(2) + ' | New: $' + newBudget.toFixed(2));

    if (!CONFIG.DRY_RUN) c.campaign.getBudget().setAmount(newBudget);
  }

  if (CONFIG.DRY_RUN) Logger.log('DRY RUN — no budgets changed.');
}
9. 异常检测
function main() {
  var CONFIG = {
    LOOKBACK_DAYS: 14,
    SPIKE_THRESHOLD: 2.0,
    DROP_THRESHOLD: 0.5,
    ALERT_EMAIL: '[email protected]',
    METRICS: ['impressions', 'clicks', 'cost_micros', 'conversions']
  };

  var today = new Date();
  var yesterday = new Date(today.getTime() - 86400000);
  var lookbackStart = new Date(today.getTime() - (CONFIG.LOOKBACK_DAYS + 1) * 86400000);

  var yesterdayStr = Utilities.formatDate(yesterday, 'UTC', 'yyyyMMdd');
  var lookbackStr = Utilities.formatDate(lookbackStart, 'UTC', 'yyyyMMdd');

  var query = "SELECT campaign.name, " +
    "metrics.impressions, metrics.clicks, metrics.cost_micros, " +
    "metrics.conversions, segments.date " +
    "FROM campaign WHERE campaign.status = 'ENABLED' " +
    "AND segments.date BETWEEN '" +
      lookbackStr.substring(0, 4) + '-' + lookbackStr.substring(4, 6) + '-' + lookbackStr.substring(6, 8) +
    "' AND '" +
      yesterdayStr.substring(0, 4) + '-' + yesterdayStr.substring(4, 6) + '-' + yesterdayStr.substring(6, 8) + "'";

  var results = AdsApp.search(query);
  var campaignDays = {};

  while (results.hasNext()) {
    var row = results.next();
    var name = row.campaign.name;
    var date = row.segments.date;
    if (!campaignDays[name]) campaignDays[name] = {};
    campaignDays[name][date] = {
      impressions: row.metrics.impressions,
      clicks: row.metrics.clicks,
      cost_micros: row.metrics.costMicros,
      conversions: row.metrics.conversions
    };
  }

  var alerts = [];
  var yesterdayDate = Utilities.formatDate(yesterday, 'UTC', 'yyyy-MM-dd');

  for (var campaignName in campaignDays) {
    var days = campaignDays[campaignName];
    var yesterdayData = days[yesterdayDate];
    if (!yesterdayData) continue;

    for (var m = 0; m < CONFIG.METRICS.length; m++) {
      var metric = CONFIG.METRICS[m];
      var sum = 0, count = 0;
      for (var d in days) {
        if (d !== yesterdayDate) { sum += days[d][metric] || 0; count++; }
      }
      if (count === 0) continue;
      var avg = sum / count;
      var current = yesterdayData[metric] || 0;

      if (avg > 0) {
        var ratio = current / avg;
        if (ratio >= CONFIG.SPIKE_THRESHOLD || ratio <= CONFIG.DROP_THRESHOLD) {
          alerts.push({
            campaign: campaignName, metric: metric,
            type: ratio >= CONFIG.SPIKE_THRESHOLD ? 'SPIKE' : 'DROP',
            current: current, average: avg, ratio: ratio
          });
        }
      }
    }
  }

  if (alerts.length > 0) {
    var body = 'Anomaly Report — ' + yesterdayDate + '\n\n';
    for (var a = 0; a < alerts.length; a++) {
      body += alerts[a].type + ': ' + alerts[a].campaign +
        ' | ' + alerts[a].metric + ' | ' + alerts[a].ratio.toFixed(2) + 'x avg\n';
    }
    MailApp.sendEmail(CONFIG.ALERT_EMAIL, '[Google Ads] ' + alerts.length + ' Anomalies', body);
  }
}
10. 着陆页效果
function main() {
  var CONFIG = {
    DATE_RANGE: 'LAST_30_DAYS',
    MIN_CLICKS: 50,
    SHEET_URL: 'YOUR_GOOGLE_SHEET_URL',
    SHEET_NAME: 'Landing Pages'
  };

  var query = "SELECT " +
    "landing_page_view.unexpanded_final_url, " +
    "metrics.clicks, metrics.impressions, " +
    "metrics.cost_micros, metrics.conversions, " +
    "metrics.conversions_value " +
    "FROM landing_page_view " +
    "WHERE segments.date DURING " + CONFIG.DATE_RANGE + " " +
    "AND metrics.clicks > " + CONFIG.MIN_CLICKS + " " +
    "ORDER BY metrics.clicks DESC";

  var results = AdsApp.search(query);
  var pages = [];

  while (results.hasNext()) {
    var row = results.next();
    var cost = row.metrics.costMicros / 1000000;
    var cvr = row.metrics.clicks > 0 ? row.metrics.conversions / row.metrics.clicks : 0;
    var roas = cost > 0 ? row.metrics.conversionsValue / cost : 0;

    pages.push([
      row.landingPageView.unexpandedFinalUrl,
      row.metrics.clicks, row.metrics.impressions,
      cost.toFixed(2), row.metrics.conversions,
      row.metrics.conversionsValue.toFixed(2),
      (cvr * 100).toFixed(2) + '%', roas.toFixed(2)
    ]);
  }

  if (CONFIG.SHEET_URL && pages.length > 0) {
    var sheet = SpreadsheetApp.openByUrl(CONFIG.SHEET_URL)
      .getSheetByName(CONFIG.SHEET_NAME);
    sheet.clear();
    sheet.appendRow(['URL', 'Clicks', 'Impressions', 'Cost', 'Conversions',
      'Conv Value', 'CVR', 'ROAS']);
    sheet.getRange(2, 1, pages.length, pages[0].length).setValues(pages);
    Logger.log('Exported ' + pages.length + ' landing pages');
  }
}

Google Sheets 集成

打开电子表格
var ss = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/SHEET_ID/edit');
var ss = SpreadsheetApp.openById('SHEET_ID');
var sheet = ss.getSheetByName('Data');
读取数据
var values = sheet.getRange('A1:D10').getValues();  // 2D array
var allData = sheet.getDataRange().getValues();
var value = sheet.getRange('B2').getValue();         // Single cell
写入数据
sheet.getRange('A1').setValue('Campaign Name');
sheet.appendRow(['2025-02-11', 'Brand', 150, 12]);

// Batch write (much faster than appendRow in a loop)
var data = [['Campaign', 'Clicks'], ['Brand', 150], ['Generic', 300]];
sheet.getRange(1, 1, data.length, data[0].length).setValues(data);

电子邮件和通知

// Simple email
MailApp.sendEmail('[email protected]', 'Subject', 'Body text');

// HTML email
MailApp.sendEmail({
  to: '[email protected]',
  subject: '[Google Ads] Weekly Report',
  htmlBody: '<h1>Report</h1><p>Details here.</p>'
});

计划安排

在 工具和设置 > 批量操作 > 脚本 中进行设置。可用频率:每小时、每天、每周、每月。

建议的计划安排:

  • 预算消耗进度:每天或每小时
  • 否定关键词挖掘:每周
  • 质量得分跟踪:每天
  • 异常检测:每天(早上)
  • 失效 URL 检查:每周
  • 信息中心:每天

错误处理

function main() {
  try {
    processKeywords();
  } catch (e) {
    Logger.log('ERROR: ' + e.message + '\nStack: ' + e.stack);
    MailApp.sendEmail('[email protected]',
      '[Google Ads Script] Error', 'Error: ' + e.message);
  }
}

**执行时间限制:**30 分钟(单个账号),60 分钟(MCC)。请妥善处理:

var startTime = new Date().getTime();
var MAX_RUNTIME = 25 * 60 * 1000;  // 25 min buffer

while (iterator.hasNext()) {
  if (new Date().getTime() - startTime > MAX_RUNTIME) {
    Logger.log('Approaching time limit. Stopping.');
    break;
  }
  // Process...
}

MCC 脚本

基本 MCC 脚本
function main() {
  var accounts = AdsManagerApp.accounts()
    .withCondition('Impressions > 0')
    .forDateRange('LAST_7_DAYS')
    .withLimit(50)
    .get();

  while (accounts.hasNext()) {
    var account = accounts.next();
    AdsManagerApp.select(account);
    Logger.log('Processing: ' + account.getName());
    // Now AdsApp refers to this account
  }
}
使用 executeInParallel() 并行处理
function main() {
  var accounts = AdsManagerApp.accounts()
    .withCondition('Impressions > 0')
    .forDateRange('LAST_7_DAYS')
    .get();

  accounts.executeInParallel('processAccount', 'aggregateResults');
}

function processAccount() {
  var account = AdsApp.currentAccount();
  var result = { name: account.getName(), id: account.getCustomerId(), campaigns: [] };

  var campaigns = AdsApp.campaigns()
    .withCondition('Status = ENABLED')
    .forDateRange('LAST_7_DAYS').get();

  while (campaigns.hasNext()) {
    var c = campaigns.next();
    var s = c.getStatsFor('LAST_7_DAYS');
    result.campaigns.push({
      name: c.getName(), clicks: s.getClicks(),
      cost: s.getCost(), conversions: s.getConversions()
    });
  }

  return JSON.stringify(result);  // Must return string
}

function aggregateResults(results) {
  for (var i = 0; i < results.length; i++) {
    if (results[i].getStatus() === 'OK') {
      var data = JSON.parse(results[i].getReturnValue());
      Logger.log(data.name + ': ' + data.campaigns.length + ' campaigns');
    } else {
      Logger.log('Error: ' + results[i].getError());
    }
  }
}

executeInParallel() 规则:

  • 每个账号的函数必须返回字符串(JSON.stringify())
  • 回调接收 ExecutionResult 对象数组(.getStatus()、.getReturnValue()、.getError()、.getCustomerId())
  • 每次调用最多处理 50 个账号
  • 每个账号的执行时间限制为 30 分钟

最佳实践

  1. 先使用预览模式 — 运行前务必点击“预览”
  2. 修改前先记录日志 — 在 keyword.pause() 之前使用 Logger.log('Pausing: ' + keyword.getText())
  3. DRY_RUN 标志 — 每个修改数据的脚本都应在配置中设置 DRY_RUN: true
  4. 逐步推广 — 先从一个广告系列开始,通过验证后再扩大范围
  5. 错误通知 — 失败时发送电子邮件,不要依赖检查日志
  6. 使用标签 — 通过标签指定脚本的目标,而不是将广告系列名称硬编码
  7. 批量写入 Sheets — 使用包含二维数组的 setValues(),不要在循环中使用 appendRow()
  8. 记录脚本说明 — 添加注释块,说明用途、计划安排、作者和配置说明

限制

限制详情
执行时间30 分钟(单个账号),60 分钟(MCC)
不支持 async/await仅支持同步执行
ES6 支持有限不支持 import/export,不支持 class
不支持 Display/Video API通过脚本仅提供有限支持
不支持 Performance Max只读(报告功能有限)
URL 获取响应上限为 50 MB,超时时间为 60 秒
MailApp每天最多 100 位收件人
不支持 npm/模块无法导入外部库
executeInParallel()每次调用最多处理 50 个账号

资源

评论

支持 Markdown