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

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

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

11.06.2021 08:06:47 | Автор: admin

Привет, меня зовут Маша, я работаю маркетинговым аналитиком в 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 крайне удобно. Надеюсь, моя статья вам немного в этом поможет.

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

Подробнее..

Из песочницы Дайджест главных новостей мобайла и ASO за месяц

05.10.2020 12:23:18 | Автор: admin

Каждый месяц готовлю подборку основных новостей мобайла для Hackernoon. Решила, что стоит поделиться русскоязычной версией с Habr. В этом выпуске: iOS 14 привел в негодование разработчиков и помог Pinterest побить дневной рекорд загрузок, измененные правила App Store, попытки Facebook манипулировать Apple, грядущее обновится Google Play Console и другие нашумевшие истории.


Неожиданная iOS 14, кастомизация экрана, виджеты бустят Pinterest


Внезапно (!) вышла новая iOS 14 многие разработчики не успели подготовиться к релизу и были возмущены. Из важного для маркетинга и ASO: мини-приложения, автоисправления в поиске, фичер-карточки (можно почитать про плюсы и минусы здесь). Из неожиданного: виджеты главного экрана стали большой новостью. После выхода iOS 14 приложения для кастомизации главного экрана за несколько дней установили около 5,7 миллионов пользователей. Лидером стал Pinterest, который за день побил дневной рекорд загрузок 616 000 установок.


Коалиция против Apple, коды предложений и зеленый свет игровым стримингам


Кажется, недовольство Apple становится трендом. 13 издателей приложений, включая Epic Games, Deezer, Basecamp, Tile и Spotify создали лигу справедливости Coalition for App Fairness, чтобы призвать Apple пересмотреть политику стора. Apple действительно идет на некоторые уступки. Например, вводит другие варианты офферов коды предложений, которые, например, дают скидки на автоматически возобновляемые подписки. Еще Apple фактически разрешила игровые стриминговые сервисы. Правда, там всё довольно запутано.


Facebook VS Apple: давление и манипуляции двух гигантов


Facebook не отстает и тоже давит на Apple. Возможно, вы слышали об инициативе Facebook запускать платные онлайн-мероприятия, чтобы помочь бизнесу после пандемии. Apple решил до конца года отказывается от 30% комиссии. Все платежи будут обрабатываться через Facebook Pay. Также Facebook собирается продавливать и вопрос с Messenger. Если теперь пользователи iPhone могут по умолчанию выбирать приложения для почты и просмотра веб-страниц, то почему должны довольствоваться единственным инструментом для отправки сообщений? Тем более в мессенджер появилась фича с совместным просмотром видео.


Swift для Windows, AR от Pokmon Go в масштабах планеты, уведомления о заражении COVID-19


Раньше приложения для iOS можно было создавать только на устройствах Apple. Но сейчас границы дозволенного стерлись язык Swift перенесли на Windows. Про другие границы (личные!): Великобритания все же запустила приложение, отслеживающее социальные контакты. Благодаря Bluetooth оно может оповещать о потенциальном заражении COVID-19. У Checkaso есть обзор подобных приложений. А вот создатели Pokmon Go обещают не только обнаружение покоменов (а какую бы вы сделали подводку от ковида к покемонам?). Разработчик предоставит мобильным операторам контент дополненной реальности с поддержкой 5G и будет работать над стандартами этой инновации. Вот такой Planet-Scale AR Alliance намечается.


Эпопея с TikTok: решения суда, солидарность Instagram, альтернатива от YouTube


Последние вести с полей: федеральный суд США заявил, что запрет на TikTok пока не вступит в силу. Пока суд рассматривает законность запрета и решает, представляет ли приложение риск для национальной безопасности. Instagram, несмотря на их Reels, выступает против запрета TikTok. А вот YouTube подсуетился и анонсировал конкурентный формат Shorts. Пока работает только в Индии и на Android, но планы масштабные.


Коротко про ASO: названия конкурентов в метаданных, разная выдача и новая AppGallery


Теперь в метаданных своего приложения в App Store нельзя использовать названия конкурентов, в том числе и в ключевых словах. Еще оказалось, что у поисковых запросов разная выдача в зависимости от iOS, возможная причина пока не все сервисы перешли на парс iOS 14. Бонусом: обновился AppGallery. Если коротко: они продолжает гордиться созданием экосистемы и говорят, что к ней присоединилось уже больше 1,8 миллионов разработчиков. Есть разные размышления о том, может ли AppGallery стать альтернативой известных сторов.


P.S.


Закончу дайджест анонсом 2 ноября ожидается выход обновленной Google Play Concole. Обещают свежий интерфейс и удобную навигацию. И, кстати, про Google: они запретят разработчикам приложений принимать платежи в обход Google Play. В течение года они должны будут перейти на платежную систему компании.

Подробнее..

Категории

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

  • Имя: Макс
    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