rpi: tg_bios.py — личные каналы участников; FloodWait ResolveUsername ~22 ч — размеры с t.me

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gilfanovr
2026-10-18 21:22:09 +03:00
co-authored by Claude Opus 5.5
parent 1a525e9073
commit 69ad9cf123
2 changed files with 112 additions and 0 deletions
+6
View File
@@ -67,3 +67,9 @@ sudo systemctl enable --now tg-export.timer
с `tg-export.service` — одна сессия): `/mnt/backup/tg/members/<id>_<название>/members.{json,csv}` + `avatars/<id>.jpg` с `tg-export.service` — одна сессия): `/mnt/backup/tg/members/<id>_<название>/members.{json,csv}` + `avatars/<id>.jpg`
(маленькие). Персональные данные участников — в поиск/индекс не идут. 2026-10-18: CY Fast Track Talks — 2774 из 2774, (маленькие). Персональные данные участников — в поиск/индекс не идут. 2026-10-18: CY Fast Track Talks — 2774 из 2774,
аватарок 1619. Разовая выгрузка, не по таймеру. аватарок 1619. Разовая выгрузка, не по таймеру.
- **Личные каналы участников** — `app/tg_bios.py`: bio и «личный канал» каждого участника (GetFullUser, ~1 запрос/с —
2765 профилей за ~55 мин без ограничений) → `bios.json`. 2026-10-18 CY Fast Track Talks: личный канал у 145 из 2765,
топ — `top-personal-channels.txt` в папке участников. ⚠️ Второй шаг скрипта (размер каждой ссылки через
`get_entity` = ResolveUsername) после ~190 запросов получил **FloodWait 77 545 с (~22 ч)** — остановлен; размеры
публичных каналов взяты со страниц `t.me/<username>` (без сессии). Массово резолвить username через API нельзя —
брать `t.me`-страницы или `full.chats` из GetFullUser.
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Ссылки на группы/каналы в профилях участников (владелец 2026-10-18): по members.json группы — bio и «личный канал»
каждого участника (GetFullUser, ~1 запрос/с), затем размер каждой найденной группы/канала → топ по участникам.
sudo -u tgexport sh -c '. /etc/tg-export/env; /opt/tg-export/venv/bin/python /opt/tg-export/tg_bios.py <папка members>'
Результат в той же папке: bios.json (id → bio, личный канал; докачка с места остановки), links.json (группа/канал →
кто ссылается, размер), top.txt. ⚠️ Не параллельно с tg-export.service (одна сессия).
"""
import asyncio
import json
import os
import re
import sys
from pathlib import Path
from telethon import TelegramClient, errors
from telethon.tl.functions.channels import GetFullChannelRequest
from telethon.tl.functions.users import GetFullUserRequest
LINK = re.compile(r"(?:https?://)?(?:t\.me|telegram\.me)/(?:s/)?([A-Za-z][\w]{3,31})(?![\w/])|(?<![\w@])@([A-Za-z][\w]{3,31})")
SKIP = {"joinchat", "addstickers", "share", "proxy", "socks", "iv", "c", "addlist", "boost"}
async def main(folder):
folder = Path(folder)
members = json.loads((folder / "members.json").read_text())["members"]
bios_f = folder / "bios.json"
bios = json.loads(bios_f.read_text()) if bios_f.exists() else {}
c = TelegramClient("/var/lib/tg-export/tg.session", int(os.environ["TG_API_ID"]), os.environ["TG_API_HASH"])
c.flood_sleep_threshold = 0 # FloodWait ловим сами, с логом
await c.start()
try:
todo = [m for m in members if not m["bot"] and not m["deleted"] and str(m["id"]) not in bios]
print(f"профилей: {len(members)}, уже есть {len(bios)}, осталось {len(todo)}", flush=True)
for i, m in enumerate(todo, 1):
while True:
try:
full = await c(GetFullUserRequest(m["id"]))
break
except errors.FloodWaitError as e:
print(f" FloodWait {e.seconds} с на {i}/{len(todo)}", flush=True)
await asyncio.sleep(e.seconds + 2)
except Exception as e:
full = None
print(f" {m['id']}: {type(e).__name__}", flush=True)
break
rec = {"username": m["username"], "bio": "", "channel": None}
if full:
rec["bio"] = full.full_user.about or ""
pc = getattr(full.full_user, "personal_channel_id", None)
if pc:
ch = next((x for x in full.chats if x.id == pc), None)
rec["channel"] = {"id": pc, "username": getattr(ch, "username", None), "title": getattr(ch, "title", None)}
bios[str(m["id"])] = rec
if i % 50 == 0:
bios_f.write_text(json.dumps(bios, ensure_ascii=False))
print(f" {i}/{len(todo)}", flush=True)
await asyncio.sleep(1.0)
bios_f.write_text(json.dumps(bios, ensure_ascii=False))
# ссылки: username → кто ссылается
refs = {}
for uid, r in bios.items():
found = set()
for a, b in LINK.findall(r["bio"] or ""):
u = (a or b).lower()
if u not in SKIP:
found.add(u)
if r["channel"] and r["channel"].get("username"):
found.add(r["channel"]["username"].lower())
for u in found:
refs.setdefault(u, set()).add(r["username"] or uid)
print(f"уникальных ссылок: {len(refs)}", flush=True)
links = {}
for i, (u, who) in enumerate(sorted(refs.items()), 1):
info = {"refs": sorted(who), "kind": None, "title": None, "members": None}
while True:
try:
e = await c.get_entity(u)
if getattr(e, "broadcast", None) is not None or getattr(e, "megagroup", None) is not None:
full = await c(GetFullChannelRequest(e))
info.update(kind="channel" if e.broadcast else "group", title=e.title,
members=full.full_chat.participants_count)
else:
info["kind"] = "user" if hasattr(e, "first_name") else type(e).__name__
break
except errors.FloodWaitError as ex:
print(f" FloodWait {ex.seconds} с на ссылке {i}", flush=True)
await asyncio.sleep(ex.seconds + 2)
except Exception as ex:
info["kind"] = f"error:{type(ex).__name__}"
break
links[u] = info
await asyncio.sleep(1.5)
(folder / "links.json").write_text(json.dumps(links, ensure_ascii=False, indent=1))
top = sorted(((u, x) for u, x in links.items() if x["members"]), key=lambda t: -t[1]["members"])
lines = [f"{n}. {x['title']} (@{u}) — {x['kind']}, {x['members']} участников; в профилях: {', '.join('@' + w if not w.isdigit() else w for w in x['refs'][:5])}"
for n, (u, x) in enumerate(top[:30], 1)]
(folder / "top.txt").write_text("\n".join(lines) + "\n")
print("\n".join(lines[:10]))
finally:
await c.disconnect()
asyncio.run(main(sys.argv[1]))