MAATRIX / Блог / Выгрузка из SaaS пришла в формате, который никто не читает

Выгрузка из SaaS пришла в формате, который никто не читает

MAATRIX

Вы нажимаете «Экспортировать данные» в очередном облачном сервисе, ждёте письмо со ссылкой на архив — и через час скачиваете его с чувством выполненного долга. Открываете архив, а внутри набор файлов, которые не читаются ни в одном привычном инструменте: собственный бинарный формат, вложенный JSON без документированной схемы или XML на пять уровней вложенности с внутренними идентификаторами вместо понятных полей. Формально сервис своё обязательство выполнил — кнопка «экспорт» есть и она сработала. Практически для переноса данных куда-либо ещё эта выгрузка почти бесполезна, и разбираться с этим приходится вам, причём обычно в тот момент, когда на эксперименты уже нет времени.

Почему «экспорт есть» не значит «экспорт полезен»

У большинства SaaS-сервисов функция экспорта появляется не из заботы об удобстве клиента, а из требования закона или площадки: право на портируемость данных, условия платёжной системы, требования регулятора. Формулировка обычно звучит как «пользователь должен иметь возможность получить свои данные» — и ничего не говорит про то, в каком виде. Сервис выполняет это требование самым дешёвым для себя способом: выгружает данные так, как они лежат внутри, а не так, как их удобно читать снаружи.

Это рациональное поведение с точки зрения вендора. Разработка нормального, документированного, стабильного формата экспорта — отдельная фича, которую нужно проектировать и поддерживать при каждом изменении внутренней схемы. У продукта, который живёт с продаж подписки, а не с ухода клиентов, стимул вкладываться в качество экспорта близок к нулю. Он нужен ровно для того, чтобы формально закрыть галочку «данные можно забрать», а не для того, чтобы конкурент мог легко переманить клиента.

Отсюда и типичная картина: выгрузка есть, но она отражает внутреннюю структуру базы данных сервиса на момент экспорта — со всеми служебными полями, версионными артефактами и оптимизациями под собственный движок. Такой дамп прекрасно подходит для одной задачи: восстановить те же данные обратно в тот же сервис. Для любой другой цели — миграции в другую систему, аудита, простого «почитать, что у нас есть» — он требует дополнительной работы, объём которой заранее никто не оценивал.

Что обычно скрывается внутри «родного» экспорта

На практике проблемные места повторяются от сервиса к сервису:

  • Внутренние идентификаторы вместо значений. Вместо status: "оплачен" в файле лежит status_id: 4, а таблица соответствий кодов и значений в экспорт не входит — она есть только внутри самого сервиса.
  • Денормализованный дамп внутренней БД. Экспорт мог собираться прямым SELECT * из рабочих таблиц с техническими полями (_internal_flag, shard_key, sync_version), которые не несут смысла вне системы, но занимают половину файла.
  • Собственный сериализованный формат. Иногда это не JSON и не CSV, а внутренний бинарный формат или сериализация конкретного языка/фреймворка — открыть его можно только той же библиотекой, которой он был создан.
  • JSON внутри текстового поля. Отдельное поле в CSV или таблице содержит экранированную строку, которая сама по себе — вложенный JSON или сериализованный объект. Стандартный импорт в другую систему просто кладёт эту строку как текст, не разворачивая её.
  • Отсутствие документации по схеме. Файлы называются осмысленно, но что означает конкретное поле, в каких единицах хранится число, что означает null против пустой строки — нигде не написано, приходится восстанавливать по образцам.
  • Файлы и вложения — отдельная головная боль. Экспорт метаданных часто не включает сами файлы (документы, изображения, записи звонков) — только ссылки на внутреннее хранилище сервиса, которые перестают работать после закрытия аккаунта.
  • Локаль и кодировка. Даты в национальном формате день/месяц вместо ISO, числа с запятой как разделителем дробной части, кодировка не UTF-8 — мелочи, которые не видны на глаз, но ломают импорт при автоматической обработке. Про то, как формат обмена вроде CSV молча теряет данные на подобных мелочах, отдельно разобрано в статье про CSV как формат обмена.

Ни один из этих пунктов не является злым умыслом вендора — это побочный эффект того, что экспорт написан «для галочки» человеком, который в первую очередь думал о внутренней структуре, а не о читателе файла.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Проверяйте экспорт заранее, а не в день переезда

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

Правильный порядок обратный: тестировать экспорт нужно заранее, пока у вас есть время разбираться без давления сроков, и желательно не один раз, а регулярно — потому что формат экспорта может незаметно поменяться при очередном обновлении сервиса.

Практический чек-лист для такой проверки:

  1. Сделайте тестовый экспорт на реальном объёме данных, а не на демо-аккаунте с тремя записями. Проблемы вроде обрыва экспорта на больших таблицах или лимита по количеству строк в файле проявляются только на масштабе, близком к боевому.
  2. Откройте всё, что скачалось, а не только первый файл из архива. Часто экспорт состоит из десятка файлов, часть из которых — служебные логи или пустые заглушки для функций, которыми вы не пользуетесь.
  3. Посчитайте записи в экспорте и сверьте с количеством в самом сервисе (в интерфейсе обычно есть счётчик — «1 284 контакта», «342 сделки»). Расхождение — сигнал, что часть данных экспорт не берёт вовсе.
  4. Проверьте вложения отдельно. Если в системе есть файлы, документы, изображения — убедитесь, что они реально скачиваются, а не остаются ссылками на внутреннее хранилище сервиса.
  5. Попробуйте реальный импорт — хотя бы в собственную тестовую базу, а не просто открыть файл текстовым редактором. Только полный цикл экспорт → импорт показывает, что данные действительно пригодны к использованию, а не просто выглядят похожими на структурированные.
  6. Повторяйте проверку раз в квартал или после каждого заметного обновления интерфейса сервиса. Формат может измениться без объявления в чейнджлоге — новое поле, переименованный столбец, другая кодировка.

Это тот же принцип, что и с бэкапами: непроверенный экспорт нельзя считать рабочим, точно так же как нельзя считать рабочим бэкап, который ни разу не разворачивали.

Пишем свой конвертер: скрипт, который экономит недели в момент переезда

Если разбор экспорта показал, что формат неудобный, но структурно понятный, разумный шаг — написать небольшой скрипт-конвертер, который переводит выгрузку в нормальный, человекочитаемый вид: плоские CSV с понятными заголовками или таблицы в SQLite/PostgreSQL с расшифрованными значениями вместо внутренних кодов.

Пример на Python — конвертация условного JSON-экспорта контактов с внутренними кодами статуса в нормализованную SQLite-базу:

import json
import sqlite3

with open("export.json", encoding="utf-8") as f:
    data = json.load(f)

conn = sqlite3.connect("import.db")
cur = conn.cursor()
cur.execute("""
    CREATE TABLE IF NOT EXISTS contacts (
        id INTEGER PRIMARY KEY,
        name TEXT,
        email TEXT,
        status TEXT,
        created_at TEXT
    )
""")

# Соответствие внутренних кодов статуса читаемым значениям
# восстановлено вручную по документации или методом проб
status_map = {1: "active", 2: "paused", 3: "closed", 4: "archived"}

for item in data.get("contacts", []):
    cur.execute(
        "INSERT OR REPLACE INTO contacts (id, name, email, status, created_at) "
        "VALUES (?, ?, ?, ?, ?)",
        (
            item["id"],
            item.get("full_name", "").strip(),
            item.get("email", "").lower(),
            status_map.get(item.get("status_id"), "unknown"),
            item.get("created_at"),
        ),
    )

conn.commit()
print(f"Импортировано {cur.execute('SELECT COUNT(*) FROM contacts').fetchone()[0]} записей")

Ключевая идея не в самом коде — он будет разным для каждого сервиса, — а в подходе: вы один раз инвестируете время в то, чтобы понять структуру конкретного экспорта, и превращаете это понимание в работающий скрипт, а не держите в голове или в заметках «на будущее». Несколько практических моментов:

  • Держите конвертер в системе контроля версий, как обычный код проекта, а не как одноразовый скрипт в папке «Загрузки». Формат экспорта может измениться — вы обновите скрипт и увидите разницу в diff.
  • Гоняйте конвертер на каждом тестовом экспорте из чек-листа выше — это заодно и способ автоматически заметить, что вендор поменял схему. Формат может смениться так же незаметно, как меняется формат данных в стороннем API — и обнаруживается это обычно уже по упавшей интеграции, а не по объявлению в чейнджлоге.
  • Не пытайтесь сохранить 1:1 всё, что есть во внутреннем формате. Часть служебных полей вендора вам не нужна вообще — конвертер должен приводить данные к вашей целевой структуре, а не быть зеркалом чужой базы.
  • Логируйте, что не удалось сконвертировать. Записи с неожиданными значениями (например, статус, которого нет в вашей карте соответствий) не должны молча пропадать — пусть скрипт падает или явно пишет их в отдельный файл для ручной проверки.

API вместо «стандартного» экспорта: когда это работает лучше

Кнопка «Экспорт» в интерфейсе — не единственный способ забрать данные. У многих сервисов есть API, и часто через него данные достаются в более пригодном для использования виде, чем через штатный экспорт, — просто потому, что API проектируют для программного потребления, а массовый экспорт — как разовую функцию для конечного пользователя.

Сравнение подходов на практике:

КритерийШтатный экспортAPI
ФорматЧасто проприетарный, без схемыОбычно JSON/REST, задокументирован
Полнота связей между сущностямиМожет терять связи между таблицамиОтдельные запросы на связанные объекты
Стабильность форматаМеняется без предупрежденияВерсионируется (/v1/, /v2/), ломающие изменения анонсируются
Скорость получения больших объёмовОдин архив, но может обрываться на масштабеПостраничная выгрузка, повторяемая при сбое
Порог входаОдин клик в интерфейсеНужен токен, обработка пагинации и лимитов
Вложения/файлыЧасто только ссылкиОтдельные эндпоинты на скачивание бинарных объектов

Пример постраничного вытягивания данных через API вместо ожидания архива:

#!/usr/bin/env bash
TOKEN="ваш_api_token"
PAGE=1

while :; do
  RESPONSE=$(curl -s -H "Authorization: Bearer $TOKEN" \
    "https://api.example-saas.com/v2/contacts?page=$PAGE&per_page=100")

  COUNT=$(echo "$RESPONSE" | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('items', [])))")
  [ "$COUNT" -eq 0 ] && break

  echo "$RESPONSE" >> "contacts_raw.jsonl"
  PAGE=$((PAGE + 1))
  sleep 1  # уважаем rate limit
done

API — не универсальное решение: у него свои лимиты по количеству запросов в минуту, не все данные могут быть доступны программно (иногда часть полей доступна только через интерфейс), а доступ к нему может требовать более дорогого тарифного плана. Но если у сервиса есть и штатный экспорт, и API — стоит протестировать оба способа заранее и выбрать тот, что реально даёт более полную и структурированную картину, а не полагаться на экспорт по умолчанию просто потому, что он ближе лежит.

Где хранить промежуточный результат и как встроить это в регламент

Тестовые экспорты, конвертер и результат его работы — это данные и код, которым нужно постоянное, а не разовое место хранения. Держать это только внутри аккаунта того же SaaS-сервиса бессмысленно: если доступ к сервису закроется, вместе с ним пропадёт и площадка для тестов. Разумно вынести весь цикл — скачивание, конвертацию, промежуточное хранилище — на свой сервер: там можно поставить cron-задачу, которая регулярно тянет экспорт или данные через API, прогоняет через конвертер и складывает результат в собственную базу или файловое хранилище, независимо от того, что происходит с исходным сервисом.

Это особенно важно, если вы уже сталкивались с ситуацией, когда доступ к SaaS обрывается не по вашей инициативе — истёк грант, изменились условия оплаты из-за санкционных ограничений, сервис приостановил работу для региона. Что в такой ситуации переносить в первую очередь и в каком порядке, разобрано отдельно в статье про подписки на SaaS, которые слетели — общий принцип тот же: чем раньше у вас есть работающий, проверенный процесс экспорта и конвертации, тем меньше пожара в момент, когда сервис перестаёт быть доступен.

Стоит заранее прикинуть и объём: если данных накопилось на терабайты, а не на мегабайты, у части облачных провайдеров сама выгрузка наружу может оказаться платной статьёй расхода — это отдельно разобрано в материале про стоимость выгрузки данных из облака. А если речь идёт о полном закрытии проекта, а не просто о смене инструмента, имеет смысл сразу продумать регламент архивации и экспорта — как оформить и что сохранить, чтобы данные оставались доступны и через год, описано в статье про закрытие проекта, архив и экспорт.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Что делать, если у сервиса вообще нет API, только кнопка «Экспорт»?

Разобрать штатный экспорт максимально подробно и написать конвертер под него. Проверьте также, нет ли у сервиса недокументированного внутреннего API, который использует сам веб-интерфейс — иногда через инструменты разработчика в браузере видно, какие запросы уходят на бэкенд. Это менее надёжно официального API (могут поменять без предупреждения), но лучше, чем ничего.

Стоит ли писать конвертер заранее, если миграция прямо сейчас не планируется?

Да, если данные в этом сервисе для вас критичны. Написание конвертера — это одновременно способ проверить, что данные вообще можно забрать в пригодном виде, и страховка на случай внезапного отказа от сервиса не по вашей инициативе. Полноценный конвертер под каждый сервис не нужен, но минимальный скрипт «прочитать и посчитать записи» стоит иметь для всех систем с действительно важными данными.

Как понять, что экспорт содержит вложения, а не только метаданные о них?

Откройте несколько записей и найдите поля, похожие на пути к файлам или URL. Если это ссылки на внутренний домен сервиса — попробуйте скачать их отдельно и проверьте, что они не требуют активной сессии. Если ссылка перестаёт работать после выхода из аккаунта — вложения фактически не экспортированы, а только упомянуты.

Формат экспорта менялся без предупреждения — как защититься от этого в будущем?

Регулярный тестовый прогон конвертера — единственная реальная защита. Изменение схемы почти всегда проявляется как ошибка при конвертации: новое поле, пропавшее поле, другой тип значения. Если гонять конвертер раз в квартал, вы заметите изменение через пару месяцев после релиза, а не в день, когда данные реально понадобились.

Можно ли доверять сторонним сервисам-конвертерам, которые обещают распарсить экспорт из популярных SaaS?

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

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →