📱

Get Our Mobile App

Take your business learning on the go!

Download on the App StoreGet it on Google Play

Ozon API | Автоматизируем аналитику продвижения

Алексей Куличевский39:38

Transcription

Привет друзья! Добро пожаловать обратно на канал. Я Алексей Личес. А в этом видео мы научимся работать с API Озона. А именно, мы научимся выгружать статистику из их API продвижения. Это их внутренняя рекламная система, с помощью которой можно всячески продвигать свои товары.

Это видео будет интересно не только предпринимателям, которые торгуют на Озоне, но ещё и тем, кто в принципе изучает, как работать с API в Python. Потому что у Озона API сделан очень интересно. Там есть несколько дизайн-паттернов, не совсем типичных для других API. Например, их API асинхронный. То есть, когда мы просим его отдать нам какие-то данные, он нам не сразу отдаёт данные. Он сначала создаёт задачу для своей внутренней системы, которая постепенно готовит отчёт, а потом только мы можем этот отчёт скачать. И во-вторых, он отдаёт нам не, например, JSON, как делают большинство API, а отдаёт нам файл. Причём не просто файл, а заархивированный. Если мы хотим из этого файла достать данные, мы сначала должны разархивировать его. Там, кстати, внутри не один файл с данными, а несколько. И потом мы должны по ним по всем пройтись и достать нужную нам информацию. В общем, интересно. Давайте посмотрим, как он работает.

Работа с любым API начинается с поиска документации этого API. Давайте так и сделаем. Напишем "Ozone API документация". Отлично! Так, документация по API для Seller и Performance. Насколько я знаю, у Озона есть два различных API. Seller – это набор методов для непосредственно управления товарами, остатками, ценами, штрих-кодами, вот этим всем. И Performance – это то, что нам нужно. Это интерфейс для работы с рекламным кабинетом. Поехали!

Чтобы использовать API, нам нужно получить Client ID и Client Secret, которые мы можем получить в личном кабинете. Тут я, я вам, к сожалению, не могу показать, как это всё выглядит. Тут где-то в документации, по-моему, были скриншоты, но я их не могу что-то найти. В общем, тут, если у вас есть магазин, вы входите, регистрируетесь и создаёте специальный, так называемый, сервисный аккаунт или он называется просто аккаунт. Таким образом, вы как бы... [музыка] регистрируетесь. Это типа пароль, и их надо будет сохранить и потом использовать уже непосредственно в. У меня уже есть ID и Secret. Мне их выдал мой, собственно, интернет-магазин, с которым мы будем работать. Давайте я вам покажу, как они выглядят. Откроем наш VS Code. Давайте я создам новый файл. И вот у меня есть Client ID вот такой вот. Это, на самом деле, тип email. Если обратить внимание, у нас есть какой-то набор символов, потом собака и адрес домена. Точно так же, как и в любых других email. То есть, когда пользователь, человек регистрируется в Озоне, он указывает свой email. И вот пользователь, программа с точки зрения Озона, он на самом деле очень похожая сущность. Поэтому у него тоже айтишник – это email. И Secret – это такая длинная последовательность символов, такой длинный. Ну так вот, Client ID, Secret мы получили и сохранили. И теперь начинаем работать с API.

И первое, что он нам предлагает, это нужно получить API ключ. Это может само по себе сбить с толку. Мы же только что получили какие-то логин и пароль. Но фишка в том, что Озоновский API сделан таким образом, что чтобы получить доступ к основным методам, нам нужно сначала получить специальный токен, специальный ключ. Для получения которого, в свою очередь, нам нужны Client ID и Client Secret. То есть, мы сначала берём, превращаем ID и Secret в ключ, который действует всего 1800 секунд, то есть 30 минут. И вот, пока этот ключ действует в течение 30 минут, мы можем отправлять запросы ко всем другим методам, получать данные, как что-нибудь там управлять, запускать компании и так далее. Когда полчаса закончится, нам нужно будет повторить процесс, отправить запрос ещё один раз и получить новый токен. И так нужно делать каждый раз, каждый раз, каждый раз. Это сделано специально, чтобы увеличить безопасность доступа в API. Допустим, каким-то образом вот мы получаем этот Access Token, и он куда-нибудь убежит, и злоумышленник таким образом получит доступ к нашей рекламе. Но даже если это произойдёт, то через полчаса злоумышленник доступ потеряет.

Ну что, поехали? Напишем первый код. Вообще, что он нам здесь предлагает? Он нам говорит, что для получения авторизации нужно отправить вот такой запрос, и мы ожидаем, что в ответ получим вот такой ответ. Как работать с этой штукой? То есть, что здесь написано? Здесь написано, что нам нужно отправить POST-запрос вот на этот адрес. Вернее, так, на эту страницу этого адреса. А нам нужно в хедере указать Content-Type: Application/json, и что Accept тоже Application/json. Это два ключа с двумя значениями. И дальше в теле запроса нам нужно передать вот такой вот словарь. И в ответ мы ожидаем получить вот словарь. Здесь запрос описан универсальным языком, то есть такие основные всякие параметры. Но мы будем пользоваться Python, и нам нужно понимать, как вот эту штуку превратить из вот этого описания, которое мы видим, в Python-ский код. Скопирую всё, что у нас здесь написано. Перейдём в VS Code. Давайте сразу создадим новый файл. Назовём его что-нибудь типа app.py. Или даже нет, мы же с вами ещё даже виртуальное окружение не создали. Давайте сначала создадим виртуальное окружение. В терминале напишем `python -m venv venv`. Создаётся виртуальное окружение. Создалось. Давайте его теперь активируем. И тут нам по-хорошему надо бы сразу установить пару библиотек. Мы с вами будем работать с API, будем отправлять всякие запросы, поэтому нам точно понадобится библиотека `requests`. У нас есть всякие секретные значения типа Client ID, Client Secret, которые мы захотим записать в виртуально в переменные окружения, читать их оттуда, поэтому нам понадобится `python-dotenv`. Ну и давайте, нам наверняка понадобится `pandas`, потому что мы будем работать со статистикой, с аналитикой, которая будет наверняка в каких-нибудь табличных форматах. Лишним не будет. Устанавливаем. Почти установилось. И теперь уже давайте создадим новый файл. `app.py`. Терминал пока можно скрыть. И давайте напишем `import requests`. Установим Python kernel. У меня VS Code сам подсказывает. Готово. Так. И теперь, наконец-то, скопирую всё, что у нас здесь написано, и давайте начнём конвертировать это в Python-код.

Первое, у нас будет POST-запрос. Мы можем написать `url = requests.post`. Дальше у нас будет вот такой хост. Давайте прямо сделаем `host =`. А вот это будет наш endpoint. `endpoint =`. HTTP 1.1 – это нас не интересует. И Content-Type, и Accept мы пропишем. Это скопирую сюда и вставлю. И, естественно, давайте это превратим в строку. Это тоже превратим в строку. И это отлично. Ну и последнее, нам нужен наш вот этот. Назовём его `payload` равно вот этот словарь. Но вместо вот этих значений мне нужно подставить те Client ID, Client Secret, которые у нас есть. Как это сделать? Ну, давайте сначала мы с вами создадим файл `.env`. Теперь эти все штуки мы в этот файл запишем и сохраним. И в коде мы добавим `import os` и `from dotenv import load_dotenv`. Я эти штуки использую каждый раз практически во всех проектах. Что делает эта функция? Давайте мы её сразу выполним. Она ищет `.env` в нашей директории, читает всё его содержание и записывает в окружение соответствующие переменные: Client ID и Client Secret. Так что теперь мы с помощью библиотеки `os` можем прочитать их в коде. Например, `client_id = os.getenv('CLIENT_ID')`. Например, вот наш Client ID. Теперь мы в наш `payload` можем сюда добавить `client_id=client_id`, `client_secret=client_secret`. Ну и вроде всё. Теперь мы можем попробовать это всё выполнить. `requests.post`. Ну да, конечно, мы `headers` и `endpoint` в отдельных переменных сохранили, а вот в `requests` передать забыли. В общем, мы всё забыли передать `requests`. Давайте мы сделаем `host + endpoint`. То есть складываем вот эти штуки вместе. Дальше у нас `headers = headers`. А, наверное, так как у нас POST-запрос, нам нужно написать что-нибудь типа `json=payload`. Пробуем так. Ответ 200. Уже хорошо. Текст. Супер! Вот он, получилось. Теперь смотрите, мы можем так сделать: `response.json()`. И вуаля! Мы получили в ответ словарь с тремя ключами: `access_token`, `expires_in` (в секундах) и `token_type`. Значит, теперь мы можем сделать что-то такое: `access_token = response.json()['access_token']`. Что у нас получилось? Теперь у нас есть переменная `access_token` с вот такой строкой, которая и будет являться паролем доступа ко всем остальным методам, ко всем остальным данным.

Поехали дальше. API ключ мы получили. Теперь давайте начнём доставать данные. Ну, давайте пойдём в раздел "Статистика", так как нам нужна статистика, и посмотрим. Так, даты группируются по московскому времени. О'кей. Статистика по компании. Отлично. Очень похоже на то, что нам нужно. В запросе укажите временной промежуток с помощью `from_date` и `to_date`. О'кей. Ага, Performance API асинхронный. Поэтому в результате запроса вам будет не сам отчёт, а уникальный идентификатор отправленного запроса, с помощью которого можно проверить статус формирования отчёта и скачать отчёт. Формат отчёта CSV. Ага, отлично. Сейчас покажу, как эта вся штука работает. Значит, для того, чтобы получить статистику, нам нужно будет отправить POST-запрос вот по этому адресу с вот этими параметрами: `campaign_ids`, `from_date`, `to_date`. `from_date`, видимо, это одни и те же значения, типа начальная дата периода и конечная дата периода. Я не знаю, почему их тут два варианта, видимо, старая и новая. Ага, максимальный период, за который можно получить отчёт – 62 дня. О'кей. И группировка. Мы можем группировать данные. Видимо, если мы не укажем группировку или укажем `no_group_by`, то он нам вернёт просто небольшой набор чисел: сколько всего за этот период было там показов, кликов, расходов. Если мы сгруппируем, мы потратили в каждом месяце. Но самое важное, что мы здесь видим, это поле `campaign_ids`, которое `required`. Нам обязательно в запросе нужно передать список идентификаторов компаний, для которых подготовить отчёт. А где нам взять этот список компаний? У нас их ещё нет. Видимо, нам нужно найти отдельный метод. Вот здесь "Компании и рекламируемые объекты", которые нам может отдать список компаний. Супер! Давайте вот это и сделаем.

Чтобы получить список компаний, нам нужно отправить GET-запрос вот по этому адресу с вот этими параметрами. Ага, параметры у нас: `campaign_ids` – мы можем либо, видимо, перечислить ID компании, которые мы хотим получить, если мы их знаем. Это не обязательный параметр, его можно опустить. Дальше мы можем указать тип рекламной компании, которую мы хотим получить, и её статус. Что она значит: у нас в процессе, запланирована, остановлена, неактивна, заархивировано. Вот все возможные статусы рекламных кампаний. Интересно, что все поля необязательные, и теоретически мы можем просто отправить пустой запрос и получить все рекламные кампании. Давайте так и сделаем. Скопирую endpoint. Перейдём обратно в VS Code. Вот он наш endpoint. Он у меня скопировал сразу с хостом. И тут даже ещё прописан отдельно порт зачем-то. Ну, раз прописан, так и будем использовать. Сохраним это в переменной. И попробуем просто отправить пустой GET-запрос. `response = requests.get(url)`. И, естественно, нам нужно как-то авторизоваться. Нужно в `headers` что-то указать. Я подозреваю, что нам нужно указать вот этот `access_token`. Давайте посмотрим, как его указывать. А вот так, да? Вот пример запроса. У нас нужно в заголовке указать вот такую штуку. Что это значит? Мы пишем `headers = {'Authorization': 'Bearer <YOUR_ACCESS_TOKEN>'}`. Дальше `Authorization` – это будет ключ. А вот это будет значение. Слово `Bearer` мы оставляем. Вместо вот этой штуки нам нужно прописать `access_token`. И добавим буковку `f`, чтобы Python сразу стал читать значение переменной `access_token`. Вот отсюда, вот сюда. И в самом запросе мы пишем `headers=headers`. Выполняем. `response.status_code == 200`. Уже хорошо. Текст. Кажется, сработало. Вот мы получили огромный JSON. Давайте сохраним его переменной `campaigns`. И посмотрим на них внимательно. `campaigns`. Так, ну, во-первых, это у нас словарь. Огромный словарь. Давайте посмотрим, какие в словаре ключи: `items` и `total`. Давайте посмотрим, что такое `total`. 407. А, видимо, это сколько всего компаний у нас есть. У нас есть всего 407 компаний. Давайте посмотрим на `items`. Угу. Вот `items` – это у нас уже список каких-то словарей. Но я подозреваю, что это список компаний. Давайте посмотрим на первую компанию. У неё есть `id`, `name`, `status`, `type` и вот всякие ещё параметры. Отлично. Давайте глянем, как называются активные компании. По-моему, вот. А что, если мы найдём сейчас `active_campaigns = [c for c in campaigns['items'] if c['status'] == 'active']`? То есть, таким образом, мы фильтруем наш список и ищем только те компании, которые сейчас вот в этом статусе, то есть которые прямо сейчас активны. Да, вот что-то мы нашли. Давайте посмотрим, сколько их. Пять. У нас пять активных компаний. Давайте назовём их `active_campaigns`. Так. О'кей. И во всех этих компаниях у нас есть ID. Мы можем на сделать что-нибудь типа `active_campaign_ids = [c['id'] for c in active_campaigns]`. И вот у нас ID наших активных компаний. Давайте сохраним это в переменной вот здесь: `active_campaign_ids`. Супер! Мы, в принципе, можем сделать то же самое и со всеми компаниями, но я думаю, активных нам сейчас для демонстрации того, как работает API, должно хватить.

Ну что, компании мы нашли. Теперь давайте скачаем статистику. Возвращаемся в раздел "Статистика". Вот наш POST-запрос. Давайте скопирую endpoint. Вернёмся вот сюда. `url =`. Так. И вот смотрите, у нас здесь прямо пример того, как может выглядеть запрос. Давайте прямо скопирую. Вернёмся в код, вставим. Назовём это `payload = {'campaign_ids': active_campaign_ids, 'from_date': '2024-01-01', 'to_date': '2024-01-31'}`. `from_date`, `to_date` – это мы использовать не будем, потому что помните, у нас здесь написано, что можно использовать либо `from_date` и `to_date` для дат, либо `date_from` и `date_to`. Если заполнены все четыре, то в ответе будет статистика по временному промежутку из `date_from` и `date_to`. Видимо, у них приоритет. Давайте их будем использовать тогда. Так, здесь нам нужно передать строки. А какие строки? Ну, наверное, давайте мы зададим `date_from`. Знаете, как мы сделаем? Мы наверху добавим `from datetime import datetime`. И, наверное, ладно, о'кей. Да, ставим `datetime`. Это не обязательно, потому что мы прямо здесь, в принципе, можем написать вот так: `date_from='2024-01-01'` и `date_to='2024-01-31'`. Например, вот так. То есть, вот скачаем данные за месяц. Да, наверное, наверное, давайте так и сделаем. Да, `date_from` уберём. Просто на будущее. На практике, если уж мы работаем с датами, то у нас нам нам может понадобиться из этих дат, например, между ними посчитать там расстояние, ну, в смысле, промежуток, сколько дней между первой датой, второй. Нам нужно будет, нам может понадобиться найти сегодняшнюю дату, вот это всё. И со строками, естественно, так делать неудобно. Поэтому библиотека `datetime` часто используется для решения таких задач, потому что она содержит большое количество методов для работы с датами. О'кей. Ну, пока нам этого хватит. И соответственно, `date_from` мы передадим `date_from` в параметр `date_from`. `date_to` мы передадим вот этот параметр в `campaign_ids`. Мы, кстати, передадим. У нас здесь должен быть список ID-шников компаний. Вот такой, например. Поэтому можем просто сюда передать. `group_by='DAY'`. Давайте мы сгруппируем по дням. Как это делать? Пишется `date`. О'кей. Что теперь? Давайте `headers` тоже скопирую. И нам осталось только отправить запрос. Давайте это скопирую, вставим. Нам нужно отправить не GET, а POST-запрос. Поэтому `url` с этим. Так как мы отправляем POST-запрос, то мы кодируем как `json`. Смотрим, что получается. Ответ 200. Что у нас в ответе? О, сработало. Видите, он нам вернул не данные, а ID отчёта. Давайте его сохраним. `report_uid = response.json()['uid']`. `uid` расшифровывается, если я не ошибаюсь, как Universal что-то там ID, не User ID. А давайте посмотрим, как расшифровывается `uid`. Universal Unique Identifier. Вот как это расшифровать. Такой формат вы часто сможете увидеть в разных местах. Так, `uid` у нас будет `response.json()['uid']`. Боньк! Теперь у нас есть вот такая вот строка. Что с ней делать? Это вот как раз пример работы асинхронного API. Синхронное API нам отвечает на наш запрос сразу же тем, что мы у него попросили. Например, мы просим у него подготовить нам отчёт, и он нам отвечает сразу: "Вот отчёт". Это удобно тем, что, ну, в общем, мы сразу получаем то, что мы хотим. Но недостаток такого подхода в том, что если мы, например, просим огромный отчёт, и на подготовку его может уйти достаточное количество времени, то, во-первых, нам придётся ждать, а во-вторых, что, наверное, даже более серьёзно, если этого времени будет слишком много, то у нас соединение может сломаться, мы можем просто вылететь по таймауту. Поэтому для решения таких задач используется асинхронные методы. Асинхронный метод работает так: вот мы отправили сейчас запрос, и под капотом Озон принял наш запрос и в свою очередь переадресовал систему, которая готовит отчёты. А точнее, он поставил наш запрос в очередь. То есть, все запросы сначала падают в очередь, и потом из этой очереди их очередь подходит, подбирают специальные воркеры, которые исполняют этот отчёт. И в любой момент времени мы можем проверить вот по этому ID-нику, в каком сейчас состоянии находится наш запрос. Он может либо ожидать в очереди, он может быть в процессе, он может быть сделанным, он там может что-то сломаться и так далее. Поэтому вот когда мы работаем с асинхронным API, мы отправляем запрос, получаем в ответ ID задачи, которую мы поставили в очередь, и прежде чем мы получим данные, нам нужно проверить статус этой задачи. Как это сделать? Давайте вернёмся сюда. Тут где-то должна быть ссылка: "Проверить статус формирования отчёта". Кликаем. Это тоже запрос к API, куда мы должны передать вот наш ID. И в ответ он нам, наверное, вернёт тип: это `started`, не `started`, `in_progress`, `error` или `ready`. Ой, давайте так и сделаем. Скопирую ссылку. Вставим её вот сюда. Сделаем тоже самое. `url =`. Так. И вот смотрите, у нас здесь прямо пример того, как может выглядеть запрос. Давайте прямо скопирую. Вернёмся в код, вставим. Назовём это `payload = {'uid': report_uid}`. И тут даже, видите, он нам подставляет `uid` как параметр в саму ссылку. Поэтому можем, наверное, даже `payload` не использовать. Только превращаем его в строку. И вместо `uid` мы подставим `report_uid`. И запрос у нас будет не POST, а GET. `url = f"https://performance.ozon.ru/api/v2/stats/report/status?uid={report_uid}"`. `headers` у нас будут такие же. А дальше нам нужно... А ещё нам нужно передать `Content-Type`. Нет, это ответ не нужно ничего передавать. Да, нам нужно передать только `query` параметр `uid=ID`. И мы туда передаём что? `report_uid`. Вот так. Пишем `response = requests.get(url, headers=headers)`. И теперь мы отправляем GET-запрос, не POST-запрос. Поэтому `payload` мы запишем в переменную `params`. Смотрим. Так, ответ 200. `response.json()`. Вот видите, у нас получился такой объект. Вот наш ID задачи. И вот у вас `status: 'ready'`. О'кей. Это значит, что наш запрос выполнился, и мы можем его скачать. Кстати, по вот этой ссылке. Давайте посмотрим более внимательно. Тан-тан-тан-ран. Где у нас тут метод "Получить отчёты"? Как скачать отчёт? Давайте вернёмся назад. Вот у нас есть ссылка "Как проверить статус отчёта", и вот есть отдельный метод "Как скачать отчёт". Мы отправляем GET-запрос на вот этот адрес. Пишем ещё раз "statistics/report". И, видимо, нам нужно сюда передать параметрам вот сюда наш ID. Мы можем его, наверное, передать вот так же. Но давайте сделаем более чистенько. `headers` у нас, наверное, будут такие же. А дальше нам нужно... А ещё нам нужно передать `Content-Type`. Нет, это ответ не нужно ничего передавать. Да, нам нужно передать только `query` параметр `uid=ID`. И мы туда передаём что? `report_uid`. Вот так. Пишем `response = requests.get(url, headers=headers)`. И теперь мы отправляем GET-запрос, не POST-запрос. Поэтому `payload` мы запишем в переменную `params`. Смотрим. Так, ответ 200. `response.text`. И вот смотрите, очень интересная штука. Нам в ответ вернулась вот такая вот непонятная штуковина, длинный-длинный, совершенно нечитаемый текст. Дело в том, что он нам тут писал в документации, что при успешном запросе отчёт в формате CSV (Comma Separated Values) можно скачать с помощью относительной ссылки бла-бла-бла. Но а формат отчёта в ответе указан в поле `Content-Type`. И формат зависит от того, сколько компаний в поле `campaign_ids` в исходного запроса: CSV, если только одна компания, ZIP-архив, если в списке несколько компаний. И каждый файл соответствует одной компании из списка. Мы с вами передали несколько компаний в списке, поэтому то, что мы получили, это ZIP-архив. Можем пытаться в этом убедиться, посмотрев `headers`. Что с вами сделать? Вот так? Нет, ну-ка. И `response.json()`? И сделать `response.headers`? Господи, возвращаемся. А может быть, просто вот так надо сделать? Всё, всё проще. `response.json()` не понадобился. И это вот это все заголовки, которые нам вернул API. Тут куча всего, что нам, в принципе, не нужно. Но нас интересует вот это: `Content-Type: application/zip`. Это значит то, что у нас в теле самого ответа, вот это – это ZIP-архив. Что может вызвать некую долю фрустрации, потому что не совсем понятно, что с ним делать. Но на самом деле, пугаться не нужно. Ничего особенно критически сложного здесь нет. Ну, первое, что, по крайней мере, меня всегда выбивало из колеи в каком смысле, это `zip`. Zip – это файл, правильно? Вот то, что у нас сейчас на экране, это вообще не файл. Ну так может, давайте сохраним его в файл? Мы же умеем работать с файлами, правильно? Можем написать что-нибудь типа `with open('report.zip', 'w') as f:`. И дальше мы можем написать `f.write(response.text)`. Только, например, вот так. `f.write(response.text)`. Вот у нас появился файл. Но он сейчас у нас на самом деле сохранён неправильно. Давайте расскажу, в чём дело. Дело в том, что ZIP-файл он по своей природе бинарный. И если вообще, что значит бинарный? Мы знаем, что у нас компьютер оперирует ноликами и единичками, правильно? И по большому счёту, всё, что мы записываем в память или на диск, оно так или иначе хранится в виде ноликов и единичек. Так вот, текст выглядит на самом деле тоже ноликами и единичками. Но когда мы записываем файл в режиме `w` (write), мы записываем его как текстовый файл. И это значит, что помимо ноликов и единичек, туда добавляется таблица соответствия каждого набора ноликов единичек символу. Собственно, вот что мы здесь видим. То есть, там что-то в этот символ превращается, вот что-то превращается в нолик, что-то в заглавную букву Q, и ещё какая-то. Ну, в общем, много-много-много всего. И это не то, чем является ZIP-файл. Его не нужно читать как текст. И эта табличка, которую мы сюда записываем, добавляем, когда мы записываем его как текстовый файл, она всё ломает. Компьютер сходит с ума и не знает, что с этим файликом теперь делать. На самом деле, так как это бинарный файл, нам нужно просто записывать те нолики и единички, которые мы получили. Нам нужно использовать, во-первых, другой режим: `wb` (write binary). А во-вторых, вот если я сейчас выполню, он мне тоже вернёт ошибку, потому что я пытаюсь писать байты (`bytes-like object`), а в соответствие с этим флагом, но при этом я пытаюсь запихать в него строку, потому что текст – это строка. И решить это очень просто. У библиотеки `requests` есть, у вот этого объекта `response` API, у него есть ещё такой вот метод. Это на самом деле параметр, который, параметр, параметр, который возвращает бинарный код ответа. То есть, видите, в данном случае он ничем не отличается, кроме наличия буковки `b` внизу. Но это как раз то, что нам нужно с точки зрения компьютера. Большая разница. Поэтому, когда мы получаем ZIP-архив, если мы хотим его сохранить как ZIP-архив, мы записываем его в режиме `wb` и записываем туда именно бинарный код, а не текст. Сохранили. Вот у нас теперь создался. Мы всё ещё, нам нет смысла пытаться его прочитать в VS Code. VS Code не для этого предназначен. Но теперь этот файл мы можем разархивировать. Как это сделать? А давайте даже, знаете, даже знаете что? Прежде чем мы это сделаем, я хочу сделать такую в такую папку `temp`, куда мы будем класть всякие наши, те файлы. Мы же каждый раз будем выполнять API, будем скачивать этот ZIP-архив, будем его распаковывать, потом доставать вот данные и сохранять в виде, в котором нам нужен. А потом, по-хорошему, нам надо бы навести порядок. Нам нужно удалить архив, потому что уже всё, уже всё распаковали, и всё, что мы оттуда достали, тоже удалить. Поэтому давайте мы вот это удалим. Где-нибудь здесь сделаем вот такое: `temp_folder = 'temp'`. У нас будет `os.makedirs(temp_folder, exist_ok=True)`. И когда мы будем записывать архив, мы сделаем вот так: `zip_filename = os.path.join(temp_folder, 'report.zip')`. Вот эта штука, которую я сюда запихал, она по большому счёту, так, давайте выполним. Она просто объединяет несколько строк в одну в формате адреса в нашей файловой системе. То есть, `temp_folder + 'report.zip'` даёт нам `temp/report.zip`. То есть, вот сюда будет всё записываться. Смотрите. `with open(zip_filename, 'wb') as f: f.write(response.content)`. Выполняю. И теперь файлик появился в этой директории. Так просто будет потом гораздо удобнее всё это дело чистить. Так, но это чуть-чуть на будущее. А пока мы с вами находимся в статусе, что мы скачали отчёт, он у нас в формате ZIP-архива, и нам нужно теперь этот архив разархивировать. Как это сделать? Ну, понятно, что мы можем это сделать руками, но мы же хотим всё автоматизировать. Как нам разархивировать архив в Python? Есть отличная библиотека, уже встроенная в стандартный комплект языка, который сделана специально для того, чтобы работать с ZIP-овскими архивами, называется она `zipfile`. Выполняем. Загрузили её. Не нужно устанавливать. Возвращаемся вниз. А теперь мы можем сделать вот такую магию. Давайте вот это прямо скопирую, потому что прежде чем мы будем что-то там разархивировать, нам нужно, естественно, этот файл открыть. Это пока не нужно. Но открывать мы его будем не с помощью стандартной команды `open`, а с помощью такой штуки: `with zipfile.ZipFile(zip_filename, 'r') as zip_ref:`. Так. И мы открываем наш файл точно по тому же адресу, он лежит. И у нас будет так: `zip_ref.extractall(temp_folder)`. Пробуем выполнить. Да, выполнило. То есть, наш файл открывается. И теперь, чтобы разархивировать файл, мы можем написать `zip_ref.extractall(temp_folder)`. Мы знаем, что там несколько файлов в этом архиве. И куда мы хотим разархивировать эти файлы? Да, в тот же самый `temp_folder`. Ну, ладно, пусть будет тот же самый `temp_folder`. Давайте выполняем. Вуаля! Смотрите, у нас появились CSV-файлики, где в названии есть несколько объектов. И первая, первая часть – это ID компании, вторая – это дата от, третья – дата до. Если мы откроем, мы увидим, что тут есть нужные нам данные. Супер! Но это опять же ещё не всё. У нас куча разных файликов. Нам с ними работать, наверное, не очень удобно. И удобнее было бы их все вместе слепить в один файлик. Как это сделать? Ну, тут уже, на самом деле, довольно всё просто. Мы можем использовать для этого `pandas`. Давайте напишем наверху `import pandas as pd`. И теперь, по большому счёту, всё, что нам нужно сделать, это пройтись в цикле. В общем, достать все CSV-файлы, находящиеся в этом каталоге, в каталоге `temp`, прочитать их в DataFrame, и DataFrame можно сохранить в список, а потом всё вместе схлопнуть вместе. Давайте мы можем сделать вот так: `reports = []`. Список. Дальше `for filename in os.listdir(temp_folder):`. Что мы можем сделать вот так: `if filename.endswith('.csv'):`. `reports.append(filename)`. Вот. `os.listdir(temp_folder)` нам возвращает все файлы в папочке, которую мы указали. Мы видим, что помимо CSV-файлов, тут ещё и наш архив затесался. Поэтому на самом деле мы можем этот список отфильтровать. Например, что-нибудь там типа `if filename.endswith('.csv'):`. Вот. Вуаля! Мы получили наши CSV-файлы. Давайте кстати сохраним их где-нибудь вот здесь: `csv_files = []`. Так. И теперь `for filename in csv_files:`. Всё ещё работает. Теперь мы получаем в переменную `filename` название каждого файла. И мы можем его прочитать, например, `df = pd.read_csv(filename)`. Пробуем. А, ну да, конечно, не `pd.read_csv(filename)`. Забываем, как всё работает. Ждём. Ошибка `FileNotFoundError`. А, ну да, конечно. Я же здесь должен передать относительный адрес этого файла. Относительный, в смысле, блокнота, в котором я работаю. А сейчас это просто название этого файла. То есть, мы потеряли информацию о том, что этот файл лежит в директории `temp`. Давайте это добавим: `filepath = os.path.join(temp_folder, filename)`. Что-то не случилось. Давайте посмотрим, что у нас вот здесь. А что в этом файле не так? Давайте посмотрим внимательно. Так, это 357273517297. Открываем. Ну, похоже, что о'кей, о'кей, я вижу минимум два косяка здесь. И это косяки, конечно, на стороне Озона. Озон нехорошо так делать. Во-первых, что вот это такое? Мы ожидаем, что в в CSV-файлах первая строка – это заголовки, заголовки колонок, а потом идут наши данные. Что мы здесь видим? У нас первая строка – это заголовок всего отчёта: "Компания по продвижению номер за период такой-то, такой-то". Этого здесь быть не должно. Во-вторых, мы видим, что у нас в конце файла написаны итоги всего этого. Здесь тоже быть не должно. Но это, по-хорошему, надо фильтровать. Ну и третье, что я вижу, у нас данные разделены точкой с запятой, точкой с запятой. Так тоже быть не должно. У нас файл называется CSV, CSV расшифровывается как Comma Separated Values, данные, разделённые с запятыми, не точками с запятой. Я, в принципе, понимаю, почему они так сделали, потому что в российских, в России чаще принято отделять данные точками с запятыми друг от друга. У нас другой формат даты, у нас другой формат разделения. Но всё равно, назвали бы это "текст", что ли, или как-нибудь ещё. О'кей. Но ладно, это всё мелочи. Это мы можем сейчас быстро поправить. Как именно это править? То есть, в чём проблема номер один? У нас в первой строчке находится какая-то ерунда. Мм, хотя нет. Давайте так. Первая проблема номер один, конечно, это точки с запятой вместо запятых. Это поправить проще всего. Мы можем сделать вот так: `df = pd.read_csv(filepath, sep=';')`. Отдельно вот так. Мы получаем ту же самую ошибку `Parsing error`. И здесь отдельным параметром мы можем `sep=';'`. Мы тут указываем символ, который разделяет данные. Всё, в принципе, всё прочитала. Но теперь у нас есть вторая проблема: у нас есть непонятная первая строка, которую мы можем, если я не ошибаюсь, пропустить. `skiprows=1`. Вуаля! Мы пропустили первую строку. Но в конце у нас всё ещё вот этот вот "Всего". Нам тоже не нужен. И если я не ошибаюсь, его можно пропустить через `skipfooter=1`. Вот. Ладно, вот его мы тоже пропустили. Гораздо лучше. Теперь у нас чистенькая табличка загружается. И мы можем её записать вот сюда. И можем даже, знаете что? Нам не нужно сохранять отдельные DF. Нам нужно просто `reports.append(df)`. Вот эти штуки. Так, чтобы аккуратненько было. Хоп! Выполняем. Ага, он нам какие-то ворнинги даёт, но надеюсь, нас считалось правильно. Давайте посмотрим, что у нас в `reports`. В `reports` у нас сейчас список DataFrame. И мы можем его объединить в один DataFrame через `pd.concat(reports)`. Смотрим, что у нас получилось. Ну и да, вот у нас получилась табличка со всеми нашими данными. С ней ещё можно поработать, например, привести данные к правильному формату, может быть, что-то почистить и читать какие-нибудь метрики. А может быть, её можно просто сохранить: `final_df.to_csv('final_report.csv', index=False)`. И, пожалуйста, вот они все наши данные. Ну и на этом у меня, наверное, всё. И напоследок скажу, что если вас интересует тема автоматизации работы с маркетплейсами с помощью программирования, вот посмотрите следующее видео, где я показываю, как решать схожую задачу – получение аналитики из рекламной платформы для магазинов на Wildberries. Ну а пока у меня всё. Спасибо. Пока-пока!