feat(superporn): nadrobienie katalogu z sitemapy
Deep-crawl przemielil listing HTML do konca (kursor 548, exhausted), a mimo to sitemapa ma sceny, ktorych nie mamy. Pomiar na osmiu submapach po 60 losowych adresow: 46% angielskich URL-i nieznanych, w nowszych submapach nawet 75%. Przy ok. 29 tys. adresow to rzedu 13 tys. scen. Listing nie wystawia calego katalogu. Skrypt, nie job w schedulerze, bo superporn nic juz nie publikuje: submapy sa ulozone chronologicznie, a najnowszy wpis ma date 2025-01-28. To nadrobienie raz. Wznawianie za darmo, bo bierzemy tylko adresy nieobecne w playback_sources. Sceny leca z backfill=True, zeby katalog 2022-2025 nie udawal swiezosci. Kontrola jakosci na pierwszej partii: obsada 20% (korpus superporna ma 17,2%), srednio 4,7 tagu (3,4), dlugosc w 100%, zero bledow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
17601fd0b8
commit
fa2a65b408
1 changed files with 180 additions and 0 deletions
180
scripts/superporn_sitemap_backfill.py
Normal file
180
scripts/superporn_sitemap_backfill.py
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
"""Jednorazowe nadrobienie katalogu superporn.com z sitemapy.
|
||||||
|
|
||||||
|
Dlaczego w ogóle: deep-crawl przemielił listing HTML do końca (kursor 548, status
|
||||||
|
`exhausted`), a mimo to sitemapa ma sceny, których nie mamy. Pomiar 2026-08-20 na
|
||||||
|
ośmiu submapach po 60 losowych adresów: **46% angielskich URL-i nieznanych**, przy
|
||||||
|
czym w nowszych submapach nawet 75%. Przy około 29 tys. angielskich adresów daje to
|
||||||
|
rzędu 13 tys. scen do odzyskania. Listing po prostu nie wystawia całego katalogu.
|
||||||
|
|
||||||
|
Dlaczego skrypt, a nie job w schedulerze: superporn NIC już nie publikuje. Submapy są
|
||||||
|
ułożone chronologicznie, a najnowszy wpis ma datę 2025-01-28, czyli serwis stoi od
|
||||||
|
ponad roku. To jest nadrobienie zaległości raz, nie stała maszyneria.
|
||||||
|
|
||||||
|
Wznawianie za darmo: bierzemy wyłącznie adresy, których nie ma w `playback_sources`,
|
||||||
|
więc ponowne odpalenie samo pomija to, co już weszło. Żadnego pliku stanu.
|
||||||
|
|
||||||
|
Sceny lecą z `backfill=True`, bo to katalog z lat 2022-2025. Bez tego stary import
|
||||||
|
udawałby świeżość w „+N nowych" i w ulubionych (patrz `scenes.backfill`).
|
||||||
|
|
||||||
|
Użycie (w kontenerze workera):
|
||||||
|
python3 /srv/scripts/superporn_sitemap_backfill.py --limit 200 --dry-run
|
||||||
|
python3 /srv/scripts/superporn_sitemap_backfill.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from app.connectors.direct_scrapers import ALL_BROWSE_SCRAPERS, SCRAPER_SOURCE_NAME
|
||||||
|
from app.db import session_scope
|
||||||
|
from app.extractors import browser_get
|
||||||
|
from app.ingest import _process_scene, get_or_create_source
|
||||||
|
from app.models.source import SourceKind
|
||||||
|
|
||||||
|
log = logging.getLogger("superporn-backfill")
|
||||||
|
|
||||||
|
_SITEMAP_INDEX = "https://www.superporn.com/sitemap.xml"
|
||||||
|
_LOC_RE = re.compile(r"<loc>([^<]+)</loc>")
|
||||||
|
# Bierzemy TYLKO angielskie adresy. Sitemapa zawiera te same sceny pod /nl/, /fr/, /es/
|
||||||
|
# i tak dalej, a to ta sama treść, nie nowa podaż.
|
||||||
|
_EN_SCENE_RE = re.compile(r"^https://www\.superporn\.com/video/")
|
||||||
|
# Ile stron szczegółów ciągniemy naraz. Bright Data to ryczałt, więc kosztu za GB nie ma,
|
||||||
|
# ale nie ma też powodu walić w serwis serią.
|
||||||
|
_CONCURRENCY = 5
|
||||||
|
|
||||||
|
|
||||||
|
def _scraper():
|
||||||
|
by_tag = {c.sitetag: c for c in ALL_BROWSE_SCRAPERS}
|
||||||
|
return by_tag["superporncom"]()
|
||||||
|
|
||||||
|
|
||||||
|
def _video_submaps(sc) -> list[str]:
|
||||||
|
"""Submapy wideo, NAJNOWSZE PIERWSZE (numeracja rośnie chronologicznie)."""
|
||||||
|
idx = browser_get(_SITEMAP_INDEX, timeout=60, proxy=sc._proxy).text
|
||||||
|
maps = [m for m in _LOC_RE.findall(idx) if "videos." in m]
|
||||||
|
|
||||||
|
def _num(u: str) -> int:
|
||||||
|
m = re.search(r"videos\.(\d+)\.xml", u)
|
||||||
|
return int(m.group(1)) if m else -1
|
||||||
|
|
||||||
|
return sorted(maps, key=_num, reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _unknown_urls(urls: list[str]) -> list[str]:
|
||||||
|
if not urls:
|
||||||
|
return []
|
||||||
|
with session_scope() as s:
|
||||||
|
known = {
|
||||||
|
r[0]
|
||||||
|
for r in s.execute(
|
||||||
|
text("SELECT page_url FROM playback_sources WHERE page_url = ANY(:u)"),
|
||||||
|
{"u": urls},
|
||||||
|
).fetchall()
|
||||||
|
}
|
||||||
|
return [u for u in urls if u not in known]
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_parse(args: tuple):
|
||||||
|
sc, url = args
|
||||||
|
try:
|
||||||
|
r = browser_get(url, timeout=sc._timeout, proxy=sc._proxy)
|
||||||
|
if r.status_code >= 400:
|
||||||
|
return None
|
||||||
|
return sc._parse_detail(url, r.text)
|
||||||
|
except Exception as e:
|
||||||
|
log.debug("detail padl %s: %s", url, e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument("--limit", type=int, default=0, help="max scen do zapisania (0 = bez limitu)")
|
||||||
|
p.add_argument("--dry-run", action="store_true", help="nie zapisuj, tylko policz")
|
||||||
|
p.add_argument("--max-maps", type=int, default=0, help="ogranicz liczbę submap (test)")
|
||||||
|
a = p.parse_args(argv)
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", stream=sys.stdout
|
||||||
|
)
|
||||||
|
# Bez tego httpx loguje KAŻDY fetch miniaturki i postęp tonie w szumie.
|
||||||
|
for noisy in ("httpx", "httpcore", "urllib3"):
|
||||||
|
logging.getLogger(noisy).setLevel(logging.WARNING)
|
||||||
|
sc = _scraper()
|
||||||
|
if not sc._proxy:
|
||||||
|
log.error("brak proxy, superporn bez niego oddaje CF 403; przerywam")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
submaps = _video_submaps(sc)
|
||||||
|
if a.max_maps:
|
||||||
|
submaps = submaps[: a.max_maps]
|
||||||
|
log.info("submap wideo: %d (od najnowszej)", len(submaps))
|
||||||
|
|
||||||
|
with session_scope() as s:
|
||||||
|
source_id = get_or_create_source(
|
||||||
|
s, kind=SourceKind.scraper, name=SCRAPER_SOURCE_NAME
|
||||||
|
).id
|
||||||
|
|
||||||
|
counters = {"seen": 0, "new": 0, "updated": 0, "skipped": 0, "errors": 0}
|
||||||
|
saved = scanned = unknown_total = 0
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
for i, sm in enumerate(submaps, 1):
|
||||||
|
try:
|
||||||
|
body = browser_get(sm, timeout=60, proxy=sc._proxy).text
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("submapa %s nie pobrana: %s", sm, e)
|
||||||
|
continue
|
||||||
|
urls = [u for u in _LOC_RE.findall(body) if _EN_SCENE_RE.match(u)]
|
||||||
|
scanned += len(urls)
|
||||||
|
todo = _unknown_urls(urls)
|
||||||
|
unknown_total += len(todo)
|
||||||
|
log.info(
|
||||||
|
"[%d/%d] %s: EN=%d nieznanych=%d (zapisane dotad: %d, %.0f min)",
|
||||||
|
i, len(submaps), sm.rsplit("/", 1)[-1], len(urls), len(todo), saved,
|
||||||
|
(time.time() - t0) / 60,
|
||||||
|
)
|
||||||
|
if not todo or a.dry_run:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if a.limit:
|
||||||
|
todo = todo[: max(0, a.limit - saved)]
|
||||||
|
if not todo:
|
||||||
|
break
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=_CONCURRENCY) as pool:
|
||||||
|
raws = list(pool.map(_fetch_parse, [(sc, u) for u in todo]))
|
||||||
|
|
||||||
|
for raw in raws:
|
||||||
|
if raw is None:
|
||||||
|
counters["errors"] += 1
|
||||||
|
continue
|
||||||
|
counters["seen"] += 1
|
||||||
|
try:
|
||||||
|
# backfill=True: to katalog 2022-2025, nie ma udawać nowosci.
|
||||||
|
_process_scene(
|
||||||
|
source_id=source_id, raw_scene=raw, counters=counters, backfill=True
|
||||||
|
)
|
||||||
|
saved += 1
|
||||||
|
except Exception:
|
||||||
|
counters["errors"] += 1
|
||||||
|
log.exception("ingest padl")
|
||||||
|
|
||||||
|
if a.limit and saved >= a.limit:
|
||||||
|
log.info("limit %d osiagniety", a.limit)
|
||||||
|
break
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"KONIEC: przejrzane EN=%d, nieznanych=%d, zapisane=%d, counters=%s, czas=%.0f min",
|
||||||
|
scanned, unknown_total, saved, counters, (time.time() - t0) / 60,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Loading…
Add table
Reference in a new issue