لیست منفی مشترک - حساب واحد

نماد ابزارها

حفظ فهرستی از کلمات کلیدی منفی یا قرارگیری منفی یک کار رایج در مدیریت کمپین تبلیغاتی گوگل است. این لیست به طور کلی به عنوان فیلتری در برابر کلمات کلیدی یا مکان هایی که ترافیک ناخواسته را به وب سایت شما هدایت می کنند، استفاده می شود.

Google Ads از فهرست‌های معیارهای منفی پشتیبانی می‌کند که می‌توانند با چند کمپین در یک حساب به اشتراک گذاشته شوند. با این حال، اعمال لیست برای کمپین های متعدد در یک حساب بزرگ و همگام نگه داشتن لیست در طول زمان می تواند چالش هایی را ایجاد کند. اسکریپت Common Negative List این کار را با امکان مدیریت معیارهای منفی از طریق یک صفحه گسترده ساده می کند.

چگونه کار می کند

اسکریپت معیارهای منفی را از صفحه گسترده گوگل می خواند. یک لیست معیارهای منفی مشترک در حساب Google Ads ایجاد می کند و لیست را با معیارهای صفحه گسترده همگام می کند. لیست های جداگانه ای برای کلمات کلیدی و مکان ها نگهداری می شود.

سپس اسکریپت تضمین می کند که لیست معیارهای منفی برای همه کمپین های موجود در حساب اعمال می شود. در صورت لزوم، می‌توانید فهرست کمپین‌ها را با مشخص کردن برچسبی در صفحه‌گسترده پیکربندی محدود کنید تا در هنگام پردازش کمپین‌ها درج شود.

اسکریپت به صورت اختیاری ایمیلی را ارسال می کند که در آن اقدامات خود را به یک آدرس ایمیل مشخص شده در صفحه گسترده پیکربندی خلاصه می کند.

پیکربندی

این اسکریپت از یک صفحه گسترده برای پیکربندی استفاده می کند. تنظیمات پیکربندی زیر پشتیبانی می شوند:

  • این اسکریپت تمام کمپین های ENABLED و PAUSED را در یک حساب به طور پیش فرض پردازش می کند. برای محدود کردن لیست کمپین های پردازش شده،
    1. در هر حسابی که می خواهید پردازش کنید یک برچسب ایجاد کنید.
    2. این برچسب را به لیست کمپین هایی که باید پردازش شوند اعمال کنید.
    3. این برچسب را در سلول C3 صفحه گسترده پیکربندی مشخص کنید.
  • یک آدرس ایمیل در سلول C6 مشخص کنید تا پس از اتمام اجرای اسکریپت، یک ایمیل خلاصه دریافت کنید.
  • این اسکریپت فهرست‌های معیارهای منفی مشترک جداگانه برای کلمات کلیدی و مکان‌ها در حساب‌هایی که پردازش می‌کند ایجاد می‌کند. نام فهرست‌های معیارهای منفی مشترک را در صفحه‌گسترده پیکربندی، سلول‌های C4 و C5 مشخص کنید.
  • هر گونه پیشوند پروتکل ( http:// یا https:// ) را از هر نشانی اینترنتی محل قرارگیری در لیست قرار دادن خود حذف کنید.
  • مطمئن شوید که همه نشانی‌های وب مکان‌یابی دارای اسلش‌های انتهایی ( / ) نیستند.
  • مطمئن شوید که همه نشانی‌های وب قرار دادن با حروف کوچک در صفحه‌گسترده شما قرار دارند.

برنامه ریزی

اسکریپت را برای اجرای روزانه یا ساعتی برنامه ریزی کنید.

برپایی

کد منبع

// 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);
}