Русский
Русский
English
Статистика
Реклама

Майним еще больше данных настраиваем сбор рекламной статистики 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 requestspip install six

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

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

pip install pandaspip 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 меньше # чем общее количество страниц в последнем ответе resultwhile 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 jsonfrom datetime import datetimefrom datetime import timedeltaimport requestsfrom six import string_typesfrom six.moves.urllib.parse import urlencodefrom six.moves.urllib.parse import urlunparseimport pandas as pdimport 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 крайне удобно. Надеюсь, моя статья вам немного в этом поможет.

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

Источник: habr.com
К списку статей
Опубликовано: 11.06.2021 08:06:47
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании ozon tech

Веб-аналитика

Медийная реклама

Аналитика мобильных приложений

Социальные сети и сообщества

Tiktok

Социальные сети

Маркетинг

Мобильная реклама

Мобильный маркетинг

Facebook api

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru