Привет, меня зовут Маша, я работаю маркетинговым аналитиком в Ozon. Наша команда "питонит" и "эскьюэлит" во все руки и ноги во благо всего маркетинга компании. Одной из моих обязанностей является поддержка аналитики для команды медийной рекламы Ozon.
Медийная реклама Ozon представлена на разных площадках: Facebook, Google, MyTarget, TikTok и другие. Для эффективной работы любой рекламной кампании необходима оперативная аналитика. В данной статье речь пойдет о моём опыте сбора рекламных данных с площадки TikTok без посредников и лишних заморочек.
Задача на сбор статистики: вводные
У команды медийной рекламы Ozon есть бизнес-аккаунт TikTok, в котором они управляют всей рекламой на этой площадке. Они долго терпели, сами собирали данные из рекламных кабинетов, но всё-таки настало время, когда терпеть уже больше было нельзя. Так у меня появилась задача на автоматизацию сбора статистики из TikTok.
У нас в базах уже были данные о заказах по кампаниям из TikTok, для эффективной аналитики не хватало данных о расходах.
Итак, весь процесс от "нам нужны данные по расходам из TikTok" до "у нас есть данные по расходам из TikTok" разделился для нас на следующие этапы:
регистрация аккаунта разработчика,
создание приложения,
авторизация бизнес-аккаунта в приложении,
запрос, получение, обработка и загрузка данных.
Рассмотрим каждый из этапов подробнее.
Регистрация разработчика
Мы зарегестрировали аккаунт разработчика на нашего бизнес-менеджера. Перешли на портал 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 с авторизационным кодом.
Зашли в приложение и указали
Callback Address
https://www.ozon.ru.Скопировали
Authorized URL
, перешли по нему, авторизовались под аккаунтом бизнес-менеджера.Согласились на предоставление разрешений для приложения, нажали "Confirm".
Далее нас перекинуло на сайт Ozon, но с дополнительными аргументами в url. Получилось наподобие такого
https://www.ozon.ru/?auth_code=XXXXXXXXXXX
.Скопировали значение
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 крайне удобно. Надеюсь, моя статья вам немного в этом поможет.
Полезные ссылки
TikTok Marketing API: официальная документация;
Пример запроса статистики с официальной документации TikTok;
Библиотека request: официальная документация;
Библиотека six: официальная документация;
Библиотека pandas: официальная документация;
Библиотека sqlalchemy: официальная документация.