Оповещения об остатках по счёту на уровне всего MCC для Google Ads (Скрипт на Python)

Наверняка у вас случалась ситуация, когда счет на оплату рекламы в Google был выставлен поздно или реклама вовсе «неожиданно» останавливалась. И происходило это не по вине клиента. Просто сервис Google Ads, в отличие от Яндекс.Директ, не присылает оповещений при минимальных остатках баланса на счету или после окончания средств. Чтобы избежать проблем, приходится держать все в голове или регулярно проводить мониторинг вручную, на что, как правило, не хватает времени.  

Мы сделали простой в управлении инструмент, который решил проблему с оповещениями. Для этого обратились к языку программирования Python и написали скрипт. К слову, скрипт состоит из нескольких универсальных функций. Их можно использовать по отдельности, если возникнет такая необходимость.

Но начнем с начала

Во-первых, зарегистрируйтесь, чтобы получить доступ к сервису AdWords API https://developers.google.com/adwords/api/docs/guides/signup?hl=ru,

Во-вторых, перейдите к шагу «Создание запроса» https://developers.google.com/adwords/api/docs/guides/first-api-call

Вы получите файл конфигурации, при помощи которого можно авторизовываться в сервисах API. Просто идите по порядку, и все получится.

Теперь о работе со скриптом

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

Для работы потребуются следующие модули. Если какой-то из них выдает ошибку, что модуль отсутствует, установите его через pip pip install package_name.

from googleads import adwords
import sys
import io
import csv
from datetime import datetime as dt
import pandas as pd
import re
import time
from requests.exceptions import ConnectionError
from yaml import load
import numpy as np
#модули для email рассылки
import smtplib, os
import string
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.utils import COMMASPACE, formatdate
from email import encoders

Далее объявляем нужные переменные и токены

#получаем сегодняшнюю дату
today = dt.now()
#указываем пусть до файла конфигурации в текущей рабочей директории
config_path = os.getcwd()+'\\config.yaml' 
#читаем файл конфигурации
config = load(open(config_path , mode = 'r', encoding = 'utf-8'))
#токены и логины
path_to_yaml = config['adsYamlMg']
#объявляем токены из файла yaml
client = adwords.AdWordsClient.LoadFromStorage(path_to_yaml)

Задаем список стоп-слов, по которым будем отфильтровывать те аккаунты, которые их содержат.

#Список стоп-слов в виде регулярного выражения
stop_words = ['.*архив.*','.*стоп.*','.*останов.*','.*exper.*','.*экспериме.*',
              '.*размещает.*','.*удал.*','.*сбер.*','.*test.*']

Для проверки баланса и расчета оставшихся средств по дням, объявляем 4 функции:

“check_limit” – для проверки доступного лимита бюджета в аккаунте Ads.

‘’check_accounts’’ – получение и фильтрация аккаунтов для проверки.

– Функция “get_cost_budget_time” и “get_cost_7days”, для получения расхода за последние 7 дней и с момента создания активного бюджета.

“check_limit”

Особое внимание заслуживают параметры селектора. Берутся максимум 2 бюджета (numberResults) (если таковые есть), на тот случай, если один из них является текущим, а другой запланированным. Сортировка по id бюджета (ordering) позволяет брать именно 2 последних созданных.

def check_limit(client,today):
    '''Возвращает дату создания бюджета и лимит бюджета клиента'''
    #Используем метод BudgetOrderService
    service = client.GetService('BudgetOrderService', version='v201809')
    #Выбираем параметры селектора
    selector = {'paging':{'startIndex':0,
                          'numberResults': 2}, #Берем 2 последних созданных бюджета
                'ordering': [{'field': 'Id',
                              'sortOrder': 'DESCENDING'}]} #сортировка бюджетов по убыванию id
    #получение страницы с бюджетами аккаунта с параметрами selector
    budget_page=service.get(selector)
    if ('entries' in budget_page) and (budget_page['entries'] != []):
        for budget in budget_page['entries']:
            current_budget = {}
            #Забираем лимит и переводим в рубли
            spending_limit = round(float(budget['spendingLimit']['microAmount'])/1000000,2)
            #Забираем дату создания бюджета
            start_date = budget['startDateTime'].split(' ')[0]
            #проверка действующий ли это бюджет или запланированный
            if today >= dt.strptime(start_date,'%Y%m%d'):
                #Сохраняем в случае успеха
                current_budget = {'spending_limit':spending_limit,
                                  'start_date':start_date}
                return current_budget
                break
            else:
                #Если бюджет запланированный берем след. из 2х
                next

‘’check_accounts’’

В функции загружается страница с аккаунтами доступными в MCC (My Client Center).

На странице 2 листа:

– С информацией об управляющем аккаунте по ключу link. Оттуда мы выбираем управляющие аккаунты, чтобы потом исключить их при проверке.

– С информацией об аккаунте, названии, ярлыках и т.п. по ключу entries.

Далее идет проверка, чтобы сущности были заполнены, а также не содержали выбранных стоп-слов. На выходе получается лист словарей.

def check_accounts(client):
    '''Возвращает список аккаунтов для проверки баланса, client - агентство'''
    #Используем метод ManagedCustomerService
    managed_customer_service = client.GetService('ManagedCustomerService', version='v201809')
    #Определяем параметры селектора
    selector = {'fields': ['CustomerId', 'Name', 'AccountLabels'], #Выбираем нужные поля
                'paging': {'startIndex': 0,
                           'numberResults': 5000}} #Берем все элементы с 0-го максимум 5000
    #Получаем страницу со всеми аккаунтами                    
    page = managed_customer_service.get(selector)
    #выбираем уникальные MCC
    manager_customer_id = []
    for link in page['links']:
        if link['managerCustomerId'] not in manager_customer_id:
            manager_customer_id.append(link['managerCustomerId']) 
    accounts = []      
    for entry in page['entries']:
        #Проверяем есть ли нужный ключ и не пустой ли словарь по ключу
        if 'accountLabels' in entry and entry['accountLabels'] != None:
            #Получаем лист ярлыков аккаунта
            account_labels = [x['name'] for x in entry['accountLabels']]
        if entry['customerId'] in manager_customer_id:
            #Если аккаунт является MCC пропускаем его
            next
        elif (entry['name'] == None) or (entry['customerId'] == None):
            #Если возращено пустое значение пропускаем его
            next
        elif re.findall('|'.join(stop_words),"|".join(account_labels),re.IGNORECASE)!=[]:
            #Если стоп слова есть в ярлыках пропускаем аккаунт
            next
        else:
            #Преобразуем id аккаунта в вид XXX-XXX-XXXX
            customer_id = '-'.join([str(entry['customerId'])[0:3],
                                    str(entry['customerId'])[3:6],
                                    str(entry['customerId'])[6:10]])
            #Записываем нужные значения в словарь
            current_account = {'customer_id': customer_id,
                               'name': entry['name'],
                               'account_labels': '|'.join(account_labels)}
            accounts.append(current_account) #Сохраняем в лист
    return accounts

‘’ get_cost_7days’’ и “get_cost_budget_time”

Разница между лимитом бюджета и расходом составляет остаток баланса. Разница между функциями составляет только в выгружаемом периоде.  В качестве периода дат в get_cost_7days используется ‘LAST_7_DAYS’

def get_cost_budget_time(client, date):
    #Выбираем класс загрузчика отчетов
    report_downloader = client.GetReportDownloader(version='v201809')
    #Составляем отчет
    report_query = (adwords.ReportQueryBuilder()
                    .Select('AccountDescriptiveName', 'Cost', 'AccountCurrencyCode') 
                    .From('ACCOUNT_PERFORMANCE_REPORT') #Тип отчета
                    .During(date) #Период дат 'YYYY-MM-DD,YYYY-MM-DD'
                    .Build())
    result = []
    report = (report_downloader
              .DownloadReportAsStringWithAwql(
                      report_query,
                      'CSV',
                      skip_report_header=True, #Заголовки отчета отключены
                      skip_column_header=False, #Заголовки колонок включены
                      skip_report_summary=True)) #Результаты итого отключены
    file = io.StringIO(report)
    reader = csv.DictReader(file, delimiter=',')
    result = [line for line in reader]
    return result

Далее запускаем все функции в цикле по листу полученных аккаунтов. Также используем обработчик ошибок (ConnectionError) try/ except. Если запрос не был отправлен, вам дается еще 2 попытки.

#Получаем аккаунты
accounts = check_accounts(client)
tries = 3 #Количество попыток
c = 0 #Счетчик обработанных аккаунтов
report = []
current_account = {}
for account in accounts:
    c += 1
    for i in range(tries):
        print('Обработка {} из {}'.format(c,len(accounts)))
        #Задаем обработчик ошибок
        try:
            print(account['customer_id'])
            #Меняем id аккаунта на отобранный
            client.SetClientCustomerId(account['customer_id'])
            #Получаем лимит
            limit  = check_limit(client,today)
            if limit == None:
                break # Если лимита нет выходим из цикла
            cost_7_days = get_cost_7days(client) #Получаем расход
            if cost_7_days == []:
                cost_7_days = [{'Cost':0}] #Если расхода не было подставляем 0
            date = limit['start_date'] + ',' + today.strftime('%Y%m%d')
            cost_time = get_cost_budget_time(client, date)
            if cost_time == []:
                cost_time = [{'Cost':0}] #Если расхода не было подставляем 0
            #Записываем параметры в словарь
            current_account = {'Название аккаунта': account['name'],
                               'ID Аккаунта': account['customer_id'],
                               'Ярлыки': account['account_labels'],
                               'Лимит':  limit['spending_limit'],
                               'Расход Баланс': int(cost_time[0]['Cost'])/1000000,
                               'Расход 7 дней':  int(cost_7_days[0]['Cost'])/1000000}
            report.append(current_account) #Сохраняем в лист
        except ConnectionError:
            print('Ошибка соединения')
            #Если ошибка соединения, кол-во попыток не израсходовано
            #продолжаем выполнять
            if i < tries - 1: # i начинается с 0
                print('Продолжаем работать, попытка {}, аккаунт {}'.format(
                        i+1, account['customer_id']))
                time.sleep(5)
                continue
            else:
                print('Попытки исчерпаны')
                raise
        break

Полученный репорт переводим в DataFrame для более удобной дальнейшей обработки. Рассчитываем новые столбцы.

#Переводим результат в DataFrame
df_ads = pd.DataFrame(report)
#Считаем баланс
df_ads['Баланс'] = df_ads['Лимит'] - df_ads['Расход Баланс']
#Меняем отрицательные значения на 0 если такие есть
num = df_ads._get_numeric_data()
num[num<0]=0
#Высчитываем остаток дней
df_ads['Осталось дней'] = df_ads['Баланс']/(df_ads['Расход 7 дней']/7)
df_ads['Осталось дней'].replace([np.inf, -np.inf, np.nan], 0, inplace = True)
df_ads['Осталось дней']= df_ads['Осталось дней'].round(2) #Округление до 2 знаков после ','
#Оставляем нужное
df_ads = pd.DataFrame(df_ads, columns=['ID Аккаунта', 'Название аккаунта','Баланс', 'Осталось дней'])
df_ads['Площадка'] = 'Google' #Добавляем название площадки

На выходе у вас получается таблица со всеми нужными данными.

Пример рассылки на почту

В качестве источника email адресов будем использовать CSV файл, без заголовков. В файле каждая строка соответствует почте и фамилии менеджера, которую мы будем искать в ярлыке. Соответственно, файл внутри будет выглядеть так (в зависимости от платформы используйте соответствующую кодировку);

Петров, petrov@example.ru

Иванов, ivanov@example.ru

with open ('emails.csv','r', encoding = 'UTF-8') as emails:
    for email in emails:
        curerent_account = email.strip().split(',') #Разделитель в файле
        #Выбираем нужный ярлык и переводим DataFrame в html
        html = df_ads[df_ads['Ярлыки'].str.contains(curerent_account[0])].to_html()
        recipients = [curerent_account[1]] #Забираем из строки файла email
        def send_mail( send_from, subject, text, server="localhost"):
            msg = MIMEMultipart()
            #Параметры письма
            msg['From'] = send_from
            msg['To'] = ', '.join(recipients)
            msg['Date'] = formatdate(localtime = True)
            msg['Subject'] = subject
            part = MIMEText(html, 'html') #Формируем вложение
            msg.attach(part) #Вложение
            smtp = smtplib.SMTP(server)
            smtp.sendmail(send_from, recipients, msg.as_string())
            smtp.quit()
        send_mail('Alert <no-reply@example.ru>','Остаток баланса аккаунтов Ads','Таблица:')
        time.sleep(5)

print('Время выполнения %s' % (dt.now()-today))

Итог

Вариантов использования данного скрипта может быть несколько. Наверное самый правильный, добавить его в cron, скажем с расписанием на 8.00 и 17.00. Так ваши коллеги по приходу на работу и перед уходом домой, смогут посмотреть сколько средств на аккаунтах, пополнить баланс или своевременно выставить счет.  

Также своим аккаунт-менеджерам вместе с остатками по Ads мы отправляем и данные по Яндекс.Директ. Если вам интересно данное решение — пишите в комментариях «хочу продолжения» и мы выпустим дополнение к данной статье.

Следите за нами в соцсетях и подписывайтесь на рассылку!