Пакетный DR для списка клиентов как 40-строчная Python-задача
У вас есть таблица с доменами и дашборд, которому нужно одно число в каждой строке. Инструмент проверки доменов по ссылкам для этой задачи — это в основном «инженерная» часть, и этот скрипт и есть эта инженерия.

Скрипт
Он читает domains.txt, отправляет по 200 доменов в запросе, сохраняет каждый сырой ответ в SQLite и выводит стоимость запуска. Мы сохраняем весь полезный payload, потому что имена полей лучше копировать из реального ответа, а не из статьи в блоге.
import os, sqlite3, time, json, requests
BASE = "https://seomasters.dev/api.php"
KEY = os.environ["SEO_API_KEY"]
BATCH = 200
db = sqlite3.connect("dr.db")
db.execute("create table if not exists raw (ts int, batch text, body text)")
domains = [d.strip() for d in open("domains.txt") if d.strip()]
spent = 0
for i in range(0, len(domains), BATCH):
chunk = domains[i:i + BATCH]
for attempt in range(4):
try:
r = requests.get(BASE, timeout=30,
params={"action": "bulk_dr", "domains": ",".join(chunk)},
headers={"Authorization": "Bearer " + KEY})
break
except requests.RequestException:
time.sleep(2 ** attempt)
else:
raise SystemExit(f"network gave up at batch {i}")
if r.status_code in (401, 402):
raise SystemExit(f"HTTP {r.status_code}: fix the key or top up, do not retry")
body = r.json()
if not body.get("ok"):
raise SystemExit(f"ok=false at batch {i}: {json.dumps(body)[:300]}")
db.execute("insert into raw values (?,?,?)", (int(time.time()), str(i), r.text))
db.commit()
print(body.get("_credits"))Что делает каждая часть
action=bulk_dr
Один кредит за вызов. Документированный предел — 2000 доменов за вызов.
domains=
Параметр GET. Эндпоинт также принимает тело POST, что является лучшим выбором, когда списки становятся длинными. Перед полным запуском проверьте разделитель с двумя доменами.
BATCH = 200
Наша привычка, а не ограничение: 200 коротких доменов держат URL далеко за пределами общих лимитов прокси.
_credits
Присутствует в аутентифицированных ответах: стоимость вызова и остаток. Вывод этого значения после каждого батча — самый дешевый аудиторский след.
Столбец сырого body
Вы парсите его позже, один раз, опираясь на сохраненный образец. Изменение схемы на нашей стороне тогда обойдется вам в запрос, а не в повторный запуск.

Гипотетический запуск с арифметикой
Это пример, а не история клиента. Представьте агентство, которое отслеживает 1800 доменов клиентов и потенциальных клиентов в Google Таблицах и хочет обновлять DR каждый понедельник.
При 200 доменах в батче это 9 запросов. Девять запросов по 1 кредиту — это 9 кредитов, или $0.0009 за запуск, а за 52 понедельника общая сумма составит 468 кредитов, что округляется до менее чем пяти центов.
Цена — не ограничение. Свежесть — да. Индекс перестраивается примерно раз в месяц, поэтому еженедельные запуски будут показывать одни и те же значения три недели из четырех, а ежемесячный запуск фиксирует те же изменения примерно за четверть звонков.
Так что запускайте его ежемесячно, а еженедельную таблицу оставьте для ручных правок. Подробности о том, почему оценка может стоять на месте, см. в справочнике DR.
Что может пойти не так в этом скрипте
| Симптом | Причина | Решение |
|---|---|---|
SystemExit: HTTP 402 | Баланс равен нулю | Пополните счет на странице аккаунта, перезапустите; завершенные батчи уже в SQLite |
SystemExit: HTTP 401 | Переменная среды пуста или опечатка в ключе | Выведите len(KEY) и проверьте префикс sk_ |
ok=false на батче 3 | Некорректное значение в списке, например полный URL | Удалите схему и путь перед отправкой, см. таблицу ошибок |
| Тот же DR, что и на прошлой неделе | Индекс еще не перестроился | Ожидаемо; сравнивайте ежемесячно |
| Длинный URL отклонен | Батч слишком велик для GET | Уменьшите BATCH или перейдите на тело POST |
Размер батча, GET и POST для длинных списков
Предел — 2000 доменов за вызов, а скрипт отправляет десятую часть от этого. Каждый вызов стоит одинаковый фиксированный кредит, поэтому размер батча — это вопрос отказов и полезной нагрузки, а не цены.
Почему 200, а не 2000
Меньший батч дешево падает. Если запуск останавливается посередине, хвост, который нужно перепроверить, равен длине одного батча, а не всего списка.
Короткие списки также делают каждый сохраненный body достаточно маленьким, чтобы его можно было просмотреть глазами, когда какое-то число кажется неверным, и это важнее, чем несколько сэкономленных вызовов.
Если позже вы увеличите размер батча, сделайте это один раз, в одной константе, и перезапустите тест с двумя доменами. Большее число — не повод пропускать тест.
Планируйте батч исходя из допустимой длины запуска, а не из предела. Ночная задача, которая должна завершиться до утренского отчета, может позволить себе меньшие, более безопасные батчи, чем одноразовое заполнение.
GET для коротких списков, POST для длинных
Форма GET помещает каждый домен в URL. Это работает, пока прокси или строка лога не обрежут запрос, поэтому в таблице выше рекомендуется сначала уменьшить батч. Когда списки вырастут, отправляйте то же значение domains в теле POST.
Проверьте разделитель и кодировку с двумя доменами перед полным запуском. Лишняя запятая проявится в первом батче, и дешевле увидеть ее там.
Тело POST также держит список вне журналов доступа, которые многие хосты сохраняют по умолчанию. Это небольшая приватная выгода для списков клиентов, и она стоит одной дополнительной строки заголовка в скрипте.
При переходе на POST оставьте версию GET в качестве резервного варианта для коротких списков. Два пути кода звучат громоздко, но одна маленькая функция, выбирающая один по длине, легко читается и тестируется.
Обрезка и удаление дубликатов перед первым запросом
Удалите пробелы, приведите хост к нижнему регистру и удалите дубликаты, прежде чем что-либо покинет вашу машину. Дубликаты занимают место под пределом в 2000 доменов, и они заставляют количество в вашей таблице расходиться с количеством в ответе.
Выберите одно правило для www. и применяйте его в одной функции, чтобы один и тот же сайт никогда не искался под двумя написаниями. Любой инструмент проверки доменов по ссылкам, который масштабируется более чем на несколько сотен имен, нуждается в такой дисциплине.
Чем должен быть лог запуска
Выводите значение _credits после каждого батча и записывайте номер батча, количество доменов и время рядом с ним. Эти три числа отвечают почти на любой вопрос, который финансовый читатель задаст о запуске.
Запуск, который ничего не выводит до конца, не дает вам понятия, когда он завис. Строка на батч стоит дешево и превращает тихий завис в номер батча, который можно назвать.
Храните лог в той же папке, что и файл SQLite, чтобы одна резервная копия копировала и то, и другое. Лог, живущий в другом месте, — это тот, который потеряется во время инцидента.

Три расширения, стоящие послеобеденного времени
Сохраняйте номер батча рядом с меткой времени. Он уже есть в таблице выше, и он позволяет перезапустить только тот батч, который упал с ошибкой 402 в середине запуска.
Напишите небольшую view над сырой таблицей, как только вы изучите реальный ответ: домен, оценка, время получения. Дашборды читают view, а сырой столбец остается вашим доказательством, если какое-то число когда-либо поставят под сомнение.
Запускайте его из GitHub Actions или обычного cron на маленьком VPS. Оба варианта хороши. Важно, чтобы задача завершалась с ненулевым кодом при любой ошибке, потому что тихая ошибка выглядит как устаревший график через три недели.
Сортировка вывода — задача фронтенда. Ранжируйте портфель по оценке, группируйте его или сравнивайте с dr_distribution (1 кредит), чтобы увидеть, где находится каждый клиент среди всех 181 132 097 доменов.
Дрейф полей и сырая таблица
Долгоживущая задача тихо падает, когда форма поля меняется. Сылой столбец нужен, чтобы ошибка оставалась заметной и дешевой для исправления.
Сначала сохраните сырой body
Сылой столбец — это страховка. Если поле переименовано или появилось новое, вы перезапускаете запрос по сохраненным ответам, а не платите за весь список заново. Парсинг позже дешев, а повторная покупка — нет.
Также храните один сохраненный образец в неделю в отдельном файле. Когда парсинг ломается, вы сравниваете образец этой недели с последним рабочим.
Записывайте body точно так, как он получен, без форматирования и без обрезки. Любое изменение, которое вы вносите в текст перед сохранением, — это изменение, которое нельзя будет отменить позже.
Как выглядит дрейф на практике
Допустим, дашборд внезапно показывает пустые оценки для одного батча. Сылая строка все еще там. Вы открываете ее, сравниваете ее ключи с образцом прошлой недели и находите разницу за несколько минут.
Исправление тогда — одна строка во view, а не перезапуск всего списка. В этом и есть вся цель хранения body.
Держите краткую заметку рядом с view, указывающую, какое поле она читает и когда вы последний раз проверяли ее по реальному ответу. На заметку уходит минута, а экономия времени потом огромна.
Порядок миграций с данными
Создайте сырую таблицу перед view, а view — перед любым запросом дашборда. Храните операторы создания в одном файле, запускайте их в порядке и записывайте номер батча с каждой строкой.
Раздельные базы данных для клиентов дрейфуют так же, как и одна общая база данных. Правило остается тем же: одно сохраненное body за вызов и одно место для его парсинга.
Держите парсер рядом с тестами
Поместите функцию парсинга в тот же репозиторий, что и тест, который читает один сохраненный body. Тест — это место, где дрейфующее поле проявляется первым, задолго до того, как это заметит график.
Назовите поля, которые ожидает парсер, в одной константе в начале файла. Рецензент тогда увидит все зависимости от формы ответа на одном экране.
Первый запуск, по порядку
- 1
Протестируйте один домен
Вызовите
bulk_drс одним известным вам доменом и прочитайте весь ответ. Запишите ключи, прежде чем писать какой-либо код парсинга. - 2
Запустите два батча
Дайте скрипту список из 400 доменов, чтобы запустились ровно два батча. Проверьте, что падение
_creditsсоответствует двум вызовам по одному кредиту каждый. - 3
Посчитайте сохраненные строки
Откройте файл SQLite и сравните количество строк с количеством батчей. Несоответствие означает, что батч был пропущен или сохранен дважды, и вы узнаете об этом до того, как это заметит дашборд.
- 4
Запланируйте полный список
Только после чистых тестовых запусков добавьте полный список и планировщик. Сохраняйте ненулевой выход, чтобы ошибка была заметна в первую ночь.
Перед тем как запланировать
- Сначала удалите дубликаты и приведите список к нижнему регистру
- Один раз решите в одной функции, остается
www.или нет - Храните ключ в хранилище секретов, а не в crontab
- Оповещайте о ненулевом коде выхода
- Запустите расчеты бюджета кредитов для вашего собственного объема