Files
omni-parse/omni/rank.py
Vlad Gothbreach c3eec76775
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
prod: чистая лента (дедуп перепечаток, анти-шум в score, категорийный вес), счётчик живых/мёртвых источников, таймауты net
2026-10-27 22:29:47 +03:00

99 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Скор записи: вес провайдера + вовлечённость + свежесть + ценник + объём текста + сигнал абуза."""
import re
import time
from .dates import parse_published
WEIGHT = {"reddit": 1.15, "x": 1.10, "forums": 1.0, "tavily": 1.0, "exa": 1.0,
"firecrawl": 0.9, "you": 0.9, "ddg": 0.75, "cards": 0.35, "treg": 0.8}
ABUSE_TERMS = re.compile(
r"(cve-\d{4}-\d+|0day|0-day|zero[- ]day|exploit|rce|sqli|xss|ssrf|rce|"
r"leak|leaked|dump|breach|combo[- ]?list|stealer|ransomware|botnet|credentials|"
r"password[s]?|api[- ]?key[s]?|token[s]?|аккаунт\w*|база|слив\w*|утечк\w*|комбо|"
r"взлом|эксплойт|уязвим\w*|парол\w*|ключи)",
re.I)
def _abuse(it):
"""Реальные абузы должны всплывать выше новостей: считаем сигнальные термины."""
blob = f"{it.title} {it.text}"[:2000]
hits = len({m.group(0).lower() for m in ABUSE_TERMS.finditer(blob)})
return min(4, hits)
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."""
dt = parse_published(published)
if dt is None:
return 0.3
days = max(0.0, (time.time() - dt.timestamp()) / 86400)
return max(0.0, 1.0 - days / 30.0)
def score(it):
base = it.score if it.score else 0.0
w = WEIGHT.get(it.provider, 0.7)
fresh = _fresh(it.published)
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) + cat - _noise(it))
it.score = round(total, 4)
return it
def rank(items):
return sorted((score(i) for i in items), key=lambda x: -x.score)