"""Detect a scrape that came back un-rendered (a JavaScript "app shell").""" from __future__ import annotations import html import re from html.parser import HTMLParser # A reader prefixes its markdown with a ``Key: value`` block (Jina emits # Title / URL Source / Published Time / …). Only the body below it is content. _READER_HEADER_RE = re.compile( r"\A(?:(?:Title|URL Source|Published Time|Content Length|Images|Links|" r"Warning|Markdown Content):[^\n]*\n+)+", re.IGNORECASE, ) _HTML_DOC_RE = re.compile( r"\A(?:\ufeff|\s||<\?xml[^>]*>)*" r"(?:]*>[\s\S]*?" r"(?:enable|requires?|turn on|activate)\s+(?:your\s+)?javascript", re.IGNORECASE, ) _JS_INTERSTITIAL_RE = re.compile( r"\A(?:" r"(?:please\s+)?(?:enable|turn on|activate)\s+(?:your\s+)?javascript" r"|javascript\s+(?:is\s+)?(?:required|disabled)" r"|(?:this\s+)?(?:site|page|app|application)\s+requires?\s+javascript" r")\b", re.IGNORECASE, ) _EMPTY_APP_MOUNT_RE = re.compile( r"<(?:div|main|section)\b[^>]*(?:id|class)\s*=\s*['\"][^'\"]*" r"(?:app|root|__next|__nuxt)[^'\"]*['\"][^>]*>" r"(?:\s|)*", re.IGNORECASE, ) # Body shorter than this counts as "suspiciously empty — worth one retry". # Measured: shell stubs carry 150-170 chars of body, the real page ~4800. MIN_RENDERED_BODY_CHARS = 400 _MAX_SHELL_VISIBLE_BODY_CHARS = 80 _MAX_JS_INTERSTITIAL_CHARS = 200 class _VisibleBodyParser(HTMLParser): """Collect visible body text while ignoring script/style/template payloads.""" _HIDDEN = frozenset({"script", "style", "template", "noscript", "svg"}) def __init__(self) -> None: super().__init__(convert_charrefs=True) self.seen_body = False self.in_body = False self.hidden_depth = 0 self.all_text: list[str] = [] self.body_text: list[str] = [] def handle_starttag( self, tag: str, attrs: list[tuple[str, str | None]], ) -> None: name = tag.lower() if name == "body": self.seen_body = True self.in_body = True if name in self._HIDDEN: self.hidden_depth += 1 def handle_endtag(self, tag: str) -> None: name = tag.lower() if name in self._HIDDEN and self.hidden_depth: self.hidden_depth -= 1 if name == "body": self.in_body = False def handle_data(self, data: str) -> None: if self.hidden_depth or not data.strip(): return self.all_text.append(data) if self.in_body: self.body_text.append(data) def visible_text(self) -> str: parts = self.body_text if self.seen_body else self.all_text return " ".join(" ".join(parts).split()) def _visible_body_text(content: str) -> str: parser = _VisibleBodyParser() try: parser.feed(content) parser.close() except Exception: return "" return html.unescape(parser.visible_text()) def reader_body(content: str) -> str: """Strip the reader's ``Title:``/``URL Source:``… preamble, leaving content.""" return _READER_HEADER_RE.sub("", content or "", count=1).strip() def unrendered_kind(content: str) -> str | None: """Classify a scrape that carries no page content. Returns: ``"shell"`` — high confidence: a raw pre-hydration DOM (a reader returns markdown on success, so raw HTML means it could not convert the page) or an explicit "enable JavaScript" interstitial. Nothing is recoverable by fetching the same way again. ``"empty"`` — low confidence: the body is merely suspiciously short. A legitimately tiny page looks identical, so callers should re-try but must not turn this into an error. ``None`` — looks like real page content. """ text = (content or "").strip() if not text: return "empty" head = text[:4000] is_html = bool(_HTML_DOC_RE.match(text)) visible = _visible_body_text(text) if is_html else reader_body(text) if ( is_html and _SCRIPT_TAG_RE.search(head) and not visible ): return "shell" if ( is_html and _SCRIPT_TAG_RE.search(head) and len(visible) < _MAX_SHELL_VISIBLE_BODY_CHARS and _EMPTY_APP_MOUNT_RE.search(text) ): return "shell" if ( is_html and _NOSCRIPT_JS_RE.search(head) and len(visible) < MIN_RENDERED_BODY_CHARS ): return "shell" if ( not is_html and len(visible) < _MAX_JS_INTERSTITIAL_CHARS and _JS_INTERSTITIAL_RE.search(visible) ): return "shell" if len(reader_body(text)) < MIN_RENDERED_BODY_CHARS: return "empty" return None __all__ = ["MIN_RENDERED_BODY_CHARS", "reader_body", "unrendered_kind"]