# Как выгрузить подписчиков Instagram в CSV на Python

> Выгрузка подписчиков публичного аккаунта Instagram в CSV на Python: готовый скрипт, пагинация по курсору, продолжение после обрыва, ошибки 429 и 503.

- URL: https://storiesfly.com/ru/blog/export-instagram-followers-to-csv-python
- Language: Русский (ru)
- English version: https://storiesfly.com/blog/export-instagram-followers-to-csv-python
- Published: 2026-10-21
- Topic: Tracking
- Publisher: StoriesFly (https://storiesfly.com)

## Как выгрузить подписчиков Instagram в CSV на Python?

Вызывайте эндпоинт `/v1/user/followers` в цикле, передавайте `next_cursor` из каждой страницы как `cursor` в следующем запросе и записывайте пользователей в файл через `csv.DictWriter`. Скрипт ниже делает именно это, нужна одна библиотека (`requests`). Он сохраняет место остановки после каждой страницы, поэтому его можно продолжить, а на ответы 429 и 503 ждёт и повторяет запрос. Работает только с публичными аккаунтами.

StoriesFly — это наш API, поэтому мы используем его здесь. Аккаунт Instagram, логин и куки не нужны, нужен только API-ключ.

## Что понадобится

- Python 3.10 или новее (в скрипте есть `str | None`).
- Библиотека `requests`: `pip install requests`.
- API-ключ. Войдите через Telegram на [/developers/dashboard](/developers/dashboard) и создайте ключ, он выглядит как `sf_live_...`. Новый аккаунт получает 100 бесплатных запросов (один раз на Telegram-аккаунт), этого хватит, чтобы проверить всё из статьи.

Ключ лучше положить в переменную окружения, а не в код:

```bash
export STORIESFLY_KEY=sf_live_your_key_here
```

В Windows PowerShell: `$env:STORIESFLY_KEY = "sf_live_your_key_here"`.

Описание эндпоинта: [/developers/docs/user-followers](/developers/docs/user-followers) (документация на английском).

## Полный скрипт

Сохраните как `export_followers.py`:

```python
"""Export the followers of an Instagram account to a CSV file.

Usage:  STORIESFLY_KEY=sf_live_... python export_followers.py nasa --max-pages 5

Each page is one request. The script saves the cursor after every page, so if
it stops (network, empty balance) the next run continues where it left off.
"""
import argparse
import csv
import json
import os
import time
from pathlib import Path

import requests

API = "https://storiesfly.com/api/v1"
KEY = os.environ["STORIESFLY_KEY"]
FIELDS = ["id", "username", "full_name", "verified", "is_private", "profile_pic_url"]


def fetch_page(username: str, cursor: str | None) -> dict:
    params = {"username": username}
    if cursor:
        params["cursor"] = cursor
    while True:
        r = requests.get(
            f"{API}/user/followers",
            params=params,
            headers={"X-Api-Key": KEY},
            timeout=60,
        )
        if r.status_code == 429:  # too fast: wait as told, costs nothing
            time.sleep(int(r.headers.get("Retry-After", "1")))
            continue
        if r.status_code == 503:  # failed on their side, refunded: try again
            time.sleep(5)
            continue
        if r.status_code != 200:
            err = r.json()
            raise SystemExit(f"{r.status_code} {err['code']}: {err['error']}")
        return r.json()


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("username")
    ap.add_argument("--max-pages", type=int, default=0, help="0 = no limit")
    args = ap.parse_args()
    username = args.username.lstrip("@")

    out = Path(f"{username}_followers.csv")
    state = Path(f"{username}_followers.state.json")

    cursor = None
    if state.exists():
        cursor = json.loads(state.read_text())["cursor"]
        print("Resuming from the saved cursor")

    new_file = not out.exists()
    pages = rows = 0
    with open(out, "a", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS, extrasaction="ignore")
        if new_file:
            writer.writeheader()
        while True:
            page = fetch_page(username, cursor)
            writer.writerows(page["users"])
            f.flush()
            pages += 1
            rows += len(page["users"])
            print(f"page {pages}: +{len(page['users'])} (total {rows})")

            if not page["has_more"]:
                state.unlink(missing_ok=True)
                print("Done: the list has ended")
                break
            cursor = page["next_cursor"]
            state.write_text(json.dumps({"cursor": cursor}))
            if args.max_pages and pages >= args.max_pages:
                print("Stopped at --max-pages; run again to continue")
                break

    print(f"{rows} rows written to {out}, {pages} requests spent")


if __name__ == "__main__":
    main()
```

## Запуск: сначала несколько страниц

У крупных аккаунтов очень много подписчиков, а каждая страница стоит один запрос. Для первого запуска ограничьте число страниц через `--max-pages`:

```bash
python export_followers.py nasa --max-pages 3
```

Вывод выглядит так (значения примерные):

```text
page 1: +50 (total 50)
page 2: +50 (total 100)
page 3: +50 (total 150)
Stopped at --max-pages; run again to continue
150 rows written to nasa_followers.csv, 3 requests spent
```

Откройте `nasa_followers.csv` и проверьте колонки. Если всё в порядке, запустите ту же команду без `--max-pages`. Скрипт помнит курсор, поэтому полный запуск продолжится после уже скачанных страниц.

### Сколько это стоит

Одна страница это один запрос. Размер страницы зависит от того, что отдаёт Instagram, и меняется. Чтобы оценить расход, выгрузите 3-5 страниц, посчитайте среднее число строк на страницу и поделите на него число подписчиков аккаунта. Получится примерное число запросов на полную выгрузку. Умножьте его на цену запроса со страницы [/developers/pricing](/developers/pricing).

## Колонки CSV

| Колонка | Что это |
|---|---|
| `id` | Числовой ID пользователя в Instagram. Не меняется при смене имени |
| `username` | Текущий username |
| `full_name` | Отображаемое имя |
| `verified` | Есть ли галочка верификации |
| `is_private` | Закрыт ли аккаунт самого подписчика |
| `profile_pic_url` | Ссылка на аватар |

Ссылки в `profile_pic_url` ведут на CDN Instagram и со временем перестают работать. Если нужны сами картинки, скачивайте их сразу. Для сравнения списков во времени берите `id`, а не `username`.

## Как работает постраничная загрузка

Эндпоинт возвращает страницу пользователей и два поля: `has_more` и `next_cursor`. Чтобы получить следующую страницу, отправьте тот же запрос с `cursor=<next_cursor>`. Цикл в `main()` делает именно это и останавливается, когда `has_more` равно false.

После каждой страницы скрипт записывает новый курсор в файл `nasa_followers.state.json`. Если запуск прервался (ноутбук заснул, пропала сеть, закончился баланс), запустите ту же команду ещё раз: курсор прочитается из файла и загрузка продолжится. CSV открывается в режиме добавления, заголовок пишется один раз, дубликатов нет. Когда список закончился, файл состояния удаляется.

Чтобы сделать новую выгрузку того же аккаунта, удалите `nasa_followers.csv` и `nasa_followers.state.json` или переименуйте старый CSV (см. раздел про сравнение ниже).

## Лимиты и ошибки

Два случая скрипт обрабатывает сам:

- **429 `rate_limited`.** Вы шлёте запросы слишком часто. Лимит: 1 запрос в секунду до первого пополнения и 3 в секунду на уровне Start. Скрипт ждёт столько секунд, сколько указано в заголовке `Retry-After`, и повторяет вызов. За 429 деньги не списываются.
- **503 `upstream_unavailable`.** Сбой на нашей стороне. Запрос возвращается на баланс, скрипт ждёт 5 секунд и пробует снова.

Остальные ответы останавливают скрипт с кодом статуса, кодом ошибки и сообщением. Ошибки приходят в JSON вида `{"error": "...", "code": "..."}`. Какие бывают:

| Статус | Код | Списывается? |
|---|---|---|
| 400 | `bad_request` | Нет |
| 401 | `unauthorized` | Нет |
| 402 | `credits_exhausted` | Нет |
| 403 | `private_account` | Да |
| 404 | `not_found` | Да |
| 500 | `internal_error` | Возвращается |

В каждом ответе есть заголовки, например `X-Units-Charged` и `X-Balance-Remaining`. Их можно выводить, чтобы следить за балансом при долгой выгрузке.

## Закрытые аккаунты

Подписчиков закрытого аккаунта выгрузить нельзя. API отвечает 403 `private_account` и данных не отдаёт. Так задумано, это не ошибка скрипта.

## Список подписок (following)

Чтобы выгрузить, на кого подписан аккаунт, используйте тот же скрипт с другим эндпоинтом. В `fetch_page` замените строку:

```python
f"{API}/user/followers",
```

на:

```python
f"{API}/user/following",
```

Заодно поменяйте имена файлов, чтобы две выгрузки не смешались: замените `_followers` на `_following` в двух строках `Path(...)` в `main()`. Колонки те же. Описание: [/developers/docs/user-following](/developers/docs/user-following).

## Как открыть файл в Excel или Google Таблицах

Файл в кодировке UTF-8.

- **Google Таблицы:** Файл, Импорт, Загрузка, выберите файл. Кодировка определяется правильно.
- **Excel:** используйте Данные, Из текстового/CSV-файла и проверьте, что кодировка файла UTF-8. Если открыть файл двойным щелчком, Excel может неправильно показать имена с кириллицей и другими не латинскими буквами. Через диалог импорта этого не происходит.

Длинные числовые ID в Excel могут превратиться в запись с экспонентой. При импорте задайте для колонки `id` тип «Текст», чтобы сохранить все цифры.

## Как сравнить две выгрузки и узнать, кто отписался

Сделайте выгрузку сейчас, переименуйте файл и повторите через неделю. Например, переименуйте первый файл в `nasa_followers_monday.csv` и запустите скрипт снова (он создаст новый `nasa_followers.csv`). Затем сравните колонки `id` через множества:

```python
import csv

def ids(path):
    with open(path, newline="", encoding="utf-8") as f:
        return {row["id"] for row in csv.DictReader(f)}

old, new = ids("nasa_followers_monday.csv"), ids("nasa_followers.csv")
print("Unfollowed:", len(old - new))
print("New followers:", len(new - old))
```

`old - new` это ID, которые были в первом файле и пропали во втором, `new - old` наоборот. Чтобы получить имена, найдите эти ID в CSV. Учтите, что удалённый или заблокированный аккаунт тоже пропадает из списка.

## Вариант без кода

Если программировать не нужно, у StoriesFly есть выгрузка подписчиков для обычных пользователей: [/instagram-followers-export](/instagram-followers-export). API из статьи нужен, когда вы хотите запускать выгрузку сами, по расписанию или встроить её в свой инструмент.

## Важно

Речь о публичных данных. Соблюдайте правила Instagram и законы вашей страны и не используйте выгруженные списки для спама или травли.

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

### Можно ли выгрузить подписчиков закрытого аккаунта Instagram?

Нет. Для закрытого аккаунта API возвращает 403 `private_account` и список не отдаёт. Выгружать можно только публичные аккаунты.

### Сколько запросов уходит на полную выгрузку подписчиков?

Один запрос на страницу. Размер страницы меняется, поэтому запустите 3-5 страниц с `--max-pages`, поделите строки на страницы, затем число подписчиков на это значение, и получите оценку.

### Что будет, если скрипт остановится посередине?

Запустите ту же команду снова. Курсор сохраняется в файл состояния после каждой страницы, скрипт продолжит с этого места и допишет тот же CSV.

### Нужен ли аккаунт Instagram или куки?

Нет. Скрипт отправляет только ваш ключ StoriesFly в заголовке `X-Api-Key`. Входа в Instagram в коде нет.

### Списываются ли деньги за неудачные запросы?

Ответы 429 (лимит скорости), 400 (плохой запрос), 401 (неверный ключ) и 402 (пустой баланс) бесплатны. За 503 и 500 деньги возвращаются. Ответы с данными, `not_found` и `private_account` оплачиваются.

### Можно ли выгрузить и список подписок?

Да. Замените `/user/followers` на `/user/following` в скрипте и поменяйте имена файлов. Колонки те же.
