rpi: tg_bios.py — личные каналы участников; FloodWait ResolveUsername ~22 ч — размеры с t.me
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1a525e9073
commit
69ad9cf123
@@ -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`
|
||||
(маленькие). Персональные данные участников — в поиск/индекс не идут. 2026-10-18: CY Fast Track Talks — 2774 из 2774,
|
||||
аватарок 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.
|
||||
|
||||
@@ -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]))
|
||||
Reference in New Issue
Block a user