Майним еще больше данных: настраиваем сбор рекламной статистики TikTok за день

от автора

Привет, меня зовут Маша, я работаю маркетинговым аналитиком в Ozon. Наша команда «питонит» и «эскьюэлит» во все руки и ноги во благо всего маркетинга компании. Одной из моих обязанностей является поддержка аналитики для команды медийной рекламы Ozon.

Медийная реклама Ozon представлена на разных площадках: Facebook, Google, MyTarget, TikTok и другие. Для эффективной работы любой рекламной кампании необходима оперативная аналитика. В данной статье речь пойдет о моём опыте сбора рекламных данных с площадки TikTok без посредников и лишних заморочек.

Задача на сбор статистики: вводные

У команды медийной рекламы Ozon есть бизнес-аккаунт TikTok, в котором они управляют всей рекламой на этой площадке. Они долго терпели, сами собирали данные из рекламных кабинетов, но всё-таки настало время, когда терпеть уже больше было нельзя. Так у меня появилась задача на автоматизацию сбора статистики из TikTok.

У нас в базах уже были данные о заказах по кампаниям из TikTok, для эффективной аналитики не хватало данных о расходах.

Итак, весь процесс от «нам нужны данные по расходам из TikTok» до «у нас есть данные по расходам из TikTok» разделился для нас на следующие этапы:

  1. регистрация аккаунта разработчика,

  2. создание приложения,

  3. авторизация бизнес-аккаунта в приложении,

  4. запрос, получение, обработка и загрузка данных.

Рассмотрим каждый из этапов подробнее.

Регистрация разработчика

Мы зарегестрировали аккаунт разработчика на нашего бизнес-менеджера. Перешли на портал TikTok Marketing API, нажали на «My Apps», далее кликнули на «Become a Developer», и началась череда заполнения форм.

TikTok – не Facebook, у нас ничего ни разу не отклонял, но всё равно мы были очень внимательны при заполнении полей и не добавляли то, что нам не нужно прямо сейчас. Например, в поле «What services do you provide?» добавили только «Reporting».

Последним пунктом был «Create App». Процесс создания аккаунта разработчика и приложения в первый раз происходит вместе.

Создание приложения

Заполняем имя и описание приложение, callback-address. Далее нужно выбрать разрешения, которые приложение будет запрашивать у авторизирующегося в нем аккаунта. Так же, как и при заполнении полей для аккаунта разработчика, выбрали только пункт «Reporting». Указали ID рекламного аккаунта. После этого отправили приложение на проверку.

Как сообщает TikTok в своей документации, проверка может занять от двух до трех рабочих дней. Мы отправили приложение на проверку в пятницу, в понедельник с утра у нас уже было одобренное приложение и можно было продолжить работу.

К сожалению, у меня нет для вас советов на тот случай, если ваше приложение не одобрили. Главное, о чём нужно помнить – это правильно заполнять все обязательные поля и запрашивать разрешения только на то, что действительно необходимо: ни больше, ни меньше.

Авторизация бизнес-аккаунта в приложении

Из всей рутинной работы по заполнению форм, эта часть оказалось самой интересной. У нас не было web-приложения, которое бы отлавливало редирект с авторизационным кодом, поэтому автоматическую авторизацию бизнес-аккаунта сделать не получилось. Но мы оперативно потыкали в кнопки и получили заветный Access Token, с помощью которого собираем данные всех рекламных аккаунтов нашего бизнес-менеджера.

Итак, по порядку, что мы делали не имея сайта, который бы отлавливал callback с авторизационным кодом.

  1. Зашли в приложение и указали Callback Address https://www.ozon.ru.

  2. Скопировали Authorized URL, перешли по нему, авторизовались под аккаунтом бизнес-менеджера.

  3. Согласились на предоставление разрешений для приложения, нажали «Confirm».

  4. Далее нас перекинуло на сайт Ozon, но с дополнительными аргументами в url. Получилось наподобие такого https://www.ozon.ru/?auth_code=XXXXXXXXXXX.

  5. Скопировали значение auth_code, в приложении скопировали secret и app_id и отправили запрос к TikTok на получение long-term Access Token.

curl -H "Content-Type:application/json" -X POST \ -d '{     "secret": "SECRET",      "app_id": "APP_ID",      "auth_code": "AUTH_CODE" }' \ https://ads.tiktok.com/open_api/v1.2/oauth2/access_token

Получили ответ такого вида:

{     "message": "OK",      "code": 0,      "data": {         "access_token": "XXXXXXXXXXXXXXXXXXXX",          "scope": [4],          "advertiser_ids": [             1111111111111111111,              2222222222222222222]     },      "request_id": "XXXXXXXXXXXXXXX" }

Важно было успеть отправить запрос на получение long-term Access Token как можно быстрее, после редиректа на сайт Ozon. Связано это с временем жизни auth_code – 10 минут.

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

Так же при запросах нам понадобиться список advertiser_ids, но его не обязательно сохранять прямо сейчас – список ID аккаунтов всегда можно посмотреть в аккаунте бизнес-менеджера.

Всё, мы готовы писать запросы!

Получение статистики

Когда мы только начинали собирать данные из TikTok, я пользовалась методом, который сейчас depricated, поэтому сразу расскажу о новом.

Итак, у нас есть всё необходимое для получения данных, а именно:

  • access_token,

  • список advertiser_ids.

В результате нужно получить расходы по кампаниям, в группировке до названия рекламного объявления.

media source -> campaign -> adset -> ad_name

Значение media source всегда неизменно, так как источник один – TikTok. По остальным параметрам можно запросить данные из API TikTok.

Теперь нужно было решить, с какой детализацией по времени будем тянуть данные. TikTok позволяет загружать детализацию по часу и дню. Если выгружать детализацию по часу, то, максимум, за один запрос можно получить данные только за один день; если запрашивать детализацию по дням – максимум, на один запрос мы получим 30 дней. Конверсии в покупки анализируются за целый день, поэтому и расходы решили собирать за день.

В новом методе получения данных добавили фильтр по типу размещения рекламы: AUCTION и RESERVATION. Ozon использует только AUCTION в своей стратегии ведения кампаний.

Кроме расходов мы собирали также и другую рекламную статистику по кампаниям: просмотры, клики, количество уникальных пользователей смотревших рекламу и другое. В итоге получился такой список метрик:

METRICS = [     "campaign_name", # название кампании     "adgroup_name", # название группы объявлений     "ad_name", # название объявления     "spend", # потраченные деньги (валюта задаётся в рекламном кабинете)     "impressions", # просмотры     "clicks", # клики     "reach", # количество уникальных пользователей, смотревших рекламу     "video_views_p25", # количество просмотров 25% видео     "video_views_p50", # количество просмотров 50% видео     "video_views_p75", # количество просмотров 75% видео     "video_views_p100", # количество просмотров 100% видео     "frequency" # среднее количество просмотра рекламы каждым пользователем ]

В документации TikTok для каждого метода API описан пример на языках Java, Python, PHP и также curl-запрос. Я использовала пример на Python с небольшими изменениями.

В примерах из документации TikTok используются две дополнительные библиотеки:

pip install requests pip install six

Библиотека requests необходима для удобной отправки get-запросов. Библиотека six используется для генерации url-адреса запроса.

И еще две библиотеки, которые я уже добавила сама для того, чтобы записать данные в базу:

pip install pandas pip install sqlalchemy

В нашей компании для хранения данных используются SQL-подобные хранилища, поэтому я использую pandas для преобразования данных в DataFrame и sqlalchemy для записи DataFrame в базу.

Я использовала функции из примера в документации TikTok для генерации url и отправки запроса.

# генерирует url на основе словаря args с аргументами запроса def build_url(args: dict) -> str:     query_string = urlencode({k: v if isinstance(v, string_types) else json.dumps(v) for k, v in args.items()})     scheme = "https"     netloc = "ads.tiktok.com"     path = "/open_api/v1.1/reports/integrated/get/"     return urlunparse((scheme, netloc, path, "", query_string, ""))  # отправляет запрос к TikTok Marketing API, # возвращает результат в виде преобразованного json в словарь def get(args: dict, access_token: str) -> dict:     url = build_url(args)     headers = {         "Access-Token": access_token,     }     rsp = requests.get(url, headers=headers)     return rsp.json()

На вход функции get нужно передать список аргументов и access token. Список аргументов под наши цели выглядит следующим образом:

args = {     "metrics": METRICS, # список метрик, описанный выше     "data_level": "AUCTION_AD", # тип рекламы     "start_date": 'YYYY-MM-DD', # начальный день запроса     "end_date": 'YYYY-MM-DD', # конечный день запроса     "page_size": 1000, # размер страницы - количество объектов, которое возвращается за один запрос      "page": 1, # порядковый номер страницы (если данные не поместились в один запрос, аргумент инкрементируется)     "advertiser_id": advertiser_id, # один из ID из advertiser_ids, который мы получили при генерации access token     "report_type": "BASIC", # тип отчета     "dimensions": ["ad_id", "stat_time_day"] # аргументы группировки, вплоть до объявления и за целый день } 

Подробнее про page_size: ответ на запрос может содержать большое количество информации и загружать всё это за один раз не эффективно. Поэтому у TikTok есть ограничение на максимальное количество объектов в ответе – 1000. Чтобы получить следующую порцию данных, нужно отправить запрос с теми же входными аргументами на следующую страницу. Подробнее о постраничных запросах ниже.

В ответ на запуск функции get получаем словарь подобного вида.

{        # маркер успешности ответа     "message": "OK",     "code": 0,     "data": {         # информация о странице данных         "page_info": {             # общее количество объектов             "total_number": 3000,             # текущая страница             "page": 1,             # количество объектов на одной странице ответа             "page_size": 1000,             # общее количество страниц             "total_page": 3         },         # массив объектов         "list": [             # первый объект             {                 # метрики                 "metrics": {                     "video_views_p25": "0",                     "video_views_p100": "0",                     "adgroup_name": "adgroup_name",                     "reach": "0",                     "spend": "0.0",                     "frequency": "0.0",                     "video_views_p75": "0",                     "video_views_p50": "0",                     "ad_name": "ad_name",                     "campaign_name": "campaign_name",                     "impressions": "0",                     "clicks": "0"                 },                 # измерения (по каким параметрам группируем результаты)                 "dimensions": {                     "stat_time_day": "YYYY-MM-DD HH: mm: ss",                     "ad_id": 111111111111111                 }             }, ...         ]     },     # id ответа     "request_id": "11111111111111111111111" }

Как я описывала выше, если в ответе получается более 1000 объектов, ответ будет разбит на несколько страниц. В данном случае поле total_page говорит о том, что для получения полного набора данных по указанным параметрам, нужны будут три страницы. Следовательно, запускаем и коллекционируем ответы пока не выгрузим все страницы.

page = 1 # сначала всегда получаем данные по первой странице result_dict = {} # словарь, в который будем записывать ответы result = get(args, access_token) # первый запрос result_dict[advertiser_id] = result['data']['list'] # сохраняем ответ на запрос к первой странице  # пока текущая полученная страница page меньше  # чем общее количество страниц в последнем ответе result while page < result['data']['page_info']['total_page']:     # увеличиваем значение страницы на 1     page += 1     # обновляем значение текущей страницы в словаре аргументов запроса     args['page'] = page     # запрашиваем ответ по текущей странице page     result = get(args, access_token)     # накапливаем ответ     result_dict[advertiser_id] += result['data']['list']

Такое необходимо повторить для каждого рекламного аккаунта из списка advertiser_ids.

В результате всех вышеописанных манипуляций мы получили для каждого рекламного аккаунта данные по рекламным метрикам. Осталось только преобразовать словарь в pandas.DataFrame и отправить их в базу.

# результирующий DataFrame, который будем записывать в базу data_df = pd.DataFrame()  # для каждого рекламного аккаунта выполнить преобразование for adv_id in advertiser_ids:     # получаем накопленные разультаты для аккаунта из словаря     adv_input_list = result_dict[adv_id]     # временный список     adv_result_list = []     # для каждого объекта     for adv_input_row in adv_input_list:         # берём словарь метрик         metrics = adv_input_row['metrics']         # насыщаем этот словарь словарём измерений         metrics.update(adv_input_row['dimensions'])         # добавляем полученный объект во временный список         adv_result_list.append(metrics)      # преобразуем временный словарь в DataFrame      result_df = pd.DataFrame(adv_result_list)     # добавляем колонку со значением id аккаунта     result_df['account'] = adv_id     # добавляем получившийся DataFrame в результирующий     data_df = data_df.append(         result_df,          ignore_index=True     )  # # здесь пропущены некоторые манипуляции  # по преобразованию строк в числа #  # запись данных из результирующего DataFrame в базу data_df.to_sql(     schema=schema,      name=table,      con=connection,     if_exists = 'append',     index = False )

TikTok утверждает, что исторические данные по статистике не меняеются, а если и меняются, то это должна быть экстроординарная ситуация, наподобие аварии в ЦОД. Но на основе опыта получения данных от Facebook, я решила что всё равно буду перезаписывать семь последних дней (цифра семь появилась эмпирически).

В итоге получился вот такой скрипт, который каждый день обновляется данные по TikTok кампаниям за последние семь дней.

Полный текст скрипта.
# импорт библиотек import json from datetime import datetime from datetime import timedelta  import requests from six import string_types from six.moves.urllib.parse import urlencode from six.moves.urllib.parse import urlunparse  import pandas as pd import sqlalchemy  # генерирует url на основе словаря args с аргументами запроса def build_url(args: dict) -> str:     query_string = urlencode({k: v if isinstance(v, string_types) else json.dumps(v) for k, v in args.items()})     scheme = "https"     netloc = "ads.tiktok.com"     path = "/open_api/v1.1/reports/integrated/get/"     return urlunparse((scheme, netloc, path, "", query_string, ""))  # отправляет запрос к TikTok Marketing API, # возвращает результат в виде преобразованного json в словарь def get(args: dict, access_token: str) -> dict:     url = build_url(args)     headers = {         "Access-Token": access_token,     }     rsp = requests.get(url, headers=headers)     return rsp.json()  # обновляет данные в базе за последние семь дней # (или, если указаны start_date и end_date, для периода [start_date, end_date]) def update_tiktik_data(     # словарь с доступами к API TikTok     tiktok_conn: dict,     # словарь с доступами к базе данных     db_conn: dict,     # список id рекламных кабинетов     advertiser_ids: list,     # необязательное поле: начало периода     start_date:datetime=None,     # необязательное поле: окончание периода     end_date:datetime=None ):     access_token = tiktok_conn['password']     start_date = datetime.now() - timedelta(7) if start_date is None else start_date     end_date = datetime.now() - timedelta(1) if end_date is None else end_date      START_DATE = datetime.strftime(start_date, '%Y-%m-%d')     END_DATE = datetime.strftime(end_date, '%Y-%m-%d')     SCHEMA = "schema"     TABLE = "table"     PAGE_SIZE = 1000     METRICS = [         "campaign_name", # название кампании         "adgroup_name", # название группы объявлений         "ad_name", # название объявления         "spend", # потраченные деньги (валюта задаётся в рекламном кабинете)         "impressions", # просмотры         "clicks", # клики         "reach", # количество уникальных пользователей, смотревших рекламу         "video_views_p25", # количество просмотров 25% видео         "video_views_p50", # количество просмотров 50% видео         "video_views_p75", # количество просмотров 75% видео         "video_views_p100", # количество просмотров 100% видео         "frequency" # среднее количество просмотра рекламы каждым пользователем     ]      result_dict = {} # словарь, в который будем записывать ответы     for advertiser_id in advertiser_ids:         page = 1 # сначала всегда получаем данные по первой странице         args = {             "metrics": METRICS, # список метрик, описанный выше             "data_level": "AUCTION_AD", # тип рекламы             "start_date": START_DATE, # начальный день запроса             "end_date": END_DATE, # конечный день запроса             "page_size": PAGE_SIZE, # размер страницы - количество объектов, которое возвращается за один запрос              "page": 1, # порядковый номер страницы (если данные не поместились в один запрос, аргумент инкрементируется)             "advertiser_id": advertiser_id, # один из ID из advertiser_ids, который мы получили при генерации access token             "report_type": "BASIC", # тип отчета             "dimensions": ["ad_id", "stat_time_day"] # аргументы группировки, вплоть до объявления и за целый день         }         result = get(args, access_token) # первый запрос         result_dict[advertiser_id] = result['data']['list'] # сохраняем ответ на запрос к первой странице          # пока текущая полученная страница page меньше,          # чем общее количество страниц в последнем ответе result         while page < result['data']['page_info']['total_page']:             # увеличиваем значение страницы на 1             page += 1             # обновляем значение текущей страницы в словаре аргументов запроса             args['page'] = page             # запрашиваем ответ по текущей странице page             result = get(args, access_token)             # накапливаем ответ             result_dict[advertiser_id] += result['data']['list']      # результирующий DataFrame, который будем записывать в базу     data_df = pd.DataFrame()      # для каждого рекламного аккаунта выполнить преобразование     for adv_id in advertiser_ids:         # получаем накопленные разультаты для аккаунта из словаря         adv_input_list = result_dict[adv_id]         # временный список         adv_result_list = []         # для каждого объекта         for adv_input_row in adv_input_list:             # берем словарь метрик             metrics = adv_input_row['metrics']             # насыщаем этот словарь словарём измерений             metrics.update(adv_input_row['dimensions'])             # добавляем полученный объект во временный список             adv_result_list.append(metrics)          # преобразуем временный словарь в DataFrame          result_df = pd.DataFrame(adv_result_list)         # добавляем колонку со значением id аккаунта         result_df['account'] = adv_id         # добавляем получившийся DataFrame в результирующий         data_df = data_df.append(             result_df,              ignore_index=True         )      #     # здесь пропущены некоторые манипуляции      # по преобразованию строк в числа     #          # создание подключения к базе     connection = sqlalchemy.create_engine(         '{db_type}://{user}:{pswd}@{host}:{port}/{path}'.format(             db_type=db_conn['db_type'],              user=db_conn['user'],              pswd=db_conn['password'],             host=db_conn['host'],             port=db_conn['port'],             path=db_conn['path']          )     )      # удаление последних семи дней из базы     with connection.connect() as conn:         conn.execute(f"""delete from {SCHEMA}.{TABLE}          where date >= '{START_DATE}' and date <= '{END_DATE}'""")      # запись данных из результирующего DataFrame в базу     data_df.to_sql(         schema=SCHEMA,          name=TABLE,          con=connection,         if_exists = 'append',         index = False     )

Миссия выполнена!

Подведем итоги

Итого, на всевышеописанные действия было потрачено менее одного рабочего дня (не считая времени, которое приложение было на проверке). Надеюсь, мне удалось показать, что уровень вхождения в API TikTok достаточно низкий, и для настройки автоматического сбора данных не нужно обязательно привлекать разработчика или блуждать по лабиринтам документации и запутанной логики.

К слову о лабиринтах, в Facebook тот же самый один рабочий день уходит на то, чтобы создать аккаунт разработчика, протыкать все галочки о политике конфидециальности и условий использования, создать приложение, настроить его и т.д. И в итоге к концу дня у тебя не работающий ETL по сбору данных, а очередной Permission Denied и распухшая голова, в которой крутится только одна мысль – «что я делаю не так».

Конечно, сравнивать Facebook и TikTok не очень правильно: второй ещё относительно молод и ему еще только предстоит быть обвешанным хитрыми условиями, запретами и всеми возможными сложностями. Но сейчас всего этого пока нет, так что пользоваться TikTok Marketing API крайне удобно. Надеюсь, моя статья вам немного в этом поможет.

Полезные ссылки

ссылка на оригинал статьи https://habr.com/ru/company/ozontech/blog/562266/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *