Ведение списка минус-слов или минус-мест размещения — распространенная задача при управлении кампаниями Google Рекламы. Этот список обычно используется в качестве фильтра для ключевых слов или мест размещения, которые привлекают нежелательный трафик на ваш сайт.
Google Реклама поддерживает списки отрицательных критериев, которые можно использовать в нескольких кампаниях в одном аккаунте. Однако применение списка к нескольким кампаниям в одной большой учетной записи и синхронизация списка с течением времени могут вызвать проблемы. Сценарий общего отрицательного списка упрощает эту задачу, позволяя вам управлять отрицательными критериями с помощью электронной таблицы.
Как это работает
Скрипт считывает отрицательные критерии из таблицы Google. Он создает общий список отрицательных критериев в аккаунте Google Рекламы и синхронизирует его с критериями из электронной таблицы. Для ключевых слов и мест размещения ведутся отдельные списки.
Затем сценарий гарантирует, что список отрицательных критериев будет применен ко всем кампаниям в аккаунте. При необходимости вы можете ограничить список кампаний, указав метку в таблице конфигурации для фильтрации включения при обработке кампаний.
Сценарий дополнительно отправляет электронное письмо с кратким описанием своих действий на адрес электронной почты, указанный в электронной таблице конфигурации.
Конфигурация
Скрипт использует электронную таблицу для настройки. Поддерживаются следующие параметры конфигурации:
- Скрипт по умолчанию обрабатывает все
ENABLED
иPAUSED
кампании в аккаунте. Чтобы ограничить список обрабатываемых кампаний,- Создайте ярлык для каждого аккаунта, который хотите обработать.
- Примените этот ярлык к списку кампаний, которые будут обработаны.
- Укажите эту метку в ячейке C3 таблицы конфигурации.
- Укажите адрес электронной почты в ячейке C6, чтобы получить сводное электронное письмо после завершения выполнения сценария.
- Скрипт создает отдельные общие списки минус-критериев для ключевых слов и мест размещения в обрабатываемых им аккаунтах. Укажите имя общих списков отрицательных критериев в электронной таблице конфигурации, ячейки C4 и C5.
- Опустите любой префикс протокола (
http://
илиhttps://
) из любого URL-адреса места размещения в вашем списке мест размещения. - Убедитесь, что все URL-адреса мест размещения не содержат косых черт (
/
). - Убедитесь, что все URL-адреса мест размещения в вашей таблице указаны строчными буквами.
Планирование
Запланируйте выполнение сценария ежедневно или ежечасно.
Настраивать
Нажмите кнопку ниже, чтобы создать сценарий на основе электронной таблицы в своем аккаунте Google Рекламы.
Нажмите кнопку ниже, чтобы сделать копию таблицы шаблона.
Обновите
spreadsheet_url
в вашем скрипте.
Исходный код
// 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 Common Negative List Script
*
* @overview The Common Negative List script applies negative keywords and
* placements from a spreadsheet to multiple campaigns in your account using
* shared keyword and placement lists. See
* https://developers.google.com/google-ads/scripts/docs/solutions/common-negative-list
* for more details.
*
* @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
*
* @version 2.2
*
* @changelog
* - version 2.2
* - Fixed an issue where the match type of keywords in the negative list was
* ignored.
* - version 2.1
* - Split into info, config, and code.
* - version 2.0
* - Updated to use new Google Ads scripts features.
* - version 1.0.2
* - Added validation for external spreadsheet setup.
* - version 1.0.1
* - Improvements to time zone handling.
* - version 1.0
* - Released initial version.
*/
/**
* Configuration to be used for the Common Negative List script.
*/
CONFIG = {
/**
* The URL of the tracking spreadsheet. This should be a copy of
* https://goo.gl/PZGKVn
*/
'spreadsheet_url': 'INSERT_SPREADSHEET_URL_HERE'
};
const SPREADSHEET_URL = CONFIG.spreadsheet_url;
/**
* Keep track of the spreadsheet names for various criteria types, as well as
* the criteria type being processed.
*/
const CriteriaType = {
KEYWORDS: 'Keywords',
PLACEMENTS: 'Placements'
};
/**
* Create a shared negative criteria list in the Google Ads account and
* syncs the list with the criteria from the spreadsheet.
*/
function main() {
let emailParams = {
// Number of placements that were synced.
PlacementCount: 0,
// Number of keywords that were synced.
KeywordCount: 0,
// Number of campaigns that were synced.
CampaignCount: 0,
// Status of processing this account - OK / ERROR.
Status: ''
};
try {
const syncSummary = syncCommonLists();
emailParams.PlacementCount = syncSummary.PlacementCount;
emailParams.KeywordCount = syncSummary.KeywordCount;
emailParams.CampaignCount = syncSummary.CampaignCount;
emailParams.Status = 'OK';
} catch (err) {
emailParams.Status = 'ERROR';
}
const config = readConfig();
const spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
// Make sure the spreadsheet is using the account's timezone.
spreadsheet.setSpreadsheetTimeZone(AdsApp.currentAccount().getTimeZone());
spreadsheet.getRangeByName('LastRun').setValue(new Date());
spreadsheet.getRangeByName('CustomerId').setValue(
AdsApp.currentAccount().getCustomerId());
sendEmail(config, emailParams);
}
/**
* Sends a summary email about the changes that this script made.
*
* @param {Object} config The configuration object.
* @param {Object} emailParams Contains details required to create the email
* body.
*/
function sendEmail(config, emailParams) {
const html = [];
let summary = '';
if (emailParams.Status == 'OK') {
summary = `The Common Negative List script successfully processed ` +
`Customer ID: ${AdsApp.currentAccount().getCustomerId()}` +
` and synced a total of ${emailParams.KeywordCount}` +
` keywords and ${emailParams.PlacementCount} placements.`;
} else {
summary = `The Common Negative List script failed to process ` +
`Customer ID: ${AdsApp.currentAccount().getCustomerId()}` +
` and synced a total of ${emailParams.KeywordCount}` +
` keywords and ${emailParams.PlacementCount} placements.`;
}
html.push('<html>',
'<head></head>',
'<body>',
'<table style="font-family:Arial,Helvetica; ' +
'border-collapse:collapse;font-size:10pt; ' +
'color:#444444; border: solid 1px #dddddd;" ' +
'width="600" cellpadding=20>',
'<tr>',
'<td>',
'<p>Hello,</p>',
'<p>' + summary + '</p>',
'<p>Cheers<br />Google Ads Scripts Team</p>',
'</td>',
'</tr>',
'</table>',
'</body>',
'</html>'
);
if (config.email != '') {
MailApp.sendEmail({
to: config.email,
subject: 'Common Negative List Script',
htmlBody: html.join('\n')
});
}
}
/**
* Synchronizes the negative criteria list in an account with the common list
* in the user spreadsheet.
*
* @return {Object} A summary of the number of keywords and placements synced,
* and the number of campaigns to which these lists apply.
*/
function syncCommonLists() {
const config = readConfig();
let syncedCampaignCount = 0;
const keywordListDetails = syncCriteriaInNegativeList(config,
CriteriaType.KEYWORDS);
syncedCampaignCount = syncCampaignList(config, keywordListDetails.SharedList,
CriteriaType.KEYWORDS);
const placementListDetails = syncCriteriaInNegativeList(config,
CriteriaType.PLACEMENTS);
syncCampaignList(config, placementListDetails.SharedList,
CriteriaType.PLACEMENTS);
return {
'CampaignCount': syncedCampaignCount,
'PlacementCount': placementListDetails.CriteriaCount,
'KeywordCount': keywordListDetails.CriteriaCount
};
}
/**
* Synchronizes the list of campaigns covered by a negative list against the
* desired list of campaigns to be covered by the common list.
*
* @param {Object} config The configuration object.
* @param {AdsApp.NegativeKeywordList|AdsApp.ExcludedPlacementList}
* sharedList The shared negative criterion list to be synced against the
* common list.
* @param {String} criteriaType The criteria type for the shared negative list.
*
* @return {Number} The number of campaigns synced.
*/
function syncCampaignList(config, sharedList, criteriaType) {
const campaignIds = getLabelledCampaigns(config.label);
let totalCampaigns = Object.keys(campaignIds).length;
const listedCampaigns = sharedList.campaigns().get();
const campaignsToRemove = [];
for (const listedCampaign of listedCampaigns) {
if (listedCampaign.getId() in campaignIds) {
delete campaignIds[listedCampaign.getId()];
} else {
campaignsToRemove.push(listedCampaign);
}
}
// Anything left over in campaignIds starts a new list.
const campaignsToAdd = AdsApp.campaigns().withIds(
Object.keys(campaignIds)).get();
for (const campaignToAdd of campaignsToAdd) {
if (criteriaType == CriteriaType.KEYWORDS) {
campaignToAdd.addNegativeKeywordList(sharedList);
} else if (criteriaType == CriteriaType.PLACEMENTS) {
campaignToAdd.addExcludedPlacementList(sharedList);
}
}
for (const campaignToRemove of campaignsToRemove) {
if (criteriaType == CriteriaType.KEYWORDS) {
campaignToRemove.removeNegativeKeywordList(sharedList);
} else if (criteriaType == CriteriaType.PLACEMENTS) {
campaignToRemove.removeExcludedPlacementList(sharedList);
}
}
return totalCampaigns;
}
/**
* Gets a list of campaigns having a particular label.
*
* @param {String} labelText The label text.
*
* @return {Array.<Number>} An array of campaign IDs having the specified
* label.
*/
function getLabelledCampaigns(labelText) {
const campaignIds = {};
let campaigns;
if (labelText != '') {
const label = getLabel(labelText);
campaigns = label.campaigns().withCondition(
'Status in [ENABLED, PAUSED]').get();
} else {
campaigns = AdsApp.campaigns().withCondition(
'Status in [ENABLED, PAUSED]').get();
}
for (const campaign of campaigns) {
campaignIds[campaign.getId()] = 1;
}
return campaignIds;
}
/**
* Gets a label with the specified label text.
*
* @param {String} labelText The label text.
*
* @return {AdsApp.Label} The label text.
*/
function getLabel(labelText) {
let labels = AdsApp.labels().withCondition(
"Name='" + labelText + "'").get();
if (labels.totalNumEntities() == 0) {
const message = Utilities.formatString(`Label named ${labelText} is missing in ' +
'your account. Make sure the label exists in the account, and is ' +
'applied to campaigns and adgroups you wish to process.`);
throw (message);
}
return labels.next();
}
/**
* Synchronizes the criteria in a shared negative criteria list with the user
* spreadsheet.
*
* @param {Object} config The configuration object.
* @param {String} criteriaType The criteria type for the shared negative list.
*
* @return {Object} A summary of the synced negative list, and the number of
* criteria that were synced.
*/
function syncCriteriaInNegativeList(config, criteriaType) {
let criteriaFromSheet = loadCriteria(criteriaType);
let totalCriteriaCount = Object.keys(criteriaFromSheet).length;
let sharedList = null;
let listName = config.listname[criteriaType];
sharedList = createNegativeListIfRequired(listName, criteriaType);
let negativeCriteria = null;
try {
if (criteriaType == CriteriaType.KEYWORDS) {
negativeCriteria = sharedList.negativeKeywords().get();
} else if (criteriaType == CriteriaType.PLACEMENTS) {
negativeCriteria = sharedList.excludedPlacements().get();
}
} catch (e) {
console.error(`Failed to retrieve shared list. Error says ${e}`);
if (AdsApp.getExecutionInfo().isPreview()) {
const message = Utilities.formatString(`The script cannot create` +
` the negative ${criteriaType} list in preview mode. Either run` +
` the script without preview, or create a negative ` +
` ${criteriaType} list with name "${listName}" ` +
` manually before previewing the script.`);
console.log(message);
}
throw e;
}
const criteriaToDelete = [];
for (const negativeCriterion of negativeCriteria) {
let key = null;
if (criteriaType == CriteriaType.KEYWORDS) {
key = negativeCriterion.getText();
// Since the keyword text in the spreadsheet specifies match types in the
// syntax accepted by the UI, we need to convert our keys to match it.
const matchType = negativeCriterion.getMatchType();
if (matchType === "PHRASE") {
key = `"${key}"`;
} else if (matchType === "EXACT") {
key = `[${key}]`;
}
} else if (criteriaType == CriteriaType.PLACEMENTS) {
key = negativeCriterion.getUrl();
}
if (key in criteriaFromSheet) {
// Nothing to do with this criteria. Remove it from loaded list.
delete criteriaFromSheet[key];
} else {
// This criterion is not in the sync list. Mark for deletion.
criteriaToDelete.push(negativeCriterion);
}
}
// Whatever left in the sync list are new items.
if (criteriaType == CriteriaType.KEYWORDS) {
sharedList.addNegativeKeywords(Object.keys(criteriaFromSheet));
} else if (criteriaType == CriteriaType.PLACEMENTS) {
sharedList.addExcludedPlacements(Object.keys(criteriaFromSheet));
}
for (let i = 0; i < criteriaToDelete.length; i++) {
criteriaToDelete[i].remove();
}
return {
'SharedList': sharedList,
'CriteriaCount': totalCriteriaCount,
'Type': criteriaType
};
}
/**
* Creates a shared negative criteria list if required.
*
* @param {string} listName The name of shared negative criteria list.
* @param {String} listType The criteria type for the shared negative list.
*
* @return {AdsApp.NegativeKeywordList|AdsApp.ExcludedPlacementList} An
* existing shared negative criterion list if it already exists in the
* account, or the newly created list if one didn't exist earlier.
*/
function createNegativeListIfRequired(listName, listType) {
let negativeListSelector = null;
if (listType == CriteriaType.KEYWORDS) {
negativeListSelector = AdsApp.negativeKeywordLists();
} else if (listType == CriteriaType.PLACEMENTS) {
negativeListSelector = AdsApp.excludedPlacementLists();
}
let negativeListIterator = negativeListSelector.withCondition(
`Name = '${listName}'`).get();
if (negativeListIterator.totalNumEntities() == 0) {
let builder = null;
if (listType == CriteriaType.KEYWORDS) {
builder = AdsApp.newNegativeKeywordListBuilder();
} else if (listType == CriteriaType.PLACEMENTS) {
builder = AdsApp.newExcludedPlacementListBuilder();
}
let negativeListOperation = builder.withName(listName).build();
return negativeListOperation.getResult();
} else {
return negativeListIterator.next();
}
}
/**
* Loads a list of criteria from the user spreadsheet.
*
* @param {string} sheetName The name of shared negative criteria list.
*
* @return {Object} A map of the list of criteria loaded from the spreadsheet.
*/
function loadCriteria(sheetName) {
const spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
const sheet = spreadsheet.getSheetByName(sheetName);
const values = sheet.getRange('B4:B').getValues();
const retval = {};
for (const value of values) {
let keyword = value[0].toString().trim();
if (keyword != '') {
retval[keyword] = 1;
}
}
return retval;
}
/**
* Loads a configuration object from the spreadsheet.
*
* @return {Object} A configuration object.
*/
function readConfig() {
const spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
let values = spreadsheet.getRangeByName('ConfigurationValues').getValues();
const config = {
'label': values[0][0],
'listname': {
},
'email': values[3][0],
};
config.listname[CriteriaType.KEYWORDS] = values[1][0];
config.listname[CriteriaType.PLACEMENTS] = values[2][0];
return config;
}
/**
* DO NOT EDIT ANYTHING BELOW THIS LINE.
* Please modify your spreadsheet URL 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 == 'INSERT_SPREADSHEET_URL_HERE') {
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);
}