关于 google-ads-scripts
google-ads-scripts 是面向 Google Ads 脚本编写者的 JavaScript 参考技能,涵盖 AdsApp、GAQL、常见自动化模式及 Google Sheets 集成,并提供编写、调试和优化脚本的指导。
主要功能
- 01讲解 AdsApp 对象模型与选择器
- 02提供 GAQL 查询模式
- 03涵盖账户与 MCC 自动化
- 04展示 Google Sheets 和邮件集成
- 05给出错误处理与安全实践
适用场景
- 01编写预算节奏监控脚本
- 02挖掘搜索词并生成否定关键词
- 03跟踪质量得分并写入 Sheets
- 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 分钟
最佳实践
- 先使用预览模式 — 运行前务必点击“预览”
- 修改前先记录日志 — 在
keyword.pause()之前使用Logger.log('Pausing: ' + keyword.getText()) - DRY_RUN 标志 — 每个修改数据的脚本都应在配置中设置
DRY_RUN: true - 逐步推广 — 先从一个广告系列开始,通过验证后再扩大范围
- 错误通知 — 失败时发送电子邮件,不要依赖检查日志
- 使用标签 — 通过标签指定脚本的目标,而不是将广告系列名称硬编码
- 批量写入 Sheets — 使用包含二维数组的
setValues(),不要在循环中使用appendRow() - 记录脚本说明 — 添加注释块,说明用途、计划安排、作者和配置说明
限制
| 限制 | 详情 |
|---|---|
| 执行时间 | 30 分钟(单个账号),60 分钟(MCC) |
| 不支持 async/await | 仅支持同步执行 |
| ES6 支持有限 | 不支持 import/export,不支持 class |
| 不支持 Display/Video API | 通过脚本仅提供有限支持 |
| 不支持 Performance Max | 只读(报告功能有限) |
| URL 获取 | 响应上限为 50 MB,超时时间为 60 秒 |
| MailApp | 每天最多 100 位收件人 |
| 不支持 npm/模块 | 无法导入外部库 |
| executeInParallel() | 每次调用最多处理 50 个账号 |
资源
- **Google Ads 脚本文档:**https://developers.google.com/google-ads/scripts/docs/overview
- **AdsApp 参考:**https://developers.google.com/google-ads/scripts/docs/reference/adsapp/adsapp
- **GAQL 参考:**https://developers.google.com/google-ads/api/docs/query/overview
- **脚本示例:**https://developers.google.com/google-ads/scripts/docs/solutions
- **Cogny 完整文档:**https://cogny.com/docs/google-ads-scripts