Задача
Собрать участников группы из истории сообщений и вытащить доступные поля профиля в Excel и JSON. Работает через официальный MTProto-клиент telethon, без браузера, прокси и разбора веб-интерфейса.
Как делал
Главное решение — две фазы вместо одного прохода. Из истории сообщений берутся только sender_id, и это почти бесплатный запрос. Дорогие GetFullUserRequest делаются уже по списку уникальных ID, поэтому вместо обращения на каждое сообщение уходит обращение на уникального человека: на группе в десятки тысяч сообщений это сжимает десятки тысяч запросов до сотен.
async for msg in client.iter_messages(entity, **msg_kwargs):
if msg.sender_id and msg.sender_id > 0: # каналы дают отрицательный, анонимы — 0
user_ids.add(msg.sender_id)
if min_msg_id is None or msg.id < min_msg_id:
min_msg_id = msg.id # самый старый id прохода — точка отсчёта для следующего
count += 1
Знак sender_id отсекает каналы и анонимных авторов без отдельной проверки. min_msg_id становится offset_id следующего прохода: обход уходит вглубь истории, а не крутится по верхней тысяче сообщений. Цель считается как limit × passes, и если она уже набрана, скрипт вообще не начинает читать.
Отдельная особенность Telegram: phone приходит из GetFullUserRequest только для тех, кто уже в контактах. У остальных поле пустое, поэтому телефоны на выходе — редкая строка, а не основная колонка. Это заложено в дизайн: скрипт не пытается обойти ограничение, а просто пишет пустую ячейку и отдаёт пустую ячейку.
Авторизация — файл сессии, который telethon создаёт при первом входе. Дальше инструмент стартует сразу, без кода из Telegram.
Задержки не фиксированные, а случайные в диапазоне: random.uniform(7, 12) между запросами данных и random.uniform(4, 6) каждые 200 сообщений. Ровный ритм — тот самый рисунок, по которому flood-контроль Telegram распознаёт автоматизацию. При FloodWait скрипт не паникует, а пишет чекпоинт и выходит: перезапуск продолжит с того же места.
delay = random.uniform(*DELAY_USERS) # 7-12 секунд, а не фиксированные 7
...
if idx % CHECKPOINT_EVERY == 0:
if SAVE_JSON:
with open(users_path, 'w', encoding='utf-8') as f:
json.dump(users, f, ensure_ascii=False, indent=2)
await asyncio.sleep(delay)
Прогресс переживает перезапуск двумя файлами: _ids.json хранит собранные ID, _meta.json — offset, число проходов и сколько сообщений уже прочитано. При следующем запуске из них собирается remaining, то есть докачиваются только те, кого ещё нет. Без этого группа в десять тысяч сообщений после каждого Flood разбиралась бы заново.
if os.path.exists(users_path):
with open(users_path, 'r', encoding='utf-8') as f:
users = json.load(f)
done_ids = {u['id'] for u in users}
remaining = [uid for uid in ids_list if uid not in done_ids]
if not remaining:
print(f" [2] Все {len(ids_list)} уже обработаны!")
Телефон в Excel уводится из числового формата: без number_format = '@' таблица переписывает номер в число и теряет вид, а без плюса он читается как локальный. Имя листа дополнительно чистится от ?*/\[]: и режется до 31 символа — ровно то, что Excel не принимает.
if field == 'phone':
val = u.get(field, '')
if val and not val.startswith('+'):
val = '+' + val
...
if field == 'phone' and val:
cell.number_format = '@' # иначе Excel перепишет номер в число
ws.title = safe_name(group_name) # re.sub(r'[?*/\\[\]:]', '', text)[:31]
Лист «Сводка» собирается функцией, где у строки три состояния: false — не выводить, строка — своя подпись, иначе подпись по умолчанию. В неё же подтягивается total_scanned из _meta.json, поэтому видно, сколько сообщений реально прочитано, а не сколько найдено ID.
Конфиг написан не как JSON, а как JSONC: комментарии и висячие запятые вырезаются перед json.loads, так что каждое поле задержки описано прямо над своим значением.
if stripped.startswith('#') or stripped.startswith('//'):
continue
...
text = re.sub(r',\s*([}\]])', r'\1', text) # висячие запятые
return json.loads(text)
Итог
Консольный инструмент на telethon и openpyxl: две фазы, чекпоинты, случайные задержки, восстановление после FloodWait. Набор полей, фильтры, лимиты, задержки и состав сводки задаются одним JSONC-конфигом — под другую группу скрипт не правится, а просто перенастраивается.