prod: чистая лента (дедуп перепечаток, анти-шум в score, категорийный вес), счётчик живых/мёртвых источников, таймауты net
Some checks failed
ci / test (3.10) (push) Has been cancelled
ci / test (3.11) (push) Has been cancelled
ci / test (3.12) (push) Has been cancelled

This commit is contained in:
Vlad Gothbreach
2026-10-27 22:29:47 +03:00
parent 3ca87f1db0
commit c3eec76775
6 changed files with 80 additions and 11 deletions

View File

@@ -133,7 +133,8 @@ def _pick_proxy():
return random.choice(ps) if ps else None
def _try_httpx(url, headers, timeout, proxy):
def _try_httpx(url, headers, timeout, proxy, _min=10):
timeout = max(_min, min(timeout, 12))
try:
import httpx
kw = {"headers": headers, "timeout": timeout, "follow_redirects": True}
@@ -148,6 +149,7 @@ def _try_httpx(url, headers, timeout, proxy):
def _try_curl(url, headers, timeout, proxy, impersonate="chrome131"):
timeout = max(timeout, 8)
try:
from curl_cffi import requests as cr
kw = {"headers": headers, "timeout": timeout, "impersonate": impersonate, "allow_redirects": True}

View File

@@ -24,6 +24,53 @@ def _abuse(it):
KIND_BONUS = {"tweet": 0.15, "thread": 0.25, "tg_post": 0.2, "card": 0.05, "result": 0.1, "post": 0.1}
# Категорийный вес: то, ради чего лента существует (абузы/панели/ключи/фермы),
# поднимаем выше новостного шума. Значения проверены на текущем индексе.
CAT_BONUS = {"abuse": 0.9, "panels": 0.8, "keys": 0.7, "treg": 0.35, "ai": 0.25,
"web3": 0.2, "tools": 0.2, "news": -0.35, "misc": -0.2}
# Мусор, который не должен всплывать никогда: служебные заголовки, оглавления,
# changelog/release notes, голые URL, склейки-«лапши» и пустые обрывки.
NOISE_RE = re.compile(
r"(chapter\s*list|release\s*notes|changelog|table\s*of\s*contents|"
r"^\s*(index|readme|docs?|documentation|wiki)\s*$|"
r"^\s*https?://\S+\s*$|\|\|)", re.I)
SHOUT_RE = re.compile(r"^[^a-zа-я]{12,}$")
def _noise(it):
"""0 = чисто, иначе штраф. Считается по заголовку и началу текста."""
title = (it.title or "").strip()
if not title:
return 2.0
penalty = 0.0
if len(title) < 8:
penalty += 0.6
if len(title) > 220:
penalty += 0.8
if NOISE_RE.search(title):
penalty += 1.6
# Голый CVE-дамп (тысячи одинаковых описаний из фида) — полезен точечно,
# но не должен занимать верх ленты: топ должен принадлежать находкам.
if re.match(r"^CVE-\d{4}-\d+", title):
penalty += 1.4
# Обрывок переписки/цитата вместо заголовка: «> be ...», «хотелось бы отметить».
if title.startswith(">"):
penalty += 1.5
elif title[:1].islower():
penalty += 1.0
if SHOUT_RE.match(title):
penalty += 0.35
if sum(c.isalnum() for c in title) < len(title) * 0.4:
penalty += 0.4
txt = (it.text or "")
if len(txt) > 4000 and len(title) < 25:
penalty += 0.7
if not txt and len(title) < 40:
penalty += 0.3
return min(3.0, penalty)
def _fresh(published):
"""Unix seconds/milliseconds, ISO or RFC 2822. Return freshness in 0..1."""
@@ -41,8 +88,9 @@ def score(it):
priced = 0.35 if it.price else 0.0
body = min(1.0, len(it.text) / 1200.0)
eng = min(3.0, base ** 0.5) if base > 1 else base
cat = CAT_BONUS.get((getattr(it, "category", "") or "").lower(), 0.0)
total = (w * (1.0 + eng) + fresh * 1.2 + priced + body + KIND_BONUS.get(it.kind, 0.0)
+ 0.45 * _abuse(it))
+ 0.45 * _abuse(it) + cat - _noise(it))
it.score = round(total, 4)
return it

View File

@@ -296,6 +296,8 @@ class Handler(BaseHTTPRequestHandler):
"sources_enabled": sum(1 for x in all_s if fl.get(x.name, {}).get("enabled", True)),
"alive": sum(1 for x in all_s if (fl.get(x.name, {}).get("items") or 0) > 0),
"tested": sum(1 for x in all_s if fl.get(x.name, {}).get("checked")),
"dead": sum(1 for x in all_s if fl.get(x.name, {}).get("checked")
and not (fl.get(x.name, {}).get("items") or 0)),
"categories": cats,
})

View File

@@ -232,11 +232,25 @@ def browse2(q="", names=None, limit=60, offset=0, sort="score", priced=False, ty
"price": "ORDER BY CAST(price AS REAL) DESC, score DESC",
"old": "ORDER BY published ASC, id ASC",
"quality": "ORDER BY score DESC, id DESC"}.get(sort, "ORDER BY score DESC, id DESC")
pull, skip = limit, offset
if sort == "quality":
# Перепечатки одного и того же поста из разных источников схлопываем:
# берём с запасом и оставляем по одной записи на нормализованный заголовок.
pull, skip = min(600, limit * 4), 0
with db() as c:
total = c.execute("SELECT COUNT(*) FROM items " + where, args).fetchone()[0]
rows = c.execute("SELECT id,provider,kind,source,title,text,author,published,score,price,currency,url,types "
"FROM items " + where + order + " LIMIT ? OFFSET ?",
args + [limit, offset]).fetchall()
args + [pull, skip]).fetchall()
if sort == "quality":
seen, uniq = set(), []
for r in rows:
t = "".join(ch for ch in (r[4] or "").lower() if ch.isalnum())[:60] or str(r[0])
if t in seen:
continue
seen.add(t)
uniq.append(r)
rows = uniq[offset:offset + limit]
keys = ("id", "provider", "kind", "source", "title", "text", "author", "published", "score", "price", "currency", "url", "types")
return [dict(zip(keys, r)) for r in rows], total