feat(x): парсинг X через api.fxtwitter.com — 30 живых хендлов как источники, адаптер x_user, полный текст твитов, метрики; старый syndication-путь (429) убран
This commit is contained in:
@@ -77,30 +77,64 @@ def search_x(query, limit=15, handles=None):
|
||||
extra={"handle": d["handle"]})
|
||||
items.append(it)
|
||||
|
||||
# путь 3: конкретные профили
|
||||
for h in (handles or [])[:4]:
|
||||
r = net.get(f"https://syndication.twitter.com/srv/timeline-profile/screen-name/{h}",
|
||||
headers=UA)
|
||||
if r.status != 200:
|
||||
continue
|
||||
m = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', r.text, re.S)
|
||||
if not m:
|
||||
continue
|
||||
try:
|
||||
data = json.loads(m.group(1))
|
||||
except Exception:
|
||||
continue
|
||||
entries = (((data.get("props") or {}).get("pageProps") or {}).get("timeline") or {}).get("entries") or []
|
||||
for e in entries[:12]:
|
||||
tw = ((e.get("content") or {}).get("tweet") or {})
|
||||
txt = tw.get("full_text") or tw.get("text") or ""
|
||||
if not txt:
|
||||
continue
|
||||
tid = tw.get("id_str") or ""
|
||||
it = Item("x", "tweet", url=f"https://x.com/{h}/status/{tid}" if tid else f"https://x.com/{h}",
|
||||
title=txt[:120], text=txt, author=h, source="x.com/" + h,
|
||||
published=tw.get("created_at", ""),
|
||||
extra={"likes": tw.get("favorite_count"), "rt": tw.get("retweet_count")})
|
||||
it.score = float(tw.get("favorite_count") or 0) ** 0.5
|
||||
# путь 3: конкретные профили через fxtwitter (без ключей, отдаёт полный текст)
|
||||
for h in (handles or [])[:6]:
|
||||
for it in profile(h, limit=12):
|
||||
items.append(it)
|
||||
|
||||
# дедуп по ссылке
|
||||
uniq, seen_u = [], set()
|
||||
for it in items:
|
||||
if it.url in seen_u:
|
||||
continue
|
||||
seen_u.add(it.url)
|
||||
uniq.append(it)
|
||||
return uniq[:max(limit, 12) if handles else len(uniq)]
|
||||
|
||||
|
||||
def profile(handle, limit=20, cursor=None):
|
||||
"""Лента профиля X через api.fxtwitter.com — без токенов и логина.
|
||||
|
||||
Возвращает список Item; при ошибке сети/404 — пустой список.
|
||||
"""
|
||||
h = (handle or "").lstrip("@").strip()
|
||||
if not h:
|
||||
return []
|
||||
url = f"https://api.fxtwitter.com/2/profile/{h}/statuses"
|
||||
if cursor:
|
||||
url += "?cursor=" + urllib.parse.quote(str(cursor))
|
||||
try:
|
||||
r = net.get(url, timeout=25, json_mode=True)
|
||||
except Exception:
|
||||
return []
|
||||
if r.status != 200:
|
||||
return []
|
||||
data = r.json() or {}
|
||||
out = []
|
||||
for t in (data.get("results") or []):
|
||||
if t.get("type") != "status":
|
||||
continue
|
||||
rt = t.get("raw_text")
|
||||
txt = (rt.get("text") if isinstance(rt, dict) else rt) or t.get("text") or ""
|
||||
if not isinstance(txt, str):
|
||||
txt = str(txt)
|
||||
if len(txt) < 15:
|
||||
continue
|
||||
tid = t.get("id") or ""
|
||||
author = (t.get("author") or {}).get("screen_name") or h if isinstance(t.get("author"), dict) else h
|
||||
likes = t.get("likes") or 0
|
||||
it = Item("x", "tweet",
|
||||
url=t.get("url") or f"https://x.com/{h}/status/{tid}",
|
||||
title=txt.strip().splitlines()[0][:140], text=txt,
|
||||
author=author, source="x.com/" + h,
|
||||
published=t.get("created_at", ""),
|
||||
extra={"handle": h, "lang": t.get("lang", ""), "likes": likes, "replies": t.get("replies"),
|
||||
"reposts": t.get("reposts"), "views": t.get("views"),
|
||||
"quote": (t.get("quote") or {}).get("url") if isinstance(t.get("quote"), dict) else None})
|
||||
it.score = float(likes) ** 0.5 + float(t.get("replies") or 0) ** 0.4
|
||||
out.append(it)
|
||||
if len(out) >= limit:
|
||||
break
|
||||
return out
|
||||
|
||||
return items[:limit * 2]
|
||||
Reference in New Issue
Block a user