Рациональная автоматизация кампании в Google AdWords

Предыстория


Все началось со слов «А сделай-ка xml выгрузку для AdWords», и тут понеслось. Как ни странно, но именно эта задача была выполнена довольно быстро, но дальше было интереснее. Как оказалось, в AdWords появилась возможность писать скрипты (javascript) по автоматизации процесса ведения кампании и было бы все хорошо, если бы не лимиты по времени исполнения и xml. Да-да, именно xml. Я не знаю, почему всем так запал в душу этот формат, но мне он никогда не нравился. С 95% задачи я справился и, откровенно говоря, удовольствия я от этого не получил да и оставалось еще 5% задачи. Именно эти 5% я бросил уже не на xml, a на json и вот тут стало весело.

Больше конкретики


Давайте конкретизируем о чем вообще идет речь. Есть интернет магазин с ~25 000 наименований. Маркетологу нужна выгрузка, чтоб загнать это все в кампанию: создать группы обьявлений, сами обьявления, ключи и т.д. Как выяснилось дальше, то не важно какой формат входящих данных (xml/json), по этому я выбрал тот, что мне больше по душе — json.

{ elems: [
{
id: 555233,
n: "Agent Provocateur Maitresse",
p: 346,
u: "http://site.ua/555233.html",
v: "Agent Provocateur",
c: "Женская парфюмерия"
},
{
id: 559675,
n: "Angel Schlesser Essential for Men",
p: 191,
u: "http://site.ua/559675.html",
v: "Angel Schlesser",
c: "Мужская парфюмерия"
}
]}


И вот имеем N элементов со структурой id, n (Name), p (Price), u (Url), v (Vendor), с (Category), во всяком случае это именно те данные, что нужны были мне. Приступим к автоматизации?

Скрипт 1. Создание групп обьявлений


// Получаем google-таблицу для записи итератора добавления групп обьявлений
var doc = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/15_W4y3GpivCjuNRWPN8HKy27MjtUeW2NTThAAPPXdkc/edit#gid=0');
// Выбираем по имени лист
var sheet = doc.getSheetByName('parfums');
// Ячейка, в которую будет записан текущий итератор этого скрипта
var i_cell = sheet.getRange('B2');
var date_cell = sheet.getRange('B3');

function main() {  
  var i_cell_val = ( i_cell.isBlank() ) ? 0 : i_cell.getValue();  
  // Получаем JSON
  var json = JSON.parse(UrlFetchApp.fetch('http://site.ua/adwords.json').getContentText());
  // Получаем зараннее созданную нами кампанию
  var tmp = AdWordsApp.campaigns().withCondition('Name = "ббб"').get();
  var unloaded = json.elems;
  var export_l = unloaded.length;
  
  if(is_exported()) {   
    Logger.log('Already exported');
    return;
  }
  
  if (tmp.hasNext()) {
    var campaign = tmp.next();
  } else {
    Logger.log('Company not found');
    return;
  }
  
  for (i= i_cell_val; i<=export_l-1; i++) {
    el = unloaded[i];
    var tmp = campaign.adGroups().withCondition('Name CONTAINS  "__ID-' + el.id +'"').get();
    if (tmp.hasNext()) {
      var tmp_g = tmp.next();
      tmp_g.enable();
    } else {
      var adGroupName = el.c + '_' + el.v + '_' + el.n + '__ID-' + el.id;
      addAdGroup(adGroupName, campaign); 
    }
    i_cell.getValue();
    i_cell.setValue(i);
    if (i == export_l-1) {
      date_cell.setValue(Utilities.formatDate(new Date(), "GMT+3", "d.M.yyyy"));
      i_cell.setValue(0);
    }
  }
}

    
function addAdGroup(adGroupName, ci) {
  var adGroup = ci.newAdGroupBuilder();
  adGroup = adGroup.withName(adGroupName).withStatus("ENABLED").withKeywordMaxCpc(1).create();
}

function is_exported() {
  var exp_date = Number(Utilities.formatDate(new Date(date_cell.getValue()), "GMT+3", "dd"));
  var today = Utilities.formatDate(new Date(), "GMT+3", "dd HH").split(' ');
  if (Number(today[1]) < 6)
    return true;
  if ( (exp_date < Number(today[0])) || date_cell.isBlank())
    return false;
  else
    return true;  
}


Этот скрипт будет козой для понимания следующих. Кто пробежался глазами по скрипту, явно задался вопросами «Зачем? Зачем тут гугл док? Что вообще за бред?». Рассказываю. Как бы я не любил Google, но, увы, эти автоматизационные скрипты выполняются крайне долго, а расписание настраивается крайне туго, вот и присутствие костылей.

Зачем же нам google-док?

Spreadsheet в гугл-док будет для нас хранилищем, для доступа к которому есть описанное и поддерживаемое API. Туда мы будем писать данные, по которым скрипты будут понимать, нужно ли еще что-то делать или стоит обрываться.

Табличка будет примерно такой:

image

Ячейка B2 — сюда у нас записывается текущий итератор элементов в цикле. Нулю он равен тогда, когда все в текущий день выгружено, так же должно быть равно текущей дате значение в ячейке B3, совокупность этих равенств будет значить, что на текущей день выгружены все элементы. Для чего это нужно? Для того, чтоб можно было поставить скрипт по расписанию на каждый час и после полного выполнения он просто выключался с сообщением «Все выгружено».

Что за функция is_exported?

Эта функция присутствует в каждом скрипте и будет проверять, нужно ли гнать все данные по новой.
Конкретно в моем случае, она выглядит так:

function is_exported() {
  var exp_date = Number(Utilities.formatDate(new Date(date_cell.getValue()), "GMT+3", "dd"));
  var today = Utilities.formatDate(new Date(), "GMT+3", "dd HH").split(' ');
  if (Number(today[1]) < 6)
    return true;
  if ( (exp_date < Number(today[0])) || date_cell.isBlank())
    return false;
  else
    return true;  
}


Копируя это себе, не забывайте о паре важных моментов. Во-первых, поставьте свой часовой пояс, у меня стоит GMT+3, во-вторых, поставьте if (Number(today[1]) < 6) сюда, вместо 6, свое значения «ЧАСОВ», после которого скрипт будет выполняться. У меня стоит 6 часов, потому что, примерно, к тому времени будет готова вся выгрузка.

Скрипт 2. Создание текстов обьявлений в группах


// Получаем google-таблицу для записи итератора добавления групп обьявлений
var doc = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/15_W4y3GpivCjuNRWPN8HKy27MjtUeW2NTThAAPPXdkc/edit#gid=0');
var sheet = doc.getSheetByName('parfums');
// Ячейка, в которую будет записан текущий итератор этого скрипта
var i_cell = sheet.getRange('C2');
var date_cell = sheet.getRange('C3');

function main() {  
  var i_cell_val = ( i_cell.isBlank() ) ? 0 : i_cell.getValue();  
  // Получаем JSON
  var json = JSON.parse(UrlFetchApp.fetch('http://site.ua/adwords.json').getContentText());
  // Получаем зараннее созданную нами кампанию
  var tmp = AdWordsApp.campaigns().withCondition('Name = "ббб"').get();
  var unloaded = json.elems;
  var export_l = unloaded.length;
  
  if(is_exported()) {   
    Logger.log('Already exported');
    return;
  }
  
  if (tmp.hasNext()) {
    var campaign = tmp.next();
  } else {
    Logger.log('Company not found');
    return;
  }
  
  for (i= i_cell_val; i<=export_l-1; i++) {
    
    el = unloaded[i];
    var tmp_g = campaign.adGroups().withCondition('Name CONTAINS  "__ID-' + el.id +'"').get();
    if (tmp_g.hasNext()) {
      var adGroup = tmp_g.next();
      var lb = adGroup.labels().withCondition('Name =  "with_text"').get(); //Ищем лейбл в данной ADG
      
      if (!lb.hasNext()) { //Если в текущей ADG нету обьявления, создадим его
        adGroup.createTextAd('{KeyWord:Купить парфюмерию}', 'Покупай духи за {param1: ' + el.p + '} грн', 'Бесплатная курьерская доставка!', 'site.ua/' + el.v.replace(/ /g, '_'),  el.u);
        adGroup.applyLabel('with_text');
      }
      
    } else {
      Logger.log("Группа объявлений '" + el.id + "' не найдена.");
    }
    i_cell.getValue();
    i_cell.setValue(i);
    
    if (i == export_l-1) { //Если выгрузка закончена
      date_cell.setValue(Utilities.formatDate(new Date(), "GMT+3", "d.M.yyyy"))
      i_cell.setValue(0);
    }
    
  }
}

function is_exported() {
  var exp_date = Number(Utilities.formatDate(new Date(date_cell.getValue()), "GMT+3", "dd"));
  var today = Utilities.formatDate(new Date(), "GMT+3", "dd HH").split(' ');
  if (Number(today[1]) < 8)
    return true;
  if ( (exp_date < Number(today[0])) || date_cell.isBlank())
    return false;
  else
    return true;
}


В этом скрипте, я думаю, всем все понятно, но один моментв се же обьяснить стоит — использование ярлыков. Зачем? Да просто потому, что я не нашел, как проверить, существует ли в группе обьявление. Вот и все, если метку в группе нашли — значит существует, т.к. ярлык мы присваиваем только после добавления текста. Все просто.

Скрипт 3. Создание ключевых слов


// Получаем google-таблицу для записи итератора добавления групп обьявлений
var doc = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/15_W4y3GpivCjuNRWPN8HKy27MjtUeW2NTThAAPPXdkc/edit#gid=0');
var sheet = doc.getSheetByName('parfums');
// Ячейка, в которую будет записан текущий итератор этого скрипта
var i_cell = sheet.getRange('D2');
var date_cell = sheet.getRange('D3');
var flag_cell = sheet.getRange('D4');

function main() {  
  var i_cell_val = ( i_cell.isBlank() ) ? 0 : i_cell.getValue();  
  // Получаем JSON
  var json = JSON.parse(UrlFetchApp.fetch('http://site.ua/adwords.json').getContentText());
  // Получаем зараннее созданную нами кампанию
  var tmp = AdWordsApp.campaigns().withCondition('Name = "ббб"').get();
  var unloaded = json.elems;
  var export_l = unloaded.length;
  
  if(is_exported()) {   
    Logger.log('Already exported');
    return;
  }
  
  if (tmp.hasNext()) {
    var campaign = tmp.next();
  } else {
    Logger.log('Company not found');
    return;
  }
  
  var flag_v = ( flag_cell.isBlank() ) ? 1 : flag_cell.getValue();
  
  for (i= i_cell_val; i<=export_l-1; i++) {
    
    el = unloaded[i];
    var tmp_g = campaign.adGroups().withCondition('Name CONTAINS  "__ID-' + el.id +'"').get();
    if (tmp_g.hasNext()) {
      var adGroup = tmp_g.next();
      
      var key = el.n;      
      var tmp_key = AdWordsApp.keywords().withCondition('Text =  "' + key + '"').get(); //Ищем существует ли уже такой ключ
      if (!tmp.hasNext()) {
        adGroup.createKeyword(key);
      }
      key = '[' + el.n + ']';      
      tmp = AdWordsApp.keywords().withCondition('Text =  "' + key + '"').get();
      if (!tmp.hasNext()) {
        adGroup.createKeyword(key);
      }
      
    } else {
      flag_v = 0;
      Logger.log("Группа объявлений '" + el.id + "' не найдена.");
    }
    i_cell.getValue();
    i_cell.setValue(i);
    flag_cell.setValue(flag_v);
    if (i == export_l-1) { //Если выгрузка закончена
      if (Number(flag_v)) //Если добавили обьявления во все ADG
        date_cell.setValue(Utilities.formatDate(new Date(), "GMT+3", "d.M.yyyy"));
      i_cell.setValue(0);
    }
    
  }
}

function is_exported() {
  var exp_date = Number(Utilities.formatDate(new Date(date_cell.getValue()), "GMT+3", "dd"));
  var today = Utilities.formatDate(new Date(), "GMT+3", "dd HH").split(' ');
  if (Number(today[1]) < 8)
    return true;
  if ( (exp_date < Number(today[0])) || date_cell.isBlank())
    return false;
  else
    return true;
}


Тут тоже все просто. Есть выгрузка, по ID вытягивает группу, в группе создаем ключи. Занавес! Но и тут не без мелочей. Здесь появилась переменная flag_v. Если она равна нулю, то работа цикла не закрывается. Это сделано для того, чтоб избежать рассинхрона, в случае, если группы обьявлений еще не были созданы. Так, же поменяйте «ЧАСОВОЙ» параметр в функции is_exported, поставьте на час или 2 позже.

Скрипт 4. Обновление параметров


// Получаем google-таблицу для записи итератора добавления групп обьявлений
var doc = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/15_W4y3GpivCjuNRWPN8HKy27MjtUeW2NTThAAPPXdkc/edit#gid=0');
var sheet = doc.getSheetByName('parfums');
// Ячейка, в которую будет записан текущий итератор этого скрипта
var i_cell = sheet.getRange('E2');
var date_cell = sheet.getRange('E3');
var flag_cell = sheet.getRange('E4');

function main() {  
  var i_cell_val = ( i_cell.isBlank() ) ? 0 : i_cell.getValue();  
  // Получаем JSON
  var json = JSON.parse(UrlFetchApp.fetch('http://site.ua/adwords.json').getContentText());
  // Получаем зараннее созданную нами кампанию
  var tmp = AdWordsApp.campaigns().withCondition('Name = "ббб"').get();
  var unloaded = json.elems;
  var export_l = unloaded.length;
  
  if(is_exported()) {   
    Logger.log('Already exported');
    return;
  }
  
  if (tmp.hasNext()) {
    var campaign = tmp.next();
  } else {
    Logger.log('Company not found');
    return;
  }
  
  var flag_v = ( flag_cell.isBlank() ) ? 1 : flag_cell.getValue();
  
  for (i= i_cell_val; i<=export_l-1; i++) {
    
    el = unloaded[i];
    var tmp_g = campaign.adGroups().withCondition('Name CONTAINS  "__ID-' + el.id +'"').get();
    if (tmp_g.hasNext()) {
      
      var adGroup = tmp_g.next();
      var keywordIter = adGroup.keywords().get();
      while (keywordIter.hasNext()) {
        var keyword = keywordIter.next();
        keyword.setAdParam(1, el.p);
      }
      
    } else {
      flag_v = 0;
      Logger.log("Группа объявлений '" + el.id + "' не найдена.");
    }
    i_cell.getValue();
    i_cell.setValue(i);
    flag_cell.setValue(flag_v);
    if (i == export_l-1) { //Если выгрузка закончена
      if (Number(flag_v)) //Если добавили обьявления во все ADG
        date_cell.setValue(Utilities.formatDate(new Date(), "GMT+3", "d.M.yyyy"));
      i_cell.setValue(0);
    }
    
  }
}

function is_exported() {
  var exp_date = Number(Utilities.formatDate(new Date(date_cell.getValue()), "GMT+3", "dd"));
  var today = Utilities.formatDate(new Date(), "GMT+3", "dd HH").split(' ');
  if (Number(today[1]) < 6)
    return true;
  if ( (exp_date < Number(today[0])) || date_cell.isBlank())
    return false;
  else
    return true;
}


Тут все еще проще. Получили группу, получили итератор ключей, пробежались по всем ключевым словам, обновили параметр 1 (цена) во всех ключах. Готово.

Скрипт 5. Обновление статуса групп


function main() {
  var json_ids = JSON.parse(UrlFetchApp.fetch('http://site.ua/adwords.json').getContentText()).ids;
  var tmp = AdWordsApp.campaigns().withCondition('Name = "ббб"').get();
  if (tmp.hasNext()) {
    var campaign = tmp.next();
    var tmp = campaign.adGroups().get();
  } else {
    Logger.log('Company not found');
  }
  while (tmp.hasNext()) {
    group = tmp.next();
    name = group.getName();
    id = /__ID-(\d+)$/.exec(name)[1];
    if ( json_ids.indexOf(id) == -1 ) {
      group.pause();
    }
  }
}


Это самый крутой и быстрый скрипт. В нем мы циклом пробегаемся по всем группам обьявлений, регуляркой вытягиваем ID товара, проверяем есть ли он в выгрузке и, если нету, ставим группу на паузу, чтоб экономить денежку. Если не включать никаких логгеров, скрипт пробегает по ~3000 обьявлений за ~20 минут. Да, забыл упомянуть, в выгрузке вы должны иметь секцию с массивом всех участвующих в этом процессе ID'шников. Можете сделать для этого отдельный json, можете засунуть в тот же, что и предыдущих скриптах участвует — на вкус и цвет.

Пара интересных моментов


  • i_cell.getValue();
    i_cell.setValue(i);

    Это сделано для того, чтоб в живом режиме обновлялись данные в таблице
  • Формируя группу обьявлений, делайте это так, чтоб можно было из нее вытащить ID
  • Продумайте расписание исполнения скриптов, чтоб не было накладок на «несозданные» элементы
  • Последний скрипт можно поставить на исполнение каждые 15 минут, следовательно, если у вас живой магазин и высокая динамика в изменении наличия товара, вы будете экономить деньги. Остальные скрипты можно ставить на исполнение каждый час
  • Это все только коза для общего понимания
  • Простите за простыни кода, там многое повторяется, но иначе можно очень запутаться


Главный источник: developers.google.com/adwords/scripts/docs/reference/index
Посмотреть, как будет выглядеть док можно здесь: docs.google.com/spreadsheets/d/15_W4y3GpivCjuNRWPN8HKy27MjtUeW2NTThAAPPXdkc/edit?usp=sharing

Меньше вам ручной работы и больше экономии разумной. Всем спасибо за внимание.

UPD:

  • Исправил ошибки в скриптах небольшие. Заменил бред на использование isBlank() метода
  • Забыл в начале написать, что для полноценной работы нужно создать ярлык, если следовать моему примеру он должен называться with_text
  • +16
  • 12.2k
  • 7
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 7

    0
    А можно привести пример получившегося объявления? Список текстов и ключей составляет маркетолог?
      0
      image
      Вот, взгляните, это получается по итогу. Да, типы ключей и что в них будет входить — составляет маркетолог. Дальше оно из выгрузки и формируется.
      0
      Да просто потому, что я не нашел, как проверить, существует ли в группе обьявление.

      var adGroup = tmp_g.next();
      var ai = adGroup.ads().get();
      Logger.log("AdGroup '" + adGroup.getName() + "' (" + String(adGroup.getId()) + ") has " + ai.totalNumEntities() + " ads.");
      
        0
        и все же, кака я узнаю существует ли там данное объявление? Я узнаю имя группы, а с самим объявлением что делать?
          0
          while(ai.hasNext()) {
            var ad = ai.next();
            Logger.log("    %s (%s): %s", String(ad.getId()), ad.getType(), ad.getHeadline());
          }
          
            0
            спасибо за совет, я проверю и обязательно отпишу и покажу результаты.
        0
        А удалось ли реализовать апдейт объявлений? Когда меняется текст (например цена товара в заголовке), или включаются/отключаются ключевые слова?

        Only users with full accounts can post comments. Log in, please.