Предыстория
Все началось со слов «А сделай-ка 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.getValue()) ? 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.getValue() == 'NaN')
return false;
else
return true;
}
Этот скрипт будет козой для понимания следующих. Кто пробежался глазами по скрипту, явно задался вопросами «Зачем? Зачем тут гугл док? Что вообще за бред?». Рассказываю. Как бы я не любил Google, но, увы, эти автоматизационные скрипты выполняются крайне долго, а расписание настраивается крайне туго, вот и присутствие костылей.
Зачем же нам google-док?
Spreadsheet в гугл-док будет для нас хранилищем, для доступа к которому есть описанное и поддерживаемое API. Туда мы будем писать данные, по которым скрипты будут понимать, нужно ли еще что-то делать или стоит обрываться.
Табличка будет примерно такой:
Ячейка 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.getValue() == 'NaN')
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.getValue()) ? 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 + '} грн', 'Бесплатная курьерская доставка!', 'parfums.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.getValue() == 'NaN')
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.getValue()) ? 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.getValue()) ? 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.getValue() == 'NaN')
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.getValue()) ? 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.getValue()) ? 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.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.getValue() == 'NaN')
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
Меньше вам ручной работы и больше экономии разумной. Всем спасибо за внимание.
Автор: kricha