Четыре скрипта для управления ставками в Google Ads

Автор: Андрей Педченко, руководитель отдела рекламы Mello

Ни для кого не секрет, что по мере работы кампании ключевые слова накапливают статистику, и мы принимаем решения об эффективности отдельных слов. Кто-то обновляет ставки руками, кто-то использует автоматизацию. Но что делать, когда вы только запускаетесь, и у вас нет данных по эффективности ключевых слов? 

Первое, что приходит в голову — назначить всем ключевикам единую ставку, а далее начать корректировать значения бидов. Однако стоит учитывать, если вы рекламируете товары с большим разбросом цены, например наушники за $5 и наушники за $150, то единая ставка тут не подойдет. Более дешевый товар может выиграть больше акционов, и вы на этом не заработаете. Для такой ситуации есть второй вариант назначения первых ставок — ставки в зависимости от цены товара. Для начала рассмотрим простейший вариант скрипта Google Ads, когда цена товара уже присутствует в тексте объявления.

Скрипт Google Ads для назначения ставок, если цена товара указана в объявлении

Логика работы простая: с помощью итератора и селектора ключевых слов получаем активные слова из активных групп нужной кампании. Для каждого ключевика получаем его группу, из этой группы берем первое объявление. Из нужного поля объявления с помощью слайсов получаем цену товара. Далее назначаем ставку ключевому слову, исходя из коэффициента конверсии сайта и среднего дохода с товара; в примере цена товара просто делится на 80. Скрипт работает только с развернутыми текстовыми объявлениями, если хотите работать с другими типами, то почитайте в справке о селекторах для нужного формата объявлений.

Код скрипта с комментариями:  
function main() {

// Получаем нужные ключевики
var kw1 = AdWordsApp.keywords()
.withCondition(‘CampaignName = «Указать имя кампании»‘)
.withCondition(«Status = ENABLED»)
.withCondition(«AdGroupStatus = ENABLED»)
.get();
// Если хотите отбирать ключевые слова по каким-то другим условиям, справка по селекторам в помощь:
//https://developers.google.com/google-ads/scripts/docs/reference/adsapp/adsapp_keywordselector?hl=ru#withCondition_1

// Проходимся по словам

while(kw1.hasNext()) {
var kw = kw1.next();
var kwText = kw.getText();
var group = kw.getAdGroup();
var groupName = group.getName();
var ads = group.ads().withCondition(«Type = ‘EXPANDED_TEXT_AD'»).withCondition(«Status = ENABLED»).get();

// Если хотите отбирать объявления по иным условиям, справка по селекторам объявлений в помощь:
//https://developers.google.com/google-ads/scripts/docs/reference/adsapp/adsapp_adselector?hl=ru#withCondition_1 

var ad = ads.next();
var head3 = ad.getHeadlinePart3();

// В нашем примере цена товара указана в третьем заголовке без каких-либо других значений,
// поэтому мы просто берем значение третьего заголовка; пример — “$456.78”
// Если хотите брать значение из другого поля, то в справке описаны поля 
//https://developers.google.com/google-ads/scripts/docs/reference/adsapp/adsapp_expandedtextad?hl=ru 
// Если у вас цена прописана в формате “Товар от 576 руб с доставкой”, то надо еще вырезать цену из этой строки
// Можно попробовать что-то такое
/*
    var headline1 = ad.getHeadlinePart1(); // Предположим, что ваша цена в первом заголовке
    var reg = ‘от (.*) руб’;
    var res = headline1.match(reg);
    //Logger.log(res[1]);
    var start = headline1.indexOf(res[1]);
    //Logger.log(start);
    var startheadline = headline1.slice(0,start);
    //Logger.log(startheadline);
    var endheadline = headline1.slice(start+res[1].length);
    //Logger.log(endheadline);
    var price = headline1.slice(start,-(endheadline.length));
    //Logger.log(price);
*/

// Определяемся со ставкой: вырезаем знак валюты, переводим значение к вещественному числу, округляем до 2х знаков и делим на 80
// Если вы получали цену с помощью вырезания из строки, то вместо переменной head3 у вас будет переменная price,
// ну или как вы ее назовете,
// и заменять символ валюты уже не надо будет.

var bid = (parseFloat(head3.replace(«$»,»»))/80).toFixed(2);

// Определяемся с порогами для ставки
if (bid<=0.01){bid=0.01;} 
if (bid>=8){bid=8;}

Logger.log(‘Group — ‘ + groupName + ‘, Keyword —  ‘ + kwText + ‘New bid — ‘ + bid + ‘, Price — ‘ + head3)

// Устанавливаем ставку ключевому слову
kw.setMaxCpc(bid);

}
// Конец
}

Ну и не забудьте поставить дневное выполнение скрипта.

Скрипт Google Ads для назначения ставок, если цена товара не указана в объявлении

Есть и второй вариант скрипта Google Ads, когда цена товара не указана в тексте объявления, но имеется на посадочной странице.

Логика работы простая: с помощью итератора и селектора ключевых слов получаем активные слова из активных групп нужной кампании. Для каждого ключевика получаем его группу, из этой группы берем первое объявление. Из поля final url получаем ссылку на товар, переходим по ссылке и с помощью регулярных выражений находим значение цены на странице. Далее назначаем ставку ключевому слову, исходя из коэффициента конверсии сайта и среднего дохода с товара; в примере цена товара просто делится на 80. Скрипт работает только с развернутыми текстовыми объявлениями, если хотите работать с другими типами, то почитайте в справке о селекторах для нужного формата объявлений.

Код скрипта с комментариями:
function main() {

// Получаем нужные ключевики
var kw1 = AdWordsApp.keywords()
.withCondition(‘CampaignName = «Указать имя кампании»‘)
.withCondition(«Status = ENABLED»)
.withCondition(«AdGroupStatus = ENABLED»)
.get();
// Если хотите отбирать ключевые слова по каким-то другим условиям, справка по селекторам в помощь:
//https://developers.google.com/google-ads/scripts/docs/reference/adsapp/adsapp_keywordselector?hl=ru#withCondition_1

// Проходимся по словам

while(kw1.hasNext()) {
var kw = kw1.next();
var kwText = kw.getText();
var group = kw.getAdGroup();
var groupName = group.getName();
var ads = group.ads().withCondition(«Type = ‘EXPANDED_TEXT_AD'»).withCondition(«Status = ENABLED»).get();

// Если хотите отбирать объявления по иным условиям, справка по селекторам объявлений в помощь:
//https://developers.google.com/google-ads/scripts/docs/reference/adsapp/adsapp_adselector?hl=ru#withCondition_1 

var ad = ads.next();
var url = ad.urls().getFinalUrl();

  var response = UrlFetchApp.fetch(url);
 
  var c = response.getContentText();
  var text = c;
  var reg = ‘<strong>(.*) руб <i class=»fa fa-rub»></i></strong>’; // Вот тут в кавычках указываем типовую часть кода посадочной страницы, где находится цена, да так, чтобы сама цена была на месте (.*)
// Например на странице фрагмент с ценой выглядит так: <strong>4300 руб <i class=»fa fa-rub»></i></strong>
// Тогда в переменной reg это надо записать так: var reg = ‘<strong>(.*)  <i class=»fa fa-rub»></i></strong>’;
  var res = text.match(reg);
  //Logger.log(res[1]);
  var price = parseFloat(res[1]);

var bid = (price/80).toFixed(2);

// Определяемся с порогами для ставки
if (bid<=0.01){bid=0.01;} 
if (bid>=8){bid=8;}

Logger.log(‘Group — ‘ + groupName + ‘, Keyword —  ‘ + kwText + ‘New bid — ‘ + bid + ‘, Price — ‘ + price)

// Устанавливаем ставку ключевому слову
kw.setMaxCpc(bid);

}
// Конец
}

Ну и не забудьте поставить дневное выполнение скрипта. А и еще, если скрипт не успевает отработать сразу для всех РК (работа с UrlFetchApp требует времени), сделайте отдельный скрипт для каждой кампании, если и это не помогло, то читайте дальше про тяжелые случаи.

Скрипт Google Ads для назначения ставок, если цена товара не указана в объявлении, и у вас огромный аккаунт

Рассмотрим один из сложных вариантов скрипта Google Ads, когда цена товара не указана в тексте объявления, но имеется на посадочной странице, и при этом у вас такой большой аккаунт, что предыдущий скрипт вам не помог.

Логика работы мудреная, возможно в таком виде это вам и не пригодится, но интересные вещи почерпнете. Первый скрипт Google Ads проходит по всем вашим модельным кампаниям (надо добавить уникальный ярлык к объявлениям из таких РК), собирает все ссылки из объявлений и вставляет их в Google-таблицу. В этой Google-таблице срабатывает свой скрипт, который в соседних ячейках проставляет цены товаров по той же логике, что и в предыдущем скрипте. 

Еще один скрипт Google Ads забирает из таблицы данные по ценам и ссылкам, сохраняет их в списке, далее он же с помощью итератора и селектора ключевых слов получает активные слова из активных групп нужных кампаний. Для каждого ключевика получает его группу, из этой группы берет первое объявление. 

Из поля final url получаем ссылку на товар, для этой ссылки из массива находим цену. Далее назначаем ставку ключевому слову, исходя из коэффициента конверсии сайта и среднего дохода с товара; в примере цена товара просто делится на 80. Скрипт работает только с развернутыми текстовыми объявлениями, если хотите работать с другими типами, то почитайте в справке о селекторах для нужного формата объявлений.

Инструкция с кодами скриптов и комментариями. Везде одобряйте авторизацию для скриптов

1. Скопировать в аккаунт Ads этот скрипт и сохранить его отдельно:

function main() {
 
  var urls = [];
 
   // выбираем все объявления
  var adsIterator = AdWordsApp.ads()
      .withCondition(«CampaignStatus = ENABLED»)
      .withCondition(«AdGroupStatus = ENABLED»)
      .withCondition(«Status = ENABLED»)
      .withCondition(«LabelNames CONTAINS_ANY [‘Ярлык для отбора объявлений’]») //Тут указываем ярлык
      .withCondition(«Type = ‘EXPANDED_TEXT_AD'»)
      .get();
// Если хотите отбирать объявления по иным условиям, справка по селекторам объявлений в помощь:
//https://developers.google.com/google-ads/scripts/docs/reference/adsapp/adsapp_adselector?hl=ru#withCondition_1 

  while (adsIterator.hasNext()) {
    try {
      
    var ads = adsIterator.next();
    var url = ads.urls().getFinalUrl();
      urls.push(url);
   
   
   
    } catch(e){/*Logger.log(‘-‘);*/}
  }
 
  //Logger.log(urls);
 
    urls.sort(); // сортируем массив urls

for (var i = urls.length — 1; i > 0; i—) {
    if (urls[i] == urls[i — 1]) urls.splice( i, 1);
}
 
  //Logger.log(urls.length);
 
  // объявляем таблицу с данными
  var ss = SpreadsheetApp.openById(‘1EU6HIvhC7BtFaoWeXx1kQq_svBQnUj6uhIpuuXWfQCE’) // Указываем id таблицы
  var sheet = ss.getSheets()[0]; // Работаем с первым листом таблицы
  for (var i=1;i<=urls.length;i++) {
  var range = sheet.getRange(«A»+i).setValue(urls[i-1]);
   }
}

Этот скрипт собирает ссылки из всех объявлений и отправляет в Google-таблицу. Этому скрипту можно назначить расписание выполнения на 1 раз в день. В первый раз можно запустить руками для проверки. Тут неважно как запускать: в режиме просмотра или выполнения, в Google Ads изменений не будет, а в таблицу вставятся ссылки. В строчке с переменной ss надо указать id нужной таблицы. ID таблицы берется из ссылки на таблицу, например: https://docs.google.com/spreadsheets/d/1EU6HIvhCsssss7BtFaokQq_svBfQCE/edit#gid=0  — жирным выделили id.

2. В используемой таблице зайти в “Инструменты” -> “Редактор скриптов”:

Зайти в “Файл” — >”Создать” — >”Скрипт”:

Вставить код:

function clearSheet() {
 
    // объявляем таблицу с данными, полученными из Ads
  var ss = SpreadsheetApp.openById(‘1EU6HgtFaoWeXx1kQq_svgggWfQCE’) //id таблицы
  var sheet = ss.getSheets()[0]; // Работаем с первым листом

  // очищаем ее
  sheet.clear(); 
}

В 4й строке указываем id таблицы. Сохраняем скрипт под каким-нибудь названием, для удобства можно назвать “clearTable”. Этот скрипт будет очищать таблицу с заданной периодичностью. Если обратить внимание на первую строку, то увидим название функции — clearSheet.

Чтобы настроить расписание этого скрипта, надо зайти в “Правка” -> “Триггеры текущего проекта” и добавить новый триггер для упомянутой выше функции:

Можно на раз в неделю или раз в день ночью. Если триггер будет на каждый день, то он должен срабатывать раньше, чем скрипт добавления ссылок в таблицу, который был описан в пункте 1.

3. По аналогии создаем в этой же таблице новый скрипт.
Вставляем код:

function getPrices() {
 
   //функция получения цены
  function getPrice(url) {
   
    //парсим страницу
    var response = UrlFetchApp.fetch(url);
    var c = response.getContentText();
    var text = c;

    try {

      var reg = ‘<meta itemprop=»price» content=»(.*)» />’;// Вот тут в кавычках указываем типовую часть кода посадочной страницы, где находится цена, да так, чтобы сама цена была на месте (.*)
// Например на странице фрагмент с ценой выглядит так: ‘<meta itemprop=»price» content=»4300″ />’
// Тогда в переменной reg это надо записать так: var reg = ‘<meta itemprop=»price» content=»(.*)» />’;

      var res = text.match(reg);
      //Logger.log(res[1]);
      return res[1]
      
    }
    catch(e){Logger.log(‘-‘);}
   
  } // Это была вспомогательная функция, которая получает цену для заданного url
  
   // Далее уже будем для каждого url из ячеек столбца A вставлять цену в ячейки столбца B
   // объявляем таблицу с данными

  var ss = SpreadsheetApp.openById(‘1EU6HIvioWeXx1kQq_svBjXWfQCE’) // id таблицы
  var sheet = ss.getSheets()[0]; // Работаем с первым листом
  var lastRow = sheet.getLastRow();
  Logger.log(lastRow);
 
  var rangeAB = sheet.getRange(«A1:B»+lastRow).getValues();
  var rangeB = sheet.getRange(«B1:B»+lastRow).getValues();
  //Logger.log(rangeB.length);
  //Logger.log(rangeAB.length);
 
  var j = 1;
  while (rangeB[j-1][0].toString()!=») {j++;}

 // Logger.log(j);
  var lastRowB = j;
  //Logger.log(lastRow);
  rangeAB = sheet.getRange(«A»+lastRowB+»:B»+lastRow).getValues();
  //Logger.log(rangeAB.length);
  rangeB = sheet.getRange(«B»+lastRowB+»:B»+lastRow).getValues();
  //Logger.log(rangeB.length);
 
 
  for (var i=lastRowB;i<=lastRow;i++){
    var pr = rangeAB[i-lastRowB][1];
    //Logger.log(pr);
    //Logger.log(pr.length);
  var url = rangeAB[i-lastRowB][0];
    Logger.log(url);
    //Logger.log(rangeB);
  if (pr.length==0)
  {
    try{
    var valuePr = getPrice(url);
      sheet.getRange(«B»+i).setValue(valuePr);}
    catch(e){Logger.log(‘-‘);}
   
  }
  }
}

В строке с переменной ss указываем id таблицы. Скрипт можно назвать произвольно, но для удобства можно и “getPrices”. Этот скрипт будет парсить сайт и находить цены под соответствующие ссылки. Обратим внимание, в начале скрипта указано название главной функции — getPrices . Для нее настраиваем триггер на каждые 15 минут:

Первый раз можно запустить руками:

За один проход этот скрипт может не успевать указать все цены, поэтому расписание такое частое. Проходов за 5 он успеет указать все цены.

4. Создаем еще один скрипт для таблицы:

  function sorter() {
   
   // объявляем таблицу с данными
  var ss = SpreadsheetApp.openById(‘1EU6HgXx1kQq_svBQngfQCE’) // id таблицы
  var sheet = ss.getSheets()[0];
  var lastRow = sheet.getLastRow();
  Logger.log(lastRow);
 
  var rangeAB = sheet.getRange(«A1:B»+lastRow);
      rangeAB.sort(2);
   
  rangeAB = sheet.getRange(«A1:B»+lastRow).getValues();
  var rangeB = sheet.getRange(«B1:B»+lastRow).getValues();
  //Logger.log(rangeB);

    var mas = [];
    for (var i=0; i<rangeB.length;i++) {mas.push(rangeB[i][0]);}
    //Logger.log(mas);
    var j = 1 ;
    var lastB = 1;
    try {while (mas[j-1]>=0) {j++;}}
    catch(e) {lastB=j;}
    //Logger.log(j);
    lastB=j;
   
    rangeB = sheet.getRange(«B»+lastB+»:B»+lastRow);
    rangeB.clear();

}

В строке с переменной ss указываем id таблицы. Этот срипт будет сортировать цены и ссылки так, чтобы неопределенные значения были внизу таблицы и для них повторно определялась цена. Скрипт можно назвать произвольно. Название функции — sorter:

Триггер можно поставить на каждый час.

5. Создаем в Ads новый скрипт

function main() {
 
   
 function finder(arr,val){
    for (var i=0;i<arr.length;i++)
    {
    var elem = arr[i];
    if (elem[0].toString().toLowerCase()==val.toString().toLowerCase()) {
      return parseFloat(elem[1])}
    //else {return undefined}
    }
  }

   
      // объявляем таблицу с данными
  var ss = SpreadsheetApp.openById(‘1EU6HIvhgeXx1kQq_svBguXWfQCE’); // id таблицы
  var sheet = ss.getSheets()[0];
  var lastRow = sheet.getLastRow();
  //Logger.log(lastRow);
 
  var range = sheet.getRange(«A1:B»+lastRow).getValues(); // В этом range хранятся данные по всем сылкам и ценам
  Logger.log(range);
 
      range.sort(); // сортируем массив
for (var i = range.length — 1; i > 0; i—) {
    if (range[i][0] == range[i — 1][0]) {range.splice( i, 1);}
}
  //Logger.log(range);

// Получаем нужные ключевики
var kw1 = AdWordsApp.keywords()
.withCondition(‘CampaignName = «Указать имя кампании»‘)
.withCondition(«Status = ENABLED»)
.withCondition(«AdGroupStatus = ENABLED»)
.get();
// Если хотите отбирать ключевые слова по каким-то другим условиям, справка по селекторам в помощь:
//https://developers.google.com/google-ads/scripts/docs/reference/adsapp/adsapp_keywordselector?hl=ru#withCondition_1

// Проходимся по словам
while(kw1.hasNext()) {

var kw = kw1.next();
var kwText = kw.getText();
var group = kw.getAdGroup();
var groupName = group.getName();
var ads = group.ads().withCondition(«Type = ‘EXPANDED_TEXT_AD'»).withCondition(«Status = ENABLED»).get();

// Если хотите отбирать объявления по иным условиям, справка по селекторам объявлений в помощь:
//https://developers.google.com/google-ads/scripts/docs/reference/adsapp/adsapp_adselector?hl=ru#withCondition_1 

var ad = ads.next();
var url = ad.urls().getFinalUrl();

var price = finder(range,url);

       if (price != undefined) {
          var bid = price/80;
          if (bid>=10) {bid = 10;}
          Logger.log(‘price — ‘ + price);
          Logger.log(‘bid — ‘ + bid);

          kw.setMaxCpc(bid);}
       
        Logger.log(‘———‘); 
        price = undefined;
      }
}

Этот скрипт берет данные из таблицы и назначает ставки в нужной кампании, исходя из цен на товары. Его выполнение можно поставить на 1 раз в день.

Итоговый график выполнения всех скриптов должен получиться примерно таким: ночью раз в неделю срабатывает скрипт очистки таблицы 1 раз в день; ночью один раз в сутки срабатывает скрипт Google Ads, который собирает ссылки объявлений и отправляет их в таблицу; каждые 15-30 минут срабатывает скрипт получения цен в таблице; каждый час срабатывает скрипт сортировки в таблице; утром или днем (в зависимости от скорости заполнения всех цен в таблице) 1 раз в сутки срабатывает скрипт Google Ads, выставляющий ставки.

При желании вы можете изменить последний скрипт так, чтобы он не назначал ставки, а изменял цены в текстах объявлений, — просто надо проходить селектором не по ключевым словам, а по объявлениям, брать искомый параметр объявления, например описание 2, вырезать из него цену, сверять цену с актуальной, полученной из таблицы. 

Если цены расходятся, то надо создавать дубль объявления, но с уже новой ценой, старый вариант удалять. А если у вас в аккаунте мало объявлений, то можно обойтись и без таблиц, сканируя посадочные страницы в поисках цен непосредственно из этого же скрипта, как это делалось во втором приведенном скрипте. Про такое обновление цен можно было бы написать статью, но каждый сайт и аккаунт Google Ads уникален, поэтому общего решения все равно не выйдет, так что, приложив немного усилий, вы сделаете все сами. Удачи!

Скрипт Google Ads для назначения ставок в торговых кампаниях

И наконец, рассмотрим вариант скрипта Ads для торговой кампании, когда товарный фид оформлен в виде Google-таблицы.

Скрипт Ads забирает из Google-таблицы (фида) данные по ценам и id товаров, сохраняет их в списке, далее он же с помощью итератора и селектора групп продуктов получает активные группы товаров из активных групп объявлений нужных кампаний. Если в вашем аккаунте группы товаров выделяют товары по id, то самым глубоко вложенным элементом группы товаров будет как раз объект эквивалентный одному товару и его значение будет равно id товара:

Для нужного id находим цену товара в списке с id и ценами. Далее назначаем ставку группе продуктов, исходя из коэффициента конверсии сайта и среднего дохода с товара; в примере цена товара просто делится на 80. Скрипт работает только с объектами торговых кампаний, если хотите работать с другими типами, то почитайте в справке о селекторах для нужного формата объявлений. Ну и группировка групп продуктов должна быть оформлена примерно как на скриншоте, чтобы самым вложенным объектом был отдельный товар, выбранный по id, а не по какому-нибудь другому параметру. Если вы группируете товары по брендам или ярлыкам, то скрипт придется подправить.

Код скрипта с комментариями:

 

function main() {
 
  var ss = SpreadsheetApp.openById(‘1CXFJXShdIpZUhkP0’); // Указываем id таблицы с фидом
  var sheet = ss.getSheets()[0];
  var lastRow = sheet.getLastRow();
  //Logger.log(lastRow);
 
  var range = sheet.getRange(«A1:C»+lastRow).getValues(); // В примере берется диапазон A:C, так как id товаров у нас хранятся в A, а цены в C
  //Logger.log(range);
 
  range.sort();
  for (var i = range.length — 1; i > 0; i—) {
    if (range[i][0] == range[i — 1][0]) range.splice( i, 1);
}
 
  Logger.log(range);
  Logger.log(range.length);
 
  function finder(arr,val){
    for (var i=0;i<arr.length;i++)
    {
    var elem = arr[i];
    if (elem[0].toString().toLowerCase()==val.toString().toLowerCase()) {
      return parseFloat(elem[2])} // В квадратных скобках цифра 2, так как работаем с третьим элементом — с ценой
    //else {return undefined}
    }
  }
 
  var productGroups = AdsApp.productGroups()
      .withCondition(«CampaignName = ‘Имя торговой кампании'»)
    .withCondition(«AdGroupStatus = ENABLED»)
      .get();
  while (productGroups.hasNext()) {
    var productGroup = productGroups.next();
    var children = productGroup.children().get(); // Идем до самого вложенного объекта группы продуктов
      while (children.hasNext()) {
        var child = children.next();
        var cid = child.getValue(); // Получаем значение объекта, в нашем случае оно равно id товара
        Logger.log(cid);
        var price = finder(range,cid); // Используем функцию, объявленную в начале скрипта, чтобы найти в полученном из таблицы массиве цену для нужного id товара.

        if (price != undefined) {
          var bid = price/80;
          if (bid>=10) {bid = 10;}
          Logger.log(‘price — ‘ + price);
          Logger.log(‘bid — ‘ + bid);

          child.setMaxCpc(bid);} // Задаем ставку товару — самый вложенный элемент в группе товаров
       
        Logger.log(‘———‘); 
        price = undefined;
      }
   
  }
 
}

Можете поставить однократное дневное выполнение скрипта. В отличии от развернутых текстовых объявлений. мы не можем получить url товара и узнать цену, просканировав ссылку прям из скрипта ads. Поэтому приходится обращаться к фиду в Google-таблице. Если вы используете фиды xml, то можно использовать, в целом, такую же логику, но придется написать дополнительную функцию прохода по фиду для забора цен у всех товаров. Функционал торговых кампаний в скриптах Google Ads не так богат, как для других объектов, но все же можно найти полезные вещи.

Источник: searchengines.ru

Оцените статью
Добавить комментарий