update
This commit is contained in:
@@ -20,6 +20,7 @@ log = logging.getLogger("creator.crawl")
|
||||
MAX_TIEFE = 3
|
||||
MAX_SEITEN = 500
|
||||
SEITE_TIMEOUT = 30 # Sekunden pro Seite (Render bzw. PDF-Download)
|
||||
CRAWL_SETTLE_MS = 3000 # gedeckelter Settle nach domcontentloaded (SPA-Render); kein 30s-networkidle-Hang
|
||||
MAX_BYTES = 10_000_000 # 10 MB Deckel pro PDF
|
||||
_UA = "Mozilla/5.0 (creator-lernbot)"
|
||||
|
||||
@@ -46,6 +47,37 @@ def _is_pdf(url: str) -> bool:
|
||||
return url.lower().split("?")[0].rstrip("/").endswith(".pdf")
|
||||
|
||||
|
||||
def _scope_prefix(start_url: str) -> str:
|
||||
"""Erstes nicht-leeres Pfad-Segment der Start-URL als Crawl-Scope, z.B.
|
||||
`/learn/path/x` → `/learn`. Ohne Pfad-Segment → `""` (ganze Domain, kein Regress)."""
|
||||
seg = [s for s in urlparse(start_url).path.split("/") if s]
|
||||
return f"/{seg[0]}" if seg else ""
|
||||
|
||||
|
||||
def _in_scope(url: str, prefix: str) -> bool:
|
||||
"""Segment-genauer Prefix-Match (kein `/learn` ⊃ `/learning-x`). Leerer Prefix → alles erlaubt."""
|
||||
if not prefix:
|
||||
return True
|
||||
p = urlparse(url).path
|
||||
return p == prefix or p.startswith(prefix + "/")
|
||||
|
||||
|
||||
def _seiten_text(page) -> str:
|
||||
"""Haupttext der gerenderten Seite — Nav/Footer/Boilerplate per trafilatura entfernt.
|
||||
Fallback auf den rohen Body-Text, wenn die Extraktion leer/zu kurz ausfällt (Nicht-Artikel-Seiten)."""
|
||||
try:
|
||||
from trafilatura import extract # lazy: Backend startet auch ohne das Paket
|
||||
text = extract(page.content(), include_comments=False, include_tables=True) or ""
|
||||
except Exception:
|
||||
text = ""
|
||||
if len(text.strip()) >= 200:
|
||||
return text.strip()
|
||||
try:
|
||||
return page.inner_text("body").strip()
|
||||
except Exception:
|
||||
return text.strip()
|
||||
|
||||
|
||||
def crawl(start_url: str, ziel: Path, *, max_tiefe: int = MAX_TIEFE, max_seiten: int = MAX_SEITEN, cancelled=None) -> int:
|
||||
"""Crawlt ab start_url (nur gleiche Domain), rendert JS und legt Seiten/PDFs in `ziel` ab.
|
||||
|
||||
@@ -58,6 +90,7 @@ def crawl(start_url: str, ziel: Path, *, max_tiefe: int = MAX_TIEFE, max_seiten:
|
||||
|
||||
ziel.mkdir(parents=True, exist_ok=True)
|
||||
domain = urlparse(start_url).netloc
|
||||
prefix = _scope_prefix(start_url) # nur Links unter diesem Pfad-Segment folgen
|
||||
gesehen: set[str] = set()
|
||||
queue: list[tuple[str, int]] = [(urldefrag(start_url)[0], 0)]
|
||||
gespeichert = 0
|
||||
@@ -85,13 +118,14 @@ def crawl(start_url: str, ziel: Path, *, max_tiefe: int = MAX_TIEFE, max_seiten:
|
||||
continue
|
||||
|
||||
try:
|
||||
page.goto(url, wait_until="networkidle", timeout=SEITE_TIMEOUT * 1000)
|
||||
page.goto(url, wait_until="domcontentloaded", timeout=SEITE_TIMEOUT * 1000)
|
||||
except Exception as e:
|
||||
log.debug("crawl: goto unvollständig %s: %s", url, e) # trotzdem versuchen, Inhalt zu lesen
|
||||
try:
|
||||
text = page.inner_text("body").strip()
|
||||
page.wait_for_load_state("networkidle", timeout=CRAWL_SETTLE_MS)
|
||||
except Exception:
|
||||
text = ""
|
||||
pass # SPA mit Dauer-Traffic erreicht nie idle → nach Settle weiter, kein 30s-Hang
|
||||
text = _seiten_text(page) # Haupttext, Nav/Footer entfernt (Fallback: roher Body)
|
||||
if text:
|
||||
atomic_write_text(ziel / _name(url, ".txt"), f"QUELLE: {url}\n\n{text}")
|
||||
gespeichert += 1
|
||||
@@ -103,7 +137,8 @@ def crawl(start_url: str, ziel: Path, *, max_tiefe: int = MAX_TIEFE, max_seiten:
|
||||
for href in hrefs:
|
||||
nxt = urldefrag(href)[0]
|
||||
if (nxt.startswith(("http://", "https://"))
|
||||
and urlparse(nxt).netloc == domain and nxt not in gesehen):
|
||||
and urlparse(nxt).netloc == domain and _in_scope(nxt, prefix)
|
||||
and nxt not in gesehen):
|
||||
queue.append((nxt, tiefe + 1))
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
Reference in New Issue
Block a user