parser: ETag/304-кэш, 61 авто-источник из 81 кандидата (+RSS AI/security), правила farm/weights, тесты sources_auto
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 20:34:52 +03:00
parent 31a8dd8c4d
commit d6ad507d7e
6 changed files with 233 additions and 22 deletions

View File

@@ -10,6 +10,78 @@ _cfg = config.load()
_lock = threading.Lock()
_last = {} # host -> ts
# ---------- условный GET: ETag / If-Modified-Since ----------
_DATA = __import__("pathlib").Path(__file__).resolve().parents[1] / "data"
_COND = _DATA / "http_cond.json"
_BODIES = _DATA / "http_cache"
_COND_DB = None
_cond_lock = threading.Lock()
def _cond_load():
global _COND_DB
if _COND_DB is None:
try:
import json as _j
_COND_DB = _j.loads(_COND.read_text(encoding="utf-8"))
except Exception:
_COND_DB = {}
return _COND_DB
def _cond_save():
try:
import json as _j
with _cond_lock:
_BODIES.mkdir(parents=True, exist_ok=True)
_COND.write_text(_j.dumps(_COND_DB, ensure_ascii=False), encoding="utf-8")
except Exception:
pass
def _body_path(key):
import hashlib
return _BODIES / (hashlib.sha1(key.encode("utf-8", "replace")).hexdigest() + ".body")
def _cond_ask(key, h):
rec = _cond_load().get(key) or {}
if rec.get("etag"):
h["If-None-Match"] = rec["etag"]
if rec.get("lm"):
h["If-Modified-Since"] = rec["lm"]
return bool(rec.get("etag") or rec.get("lm"))
def _cond_store(key, resp):
if resp.status != 200 or not resp.text or len(resp.text) > 1_500_000:
return
hdr = {str(k).lower(): v for k, v in (resp.headers or {}).items()}
etag, lm = hdr.get("etag"), hdr.get("last-modified")
if not etag and not lm:
return
try:
_BODIES.mkdir(parents=True, exist_ok=True)
p = _body_path(key)
p.write_text(resp.text, encoding="utf-8")
except Exception:
return
_cond_load()[key] = {"etag": etag, "lm": lm, "body": p.name,
"ts": int(time.time()), "size": len(resp.text)}
_cond_save()
def _cond_hit(key):
rec = _cond_load().get(key)
if not rec or not rec.get("body"):
return None
try:
txt = (_BODIES / rec["body"]).read_text(encoding="utf-8")
except Exception:
return None
return Resp(key.split("|")[0], 200, txt, "cache-304", headers={"x-omni-cache": "304"})
BLOCK_MARKS = ("just a moment", "cf-browser-verification", "attention required",
"ddos-guard", "checking your browser", "enable javascript and cookies")
@@ -118,9 +190,11 @@ def post_json(url, payload, headers=None, timeout=None):
def get(url, headers=None, timeout=None, json_mode=False, proxy=None):
"""Лестница: httpx -> curl_cffi -> curl_cffi с прокси. Возвращает первый осмысленный ответ.
"""Лестница: httpx -> curl_cffi -> curl_cffi с прокси + условный GET (ETag/304).
proxy=... — принудительный прокси для конкретного источника (напр. CF-форумы через DE).
Повторный запрос неизменного документа отдаётся из data/http_cache (304), это экономит
и трафик, и время прогона (RSS/HTML отдают ETag/Last-Modified почти всегда).
"""
timeout = timeout or _cfg.get("timeout", 25)
host = urllib.parse.urlsplit(url).netloc
@@ -135,23 +209,43 @@ def get(url, headers=None, timeout=None, json_mode=False, proxy=None):
if ck:
h["Cookie"] = ck
key = url + ("|" + proxy if proxy else "")
conditional = _cond_ask(key, h)
def finish(resp):
if conditional and resp.status in (304,):
hit = _cond_hit(key)
if hit:
return hit
if resp.status == 200:
_cond_store(key, resp)
return resp
if proxy:
forced = _try_curl(url, h, timeout, proxy)
if forced.ok and not forced.blocked:
return forced
return finish(forced)
alt = _try_httpx(url, h, timeout, proxy)
return alt if alt.status == 200 else forced
return finish(alt if alt.status == 200 else forced)
first = _try_httpx(url, h, timeout, None)
if conditional and first.status == 304:
hit = _cond_hit(key)
if hit:
return hit
if first.ok and not first.blocked:
return first
return finish(first)
second = _try_curl(url, h, timeout, None)
if conditional and second.status == 304:
hit = _cond_hit(key)
if hit:
return hit
if second.ok and not second.blocked:
return second
return finish(second)
proxy = _pick_proxy()
if proxy:
third = _try_curl(url, h, timeout, proxy)
if third.status == 200:
return third
return third
return second if second.status else first
return finish(third)
return finish(third)
return finish(second if second.status else first)