本页介绍了使用 Google Ads 脚本进行开发时的各种最佳实践。
选择器
使用选择器进行过滤
尽量使用过滤器,做到只请求您需要的实体。应用适当的过滤条件具有以下好处:
- 代码更简单、更容易理解。
- 脚本执行速度更快。
我们来比较以下代码段:
| 编码方案 | 代码段 |
|---|---|
| 使用选择器进行过滤(推荐) |
var keywords = AdsApp.keywords()
.withCondition('Clicks > 10')
.forDateRange('LAST_MONTH')
.get();
while (keywords.hasNext()) {
var keyword = keywords.next();
// Do work here.
}
|
| 在代码中过滤(不推荐) |
var keywords = AdsApp.keywords().get();
while (keywords.hasNext()) {
var keyword = keywords.next();
var stats = keyword.getStatsFor(
'LAST_MONTH');
if (stats.getClicks() > 10) {
// Do work here.
}
}
|
不建议采用第二种方法,因为该方法会尝试检索您账号中的所有关键字列表,然后仅对该列表应用过滤条件。
避免遍历广告系列的各个层级
如果您想检索特定级别的实体,请使用该级别的集合方法,而不是遍历整个广告系列层次结构。除了更简单之外,这种方式的效果也会好得多:系统不必不必要地读取所有广告系列和广告组。
比较以下用于检索账号中所有广告的代码段:
| 编码方案 | 代码段 |
|---|---|
| 使用适当的收集方法(推荐) |
var ads = AdsApp.ads(); |
| 遍历层次结构(不推荐) |
var campaigns = AdsApp.campaigns().get();
while (campaigns.hasNext()) {
var adGroups = campaigns.next().
adGroups().get();
while (adGroups.hasNext()) {
var ads = adGroups.next().ads().get();
// Do your work here.
}
}
|
不建议采用第二种方法,因为它尝试提取整个对象层次结构(广告系列、广告组),而实际上只需要广告。
使用特定的父级访问器方法
有时候您需要获取某个检索对象的父级实体。在这种情况下,您应使用提供的访问器方法,而不是提取整个层次结构。
比较以下代码段,这些代码段用于检索上个月获得超过 50 次点击的文字广告所在的广告组:
| 编码方案 | 代码段 |
|---|---|
| 使用适当的父访问器方法(推荐) |
var ads = AdsApp.ads()
.withCondition('Clicks > 50')
.forDateRange('LAST_MONTH')
.get();
while (ads.hasNext()) {
var ad = ads.next();
var adGroup = ad.getAdGroup();
var campaign = ad.getCampaign();
// Store (campaign, adGroup) to an array.
}
|
| 遍历层次结构(不推荐) |
var campaigns = AdsApp.campaigns().get();
while (campaigns.hasNext()) {
var adGroups = campaigns.next()
.adGroups()
.get();
while (adGroups.hasNext()) {
var ads = adGroups.ads()
.withCondition('Clicks > 50')
.forDateRange('LAST_MONTH')
.get();
if (ads.totalNumEntities() > 0) {
// Store (campaign, adGroup) to an array.
}
}
}
|
不建议采用第二种方法,因为当您只需要与一组广告相关联的部分广告系列和广告组时,该方法会提取您账号中的整个广告系列和广告组层次结构。第一种方法仅限于提取相关广告集合,并使用适当的方法来访问其父对象。
使用特定的父级过滤器
如需访问特定广告系列或广告组中的实体,请在选择器中使用特定过滤条件,而不是先提取再遍历层次结构。
比较以下代码段,它们用于检索指定广告系列和广告组中上个月获得 50 次以上点击的文字广告列表。
| 编码方案 | 代码段 |
|---|---|
| 使用适当的父级过滤条件(推荐) |
var ads = AdsApp.ads()
.withCondition('CampaignName = "Campaign 1"')
.withCondition('AdGroupName = "AdGroup 1"')
.withCondition('Clicks > 50')
.forDateRange('LAST_MONTH')
.get();
while (ads.hasNext()) {
var ad = ads.next();
var adGroup = ad.getAdGroup();
var campaign = ad.getCampaign();
// Store (campaign, adGroup, ad) to
// an array.
}
|
| 遍历层次结构(不推荐) |
var campaigns = AdsApp.campaigns()
.withCondition('Name = "Campaign 1"')
.get();
while (campaigns.hasNext()) {
var adGroups = campaigns.next()
.adGroups()
.withCondition('Name = "AdGroup 1"')
.get();
while (adGroups.hasNext()) {
var ads = adGroups.ads()
.withCondition('Clicks > 50')
.forDateRange('LAST_MONTH')
.get();
while (ads.hasNext()) {
var ad = ads.next();
// Store (campaign, adGroup, ad) to
// an array.
}
}
}
|
不建议采用第二种方法,因为当您只需要一组选定的广告及其父广告系列和广告组时,该方法会迭代处理您账号中的广告系列和广告组层次结构。第一种方法通过在选择器上应用针对父实体的特定过滤条件,将迭代限制为广告列表。
尽量使用 ID 进行过滤
过滤实体时,最好按实体 ID 而不是其他字段进行过滤。
我们来考虑以下用于选择广告系列的代码段。
| 编码方案 | 代码段 |
|---|---|
| 按 ID 过滤(推荐) |
var campaign = AdsApp.campaigns()
.withIds([12345])
.get()
.next();
|
| 按名称过滤(效果较差) |
var campaign = AdsApp.campaigns()
.withCondition('Name="foo"')
.get()
.next();
|
第二种方案按照非 ID 字段进行过滤,因此不是最优方案。
尽量按照父级 ID 进行过滤
选择实体时,应尽量按父级 ID 进行过滤。这样可以限制服务器在过滤结果时检索的实体列表,从而加快查询速度。
请参考以下代码段,该代码段按 ID 检索广告组。 假设已知父级广告系列的 ID。
| 编码方案 | 代码段 |
|---|---|
| 按广告系列和广告组 ID 过滤(推荐) |
var adGroup = AdsApp.adGroups()
.withIds([12345])
.withCondition('CampaignId="54678"')
.get()
.next();
|
| 仅按广告组 ID 过滤(效果欠佳) |
var adGroup = AdsApp.adGroups()
.withIds([12345])
.get()
.next();
|
虽然这两个代码段会得出相同的结果,但代码段 1 中使用父 ID (CampaignId="54678") 进行的额外过滤会限制服务器在过滤结果时必须迭代的实体列表,从而提高代码效率。
在过滤条件太多时使用标签
如果您有太多过滤条件,最好为处理的实体创建标签,并使用该标签来过滤实体。
请看以下代码段,该代码段按名称检索广告系列列表。
| 编码方案 | 代码段 |
|---|---|
| 使用标签(推荐) |
var label = AdsApp.labels()
.withCondition('Name = "My Label"')
.get()
.next();
var campaigns = label.campaigns.get();
while (campaigns.hasNext()) {
var campaign = campaigns.next();
// Do more work
}
|
| 构建复杂的选择器(不推荐) |
var campaignNames = ['foo', 'bar', 'baz'];
for (var i = 0; i < campaignNames.length; i++) {
campaignNames[i] = '"' + campaignNames[i] + '"';
}
var campaigns = AdsApp.campaigns
.withCondition('CampaignName in [' + campaignNames.join(',') + ']')
.get();
while (campaigns.hasNext()) {
var campaign = campaigns.next();
// Do more work.
}
|
虽然这两个代码段都能提供相似的性能,但随着选择器中条件数量的增加,第二种方法往往会生成更复杂的代码。与修改脚本以添加新实体相比,将标签应用于新实体也更简单。
限制 IN 子句中的条件数量
在运行脚本时,一个常见的使用情形是针对实体列表运行报告。开发者通常会构建一个非常长的 GAQL 查询,该查询使用 IN 子句按实体 ID 进行过滤。当实体数量有限时,此方法效果不错。不过,随着查询长度的增加,脚本性能会因以下两个原因而下降:
- 较长的查询需要更长的时间来解析。
- 您添加到 IN 子句中的每个 ID 都是要评估的额外条件,因此需要更长时间。
在这种情况下,最好为实体应用标签,然后按 LabelId 进行过滤。
| 编码方案 | 代码段 |
|---|---|
| 应用标签并按 labelID 过滤(推荐) |
// The label applied to the entity is "Report Entities"
var label = AdsApp.labels()
.withCondition('LabelName contains "Report Entities"')
.get()
.next();
var report = AdsApp.report('SELECT AdGroupId, Id, Clicks, ' +
'Impressions, Cost FROM KEYWORDS_PERFORMANCE_REPORT ' +
'WHERE LabelId = "' + label.getId() + '"');
|
| 使用 IN 子句构建长查询(不推荐) |
var report = AdsApp.report('SELECT AdGroupId, Id, Clicks, ' +
'Impressions, Cost FROM KEYWORDS_PERFORMANCE_REPORT WHERE ' +
'AdGroupId IN (123, 456) and Id in (123,345, 456…)');
|
账号更新
批量更改
当您更改 Google Ads 实体时,Google Ads 脚本不会立即执行相应更改。相反,它会尝试将多个更改合并为批次,以便发出一个可进行多项更改的请求。这种方法可提高脚本的运行速度,并减轻 Google Ads 服务器的负载。不过,有些代码模式会强制 Google Ads 脚本频繁刷新其批量操作,从而导致脚本运行缓慢。
来考虑以下脚本,该脚本将更新某个关键字列表的出价。
| 编码方案 | 代码段 |
|---|---|
| 跟踪更新后的元素(推荐) |
var keywords = AdsApp.keywords()
.withCondition('Clicks > 50')
.withCondition('CampaignName = "Campaign 1"')
.withCondition('AdGroupName = "AdGroup 1"')
.forDateRange('LAST_MONTH')
.get();
var list = [];
while (keywords.hasNext()) {
var keyword = keywords.next();
keyword.bidding().setCpc(1.5);
list.push(keyword);
}
for (var i = 0; i < list.length; i++) {
var keyword = list[i];
Logger.log('%s, %s', keyword.getText(),
keyword.bidding().getCpc());
}
|
| 在紧凑的循环中检索更新后的元素(不推荐) |
var keywords = AdsApp.keywords()
.withCondition('Clicks > 50')
.withCondition('CampaignName = "Campaign 1"')
.withCondition('AdGroupName = "AdGroup 1"')
.forDateRange('LAST_MONTH')
.get();
while (keywords.hasNext()) {
var keyword = keywords.next();
keyword.bidding().setCpc(1.5);
Logger.log('%s, %s', keyword.getText(),
keyword.bidding().getCpc());
}
|
不建议采用第二种方法,因为对 keyword.bidding().getCpc() 的调用会强制 Google Ads 脚本刷新 setCpc() 操作,并一次只执行一个操作。第一种方法虽然与第二种方法类似,但由于 getCpc() 调用是在与 setCpc() 调用不同的循环中完成的,因此还具有支持批处理的额外优势。
尽量使用构建器
Google Ads 脚本支持两种创建新对象的方式:构建器和创建方法。构建器比创建方法更灵活,因为它们可让您访问通过 API 调用创建的对象。
我们来考虑以下代码段:
| 编码方案 | 代码段 |
|---|---|
| 使用构建器(推荐) |
var operation = adGroup.newKeywordBuilder()
.withText('shoes')
.build();
var keyword = operation.getResult();
|
| 使用创建方法(不推荐) |
adGroup.createKeyword('shoes');
var keyword = adGroup.keywords()
.withCondition('KeywordText="shoes"')
.get()
.next();
|
第二种方法不太理想,因为检索关键字时需要进行额外的选择操作。此外,创建方法也被弃用了。
不过,请注意,如果构建器使用不当,可能会导致 Google Ads 脚本无法批量处理其操作。
请看以下代码段,它们会创建一个关键字列表并输出新创建的关键字的 ID:
| 编码方案 | 代码段 |
|---|---|
| 跟踪更新后的元素(推荐) |
var keywords = ['foo', 'bar', 'baz'];
var list = [];
for (var i = 0; i < keywords.length; i++) {
var operation = adGroup.newKeywordBuilder()
.withText(keywords[i])
.build();
list.push(operation);
}
for (var i = 0; i < list.length; i++) {
var operation = list[i];
var result = operation.getResult();
Logger.log('%s %s', result.getId(),
result.getText());
}
|
| 在紧凑的循环中检索更新后的元素(不推荐) |
var keywords = ['foo', 'bar', 'baz'];
for (var i = 0; i < keywords.length; i++) {
var operation = adGroup.newKeywordBuilder()
.withText(keywords[i])
.build();
var result = operation.getResult();
Logger.log('%s %s', result.getId(),
result.getText());
}
|
不建议采用第二种方法,因为该方法会在创建操作的同一循环中调用 operation.getResult(),从而强制 Google Ads 脚本一次执行一个操作。第一种方法虽然类似,但允许批处理,因为我们在与创建操作不同的循环中调用 operation.getResult()。
考虑对大量更新使用批量上传
开发者经常执行的任务是根据当前效果值运行报告并更新实体属性(例如,关键字出价)。当您必须更新大量实体时,批量上传往往能带来更好的性能。例如,请考虑以下脚本,这些脚本会提高过去一个月内 TopImpressionPercentage > 0.4 的关键字的 MaxCpc:
| 编码方案 | 代码段 |
|---|---|
| 使用批量上传功能(推荐) |
var report = AdsApp.report(
'SELECT AdGroupId, Id, CpcBid FROM KEYWORDS_PERFORMANCE_REPORT ' +
'WHERE TopImpressionPercentage > 0.4 DURING LAST_MONTH');
var upload = AdsApp.bulkUploads().newCsvUpload([
report.getColumnHeader('AdGroupId').getBulkUploadColumnName(),
report.getColumnHeader('Id').getBulkUploadColumnName(),
report.getColumnHeader('CpcBid').getBulkUploadColumnName()]);
upload.forCampaignManagement();
var reportRows = report.rows();
while (reportRows.hasNext()) {
var row = reportRows.next();
row['CpcBid'] = row['CpcBid'] + 0.02;
upload.append(row.formatForUpload());
}
upload.apply();
|
| 按 ID 选择和更新关键字(效果较差) |
var reportRows = AdsApp.report('SELECT AdGroupId, Id, CpcBid FROM ' +
'KEYWORDS_PERFORMANCE_REPORT WHERE TopImpressionPercentage > 0.4 ' +
' DURING LAST_MONTH')
.rows();
var map = {
};
while (reportRows.hasNext()) {
var row = reportRows.next();
var adGroupId = row['AdGroupId'];
var id = row['Id'];
if (map[adGroupId] == null) {
map[adGroupId] = [];
}
map[adGroupId].push([adGroupId, id]);
}
for (var key in map) {
var keywords = AdsApp.keywords()
.withCondition('AdGroupId="' + key + '"')
.withIds(map[key])
.get();
while (keywords.hasNext()) {
var keyword = keywords.next();
keyword.bidding().setCpc(keyword.bidding().getCpc() + 0.02);
}
}
|
虽然第二种方法可提供相当不错的性能,但在此示例中,我们更倾向于使用第一种方法,因为
Google Ads 脚本对单次运行中可检索或更新的对象数量有限制,而第二种方法中的选择和更新操作会占用该限制。
批量上传在可更新的实体数量和总体执行时间方面都有更高的限制。
将批量上传按广告系列分组
创建批量上传文件时,请尝试按父广告系列对操作进行分组。这样可以提高效率,并降低出现冲突更改 / 并发错误的可能性。
考虑两个同时运行的批量上传任务。一个用于暂停广告组中的广告,另一个用于调整关键字出价。即使这些操作互不相关,它们也可能应用于同一广告组(或同一广告系列下的两个不同广告组)中的实体。发生这种情况时,系统会锁定父实体(共享广告组或广告系列),从而导致批量上传任务相互阻塞。
Google Ads 脚本可以优化单个批量上传任务中的执行,因此最简单的方法是每次只针对一个账号运行一个批量上传任务。如果您决定为每个账号运行多次批量上传,请确保这些批量上传操作针对的是互斥的广告系列(及其子实体)列表,以实现最佳效果。
报告
使用报告获取统计信息
如果您想检索大量实体及其统计信息,通常最好使用报告,而不是标准的 AdsApp 方法。由于以下原因,建议使用报告:
- 对于大型查询,报告的执行效果更好。
- 报告不会达到正常提取配额。
比较以下代码段,这些代码段用于提取上个月获得 50 次以上点击的所有关键字的点击次数、展示次数、费用和文字:
| 编码方案 | 代码段 |
|---|---|
| 使用报告(推荐) |
report = AdsApp.search(
'SELECT ' +
' ad_group_criterion.keyword.text, ' +
' metrics.clicks, ' +
' metrics.cost_micros, ' +
' metrics.impressions ' +
'FROM ' +
' keyword_view ' +
'WHERE ' +
' segments.date DURING LAST_MONTH ' +
' AND metrics.clicks > 50');
while (report.hasNext()) {
var row = report.next();
Logger.log('Keyword: %s Impressions: %s ' +
'Clicks: %s Cost: %s',
row.adGroupCriterion.keyword.text,
row.metrics.impressions,
row.metrics.clicks,
row.metrics.cost);
}
|
| 使用 AdsApp 迭代器(不推荐) |
var keywords = AdsApp.keywords()
.withCondition('metrics.clicks > 50')
.forDateRange('LAST_MONTH')
.get();
while (keywords.hasNext()) {
var keyword = keywords.next();
var stats = keyword.getStatsFor('LAST_MONTH');
Logger.log('Keyword: %s Impressions: %s ' +
'Clicks: %s Cost: %s',
keyword.getText(),
stats.getImpressions(),
stats.getClicks(),
stats.getCost());
}
|
第二种方法不是首选方法,因为它会遍历关键字并一次检索一个实体的统计信息。在这种情况下,报告的执行速度更快,因为它可以一次性提取所有数据,并根据需要进行流式传输。此外,第二种方法检索到的关键字会纳入脚本的配额,即使用 get() 调用检索到的实体数量。
使用搜索功能,而不是报告
报告方法是为旧版基础架构构建的,即使您使用的是 GAQL,也会以扁平格式输出结果。这意味着,它必须转换查询结果以匹配旧样式,但并非所有字段都支持这种转换,并且每次调用都会增加开销。
我们建议您改用搜索功能,以便充分利用新版 Google Ads API 报告的所有功能。
优先选择 GAQL 而不是 AWQL
虽然从技术上讲,AWQL 仍然适用于报告查询和 withCondition 调用,但不建议使用。如需完全掌控查询,请务必改用 GAQL。
不要选择超出需要的行数
报告(和选择器)的执行速度取决于报告将返回的总行数,无论您是否遍历这些行。这意味着,您应始终使用具体过滤条件,尽可能缩小结果集,以符合您的使用情形。
例如,假设您想查找出价不在某个特定范围内的广告组。与提取所有广告组并忽略您不感兴趣的广告组相比,分别查询低于底部阈值的出价和高于顶部阈值的出价会更快。
| 编码方案 | 代码段 |
|---|---|
| 使用两个查询(推荐) |
var adGroups = []
var report = AdsApp.search(
'SELECT ad_group.name, ad_group.cpc_bid_micros' +
' FROM ad_group WHERE ad_group.cpc_bid_micros < 1000000');
while (report.hasNext()) {
var row = report.next();
adGroups.push(row.adGroup);
}
var report = AdsApp.search(
'SELECT ad_group.name, ad_group.cpc_bid_micros' +
' FROM ad_group WHERE ad_group.cpc_bid_micros > 2000000');
while (report.hasNext()) {
var row = report.next();
adGroups.push(row.adGroup);
}
|
| 从一般性查询开始过滤(不推荐) |
var adGroups = []
var report = AdsApp.search(
'SELECT ad_group.name, ad_group.cpc_bid_micros' +
' FROM ad_group');
while (report.hasNext()) {
var row = report.next();
var cpcBidMicros = row.adGroup.cpcBidMicros;
if (cpcBidMicros < 1000000 || cpcBidMicros > 2000000) {
adGroups.push(row.adGroup);
}
}
|
Google Ads 经理账号 (MCC) 脚本
选择 executeInParallel 而非串行执行
为经理账号编写脚本时,请尽可能使用 executeInParallel() 而不是串行执行。executeInParallel() 可为您的脚本提供更长的处理时间(最长 1 小时),每个处理的账号最多可获得 30 分钟的处理时间(而不是串行执行的总共 30 分钟)。如需了解详情,请参阅我们的限额页面。
电子表格
更新电子表格时使用批量操作
更新电子表格时,请尽量使用批量操作方法(例如 getRange()),而不是一次更新一个单元格的方法。
请看以下代码段,它会在电子表格上生成分形图案。
| 编码方案 | 代码段 |
|---|---|
| 在一次调用中更新一系列单元格(推荐) |
var colors = new Array(100);
for (var y = 0; y < 100; y++) {
xcoord = xmin;
colors[y] = new Array(100);
for (var x = 0; x < 100; x++) {
colors[y][x] = getColor_(xcoord, ycoord);
xcoord += xincrement;
}
ycoord -= yincrement;
}
sheet.getRange(1, 1, 100, 100).setBackgroundColors(colors);
|
| 一次更新一个单元格(不推荐) |
var cell = sheet.getRange('a1');
for (var y = 0; y < 100; y++) {
xcoord = xmin;
for (var x = 0; x < 100; x++) {
var c = getColor_(xcoord, ycoord);
cell.offset(y, x).setBackgroundColor(c);
xcoord += xincrement;
}
ycoord -= yincrement;
SpreadsheetApp.flush();
}
|
虽然 Google 表格会尝试通过缓存值来优化第二个代码段,但由于 API 调用次数较多,其性能仍不如第一个代码段。