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
+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]))