add safe public search-engine fallback
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
# Browser-only ProspectOS
|
# Browser-only ProspectOS
|
||||||
|
|
||||||
This is the stripped-down replacement for the larger ProspectOS application.
|
This is the stripped-down replacement for the larger ProspectOS application.
|
||||||
It has one live source: Google Search through a headless Playwright Chromium
|
It searches public engines (Google first, then Bing and DuckDuckGo if Google
|
||||||
browser. Each discovery run builds a query from category, keywords, and
|
challenges the server) through a headless Playwright Chromium browser. Each
|
||||||
location, filters results already present in the local catalogue by root
|
discovery run builds a query from category, keywords, and location, filters
|
||||||
domain, and stores only new prospects. The explorer supports search and
|
results already present in the local catalogue by root domain, and stores only
|
||||||
human review states (`pending`, `approved`, `rejected`, `contacted`).
|
new prospects. The explorer supports search and human review states
|
||||||
|
(`pending`, `approved`, `rejected`, `contacted`).
|
||||||
|
|
||||||
There is deliberately no source registry, email scraper, AI provider, CRM,
|
There is deliberately no source registry, email scraper, AI provider, CRM,
|
||||||
outreach sender, or automatic contact action.
|
outreach sender, or automatic contact action.
|
||||||
|
|||||||
+36
-20
@@ -82,18 +82,31 @@ def blocked(text: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def google_search(query: str, requested: int) -> list[dict[str, str]]:
|
def google_search(query: str, requested: int) -> list[dict[str, str]]:
|
||||||
|
"""Search public engines through Playwright, with safe engine fallback.
|
||||||
|
|
||||||
|
Google frequently challenges datacenter IPs. We do not bypass that
|
||||||
|
challenge; we close the context and try Bing, then DuckDuckGo, still using
|
||||||
|
a real headless browser and the same bounded result extraction.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
|
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
raise RuntimeError("playwright_not_installed") from exc
|
raise RuntimeError("playwright_not_installed") from exc
|
||||||
url = "https://www.google.com/search?q=" + quote_plus(query) + "&num=" + str(requested)
|
|
||||||
executable = os.environ.get("PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH", "").strip()
|
executable = os.environ.get("PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH", "").strip()
|
||||||
options: dict[str, Any] = {"headless": True}
|
options: dict[str, Any] = {"headless": True}
|
||||||
if executable:
|
if executable:
|
||||||
options["executable_path"] = executable
|
options["executable_path"] = executable
|
||||||
try:
|
engines = [
|
||||||
with sync_playwright() as p:
|
("google", "https://www.google.com/search?q=" + quote_plus(query) + "&num=" + str(requested), "a:has(h3)", "h3", {"google.com", "google.co.za"}),
|
||||||
browser = p.chromium.launch(**options)
|
("bing", "https://www.bing.com/search?q=" + quote_plus(query) + "&count=" + str(requested), "li.b_algo h2 a", "h2", {"bing.com", "bingj.com"}),
|
||||||
|
("duckduckgo", "https://html.duckduckgo.com/html/?q=" + quote_plus(query), "a.result__a", None, {"duckduckgo.com"}),
|
||||||
|
]
|
||||||
|
errors: list[str] = []
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(**options)
|
||||||
|
try:
|
||||||
|
for engine, url, selector, title_selector, excluded_hosts in engines:
|
||||||
|
context = None
|
||||||
try:
|
try:
|
||||||
context = browser.new_context(
|
context = browser.new_context(
|
||||||
locale="en-ZA",
|
locale="en-ZA",
|
||||||
@@ -108,15 +121,17 @@ def google_search(query: str, requested: int) -> list[dict[str, str]]:
|
|||||||
body = page.locator("body").inner_text()
|
body = page.locator("body").inner_text()
|
||||||
html = page.content()
|
html = page.content()
|
||||||
if blocked(body) or blocked(html):
|
if blocked(body) or blocked(html):
|
||||||
raise RuntimeError("google_blocked")
|
errors.append(engine + "_blocked")
|
||||||
|
continue
|
||||||
output: list[dict[str, str]] = []
|
output: list[dict[str, str]] = []
|
||||||
anchors = page.locator("a:has(h3)")
|
anchors = page.locator(selector)
|
||||||
for i in range(min(anchors.count(), requested * 3)):
|
for i in range(min(anchors.count(), requested * 3)):
|
||||||
anchor = anchors.nth(i)
|
anchor = anchors.nth(i)
|
||||||
title = " ".join((anchor.locator("h3").inner_text() or "").split())[:200]
|
title_node = anchor.locator(title_selector) if title_selector else anchor
|
||||||
|
title = " ".join((title_node.inner_text() or "").split())[:200]
|
||||||
href = anchor.get_attribute("href") or ""
|
href = anchor.get_attribute("href") or ""
|
||||||
domain = clean_domain(href)
|
domain = clean_domain(href)
|
||||||
if not title or not href or domain in {"google.com", "google.co.za"} or not domain:
|
if not title or not href or domain in excluded_hosts or not domain:
|
||||||
continue
|
continue
|
||||||
if not href.startswith(("http://", "https://")):
|
if not href.startswith(("http://", "https://")):
|
||||||
continue
|
continue
|
||||||
@@ -124,19 +139,20 @@ def google_search(query: str, requested: int) -> list[dict[str, str]]:
|
|||||||
output.append({"name": title, "website": href, "domain": domain})
|
output.append({"name": title, "website": href, "domain": domain})
|
||||||
if len(output) >= requested:
|
if len(output) >= requested:
|
||||||
break
|
break
|
||||||
return output
|
if output:
|
||||||
|
return output
|
||||||
|
errors.append(engine + "_empty")
|
||||||
|
except PlaywrightTimeout:
|
||||||
|
errors.append(engine + "_timeout")
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append(engine + "_error")
|
||||||
finally:
|
finally:
|
||||||
try:
|
if context is not None:
|
||||||
context.close()
|
try: context.close()
|
||||||
except Exception:
|
except Exception: pass
|
||||||
pass
|
finally:
|
||||||
browser.close()
|
browser.close()
|
||||||
except RuntimeError:
|
raise RuntimeError("search_engines_unavailable:" + ",".join(errors[-3:]))
|
||||||
raise
|
|
||||||
except PlaywrightTimeout as exc:
|
|
||||||
raise RuntimeError("browser_timeout") from exc
|
|
||||||
except Exception as exc:
|
|
||||||
raise RuntimeError("browser_error") from exc
|
|
||||||
|
|
||||||
|
|
||||||
def run_discovery(payload: dict[str, Any]) -> dict[str, Any]:
|
def run_discovery(payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ProspectOS · Browser Discovery</title><link rel="stylesheet" href="/style.css"></head>
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ProspectOS · Browser Discovery</title><link rel="stylesheet" href="/style.css"></head>
|
||||||
<body><header class="top"><div><span class="eyebrow">PROSPECTOS</span><h1>Browser discovery</h1><p>Find businesses through Google, remove anything already catalogued, then review the remaining prospects.</p></div><div class="source-pill">● Google · Playwright</div></header>
|
<body><header class="top"><div><span class="eyebrow">PROSPECTOS</span><h1>Browser discovery</h1><p>Find businesses through public search engines, remove anything already catalogued, then review the remaining prospects.</p></div><div class="source-pill">● Browser search · Playwright</div></header>
|
||||||
<main>
|
<main>
|
||||||
<section class="grid stats"><article><span>Catalogued prospects</span><strong id="total">0</strong></article><article><span>Pending review</span><strong id="pending">0</strong></article><article><span>Approved</span><strong id="approved">0</strong></article><article><span>Last run</span><strong id="lastRun">—</strong></article></section>
|
<section class="grid stats"><article><span>Catalogued prospects</span><strong id="total">0</strong></article><article><span>Pending review</span><strong id="pending">0</strong></article><article><span>Approved</span><strong id="approved">0</strong></article><article><span>Last run</span><strong id="lastRun">—</strong></article></section>
|
||||||
<section class="panel"><div class="section-head"><div><span class="eyebrow">DISCOVERY</span><h2>Find new prospects</h2><p>Search criteria are used only for this run. The system stores the business name, website, query, and discovery time.</p></div></div><form id="discoverForm" class="form-grid"><label>Category<input name="category" placeholder="Plumbers" required></label><label>Keywords<input name="keywords" placeholder="plumbing services" required></label><label>Location<input name="location" value="South Africa" required></label><label>Maximum results<select name="max_results"><option value="5">5</option><option value="10" selected>10</option><option value="25">25</option></select></label><div class="form-actions"><button class="primary" type="submit" id="discoverBtn">Run browser discovery</button><span id="discoverMessage" role="status"></span></div></form></section>
|
<section class="panel"><div class="section-head"><div><span class="eyebrow">DISCOVERY</span><h2>Find new prospects</h2><p>Search criteria are used only for this run. The system stores the business name, website, query, and discovery time.</p></div></div><form id="discoverForm" class="form-grid"><label>Category<input name="category" placeholder="Plumbers" required></label><label>Keywords<input name="keywords" placeholder="plumbing services" required></label><label>Location<input name="location" value="South Africa" required></label><label>Maximum results<select name="max_results"><option value="5">5</option><option value="10" selected>10</option><option value="25">25</option></select></label><div class="form-actions"><button class="primary" type="submit" id="discoverBtn">Run browser discovery</button><span id="discoverMessage" role="status"></span></div></form></section>
|
||||||
|
|||||||
Reference in New Issue
Block a user