Skip to content

feat: add SerpBase (Google) search engine via REST API - #158

Open
gefsikatsinelou wants to merge 2 commits into
mldsveda:mainfrom
gefsikatsinelou:feat/add-serpbase-engine
Open

feat: add SerpBase (Google) search engine via REST API#158
gefsikatsinelou wants to merge 2 commits into
mldsveda:mainfrom
gefsikatsinelou:feat/add-serpbase-engine

Conversation

@gefsikatsinelou

Copy link
Copy Markdown

Summary

Adds a new built-in scraper to PyScrappy: GoogleSearchScraper returns Google organic search results as clean JSON via the SerpBase Search API — no browser, no CAPTCHA handling, no selector maintenance.

Background

PyScrappy's 25 scrapers cover Wikipedia, news, GitHub, image search, and more, but there was no Google web search scraper. That's not an oversight: Google's search pages are effectively unreachable for scripted clients (consent interstitial, CAPTCHA walls, frequently changing markup — the same class of problem the IMDB scraper already solved by switching from blocked HTML to a JSON API). The IMDB scraper sets the precedent in this codebase: blocked page → structured API with an env-var key.

SerpBase (https://serpbase.dev) is a Google Search Results API that returns organic results (title, link, snippet, position) as JSON from a single POST. It follows the exact pattern the IMDB scraper uses, right down to the key-missing behavior.

Changes

  • New: src/pyscrappy/scrapers/google_search.pyGoogleSearchScraper (name = "google_search"):
    • POST https://api.serpbase.dev/google/search with a JSON body (q, optional hl/gl) and the key in the X-API-Key header
    • Maps the organic[] array to {title, link, snippet, position} records in Google's ranking order
    • Truncates to max_results (default 10) client-side
    • Sync (scrape) and native async (scrape_async) paths, mirroring the IMDBScraper structure
  • Updated: src/pyscrappy/scrapers/__init__.py — import + __all__ entry (alphabetical)
  • Updated: src/pyscrappy/__init__.py — import, built-in registration tuple, and __all__ entry, so the MCP server, CLI, and list_scrapers() expose it automatically (no core or MCP changes needed)
  • New: tests/test_scrapers/test_google_search.py — 6 unit tests (see Testing)
  • Updated: README.md (scraper count 24 → 25 + listing) and CHANGELOG.md (Unreleased entry)

Design decisions

Decision Rationale
API key via SERPBASE_API_KEY env var / api_key= Mirrors the existing OMDB_API_KEY pattern in IMDBScraper
POST + JSON body + X-API-Key header SerpBase's current documented contract (POST-only; GET + query-param keys are deprecated)
Key missing → ScrapeResult with a helpful ScrapeError (not an exception, not a silent empty list) Exactly how IMDBScraper degrades; no behavior change for users without a key
Business error envelope (HTTP 200 + status != 0) surfaced as ScrapeError The API reports failures in the envelope, so a failed request must not look like "no results found"
No new dependencies Reuses the existing HttpClient.post_json / AsyncHttpClient.post_json (httpx is already a core dependency)
Client-side truncation to max_results Avoids depending on an unverified server-side page-size parameter; documented in the docstring
Optional language/countryhl/gl Maps 1:1 to Google's own locale controls; omitted entirely when not requested

Testing

  • 6 new unit tests (mock post_json, no network): parse, missing key, error-envelope, max_results truncation, request shape/headers, async path
  • Full suite: 490 passed, 9 skipped (unchanged from main baseline behavior)
  • ruff check clean on all changed/new files; compileall clean; git diff --check clean

No API key is ever committed; tests mock upstream HTTP per the contributing guide.

Adds a new built-in scraper that returns Google organic search results as
clean JSON from the SerpBase Search API (POST /google/search, X-API-Key
header), with no browser, CAPTCHA handling, or selector maintenance.

- Mirror the IMDBScraper API-key pattern: SERPBASE_API_KEY env var or
  api_key=..., graceful degradation with a helpful error when unset
- Optional language/country params map to Google's hl/gl; results are
  truncated to max_results client-side
- Business error envelope (HTTP 200 + status!=0) surfaces as a ScrapeError,
  never as a silent empty result
- Registered in scrapers/__init__.py and package __init__.py (import,
  registration tuple, __all__) so MCP/CLI expose it automatically
- 6 unit tests (parse, missing key, error envelope, truncation, request
  shape, async path); full suite: 490 passed, 9 skipped

Closes nothing; standalone feature addition.
@gefsikatsinelou gefsikatsinelou changed the title feat: add GoogleSearchScraper via the SerpBase Search API feat: add SerpBase (Google) search engine via REST API Aug 18, 2026
@vedaant00
vedaant00 requested a lite review from Copilot August 18, 2026 07:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new built-in, API-backed Google web search scraper (GoogleSearchScraper) that retrieves organic results as structured JSON via SerpBase, and wires it into the package’s built-in scraper exports/registry with accompanying documentation and unit tests.

Changes:

  • Introduces GoogleSearchScraper with sync + native async implementations using HttpClient.post_json / AsyncHttpClient.post_json.
  • Registers/exports the new scraper via pyscrappy.scrapers and top-level pyscrappy so it appears in list_scrapers() and related entry points.
  • Adds unit tests and updates README + changelog to reflect the new built-in scraper.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/pyscrappy/scrapers/google_search.py New SerpBase-backed Google search scraper (sync/async) and result mapping.
tests/test_scrapers/test_google_search.py Unit tests for parsing, error handling, request shape, truncation, and async path.
src/pyscrappy/scrapers/__init__.py Exposes GoogleSearchScraper via scraper package imports and __all__.
src/pyscrappy/__init__.py Registers and exports GoogleSearchScraper at the top-level package.
README.md Updates built-in scraper count and mentions GoogleSearchScraper in the list.
CHANGELOG.md Adds an Unreleased entry documenting GoogleSearchScraper.
Suppressed comments (1)

src/pyscrappy/scrapers/google_search.py:72

  • The max_results docstring implies the SerpBase API response is truncated, but truncation is performed client-side after parsing. Update the wording to match the implementation (and the PR description).
            max_results: Maximum number of organic results to return (the API
                response is truncated to this many records).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +47 to +52
def test_missing_key_returns_helpful_error(self):
s = GoogleSearchScraper()
r = s.scrape(query="anything")
assert r.data == []
assert "SERPBASE_API_KEY" in r.errors[0].message
assert "serpbase.dev" in r.errors[0].message
Comment thread README.md

- **`GenericScraper`** — scrape any URL with auto-extraction (text, links, images, tables, metadata)
- **Data / research** — **`WikipediaScraper`**, **`StockScraper`** (Yahoo Finance), **`NewsScraper`** (RSS/Atom), **`GitHubScraper`**, **`HackerNewsScraper`**, plus weather, crypto, currency, dictionary, image, LinkedIn-jobs, and book search
- **Data / research** — **`WikipediaScraper`**, **`StockScraper`** (Yahoo Finance), **`NewsScraper`** (RSS/Atom), **`GitHubScraper`**, **`HackerNewsScraper`**, **`GoogleSearchScraper`** (Google organic results as JSON via the SerpBase API), plus weather, crypto, currency, dictionary, image, LinkedIn-jobs, and book search
Comment thread src/pyscrappy/scrapers/google_search.py Outdated
Comment on lines +11 to +13
pass ``api_key`` to the constructor. New accounts start with 100 free searches
(no credit card required); afterwards it is pay-as-you-go ($0.30 per 1k
queries).
Comment thread src/pyscrappy/scrapers/google_search.py Outdated
detail = payload.get("error") or payload.get("message") or "request failed"
return ScrapeResult(
data=[],
metadata=ScrapeMetadata(scraper=self.name),
- test: clear SERPBASE_API_KEY via monkeypatch for deterministic missing-key test
- docs: mention GoogleSearchScraper alongside IMDBScraper in README API-key note
- docs: link to serpbase.dev instead of hardcoding trial limits/pricing
- fix: include source_urls in error-path metadata (consistent with success path)
- style: ruff format compliance
@gefsikatsinelou

Copy link
Copy Markdown
Author

Addressed the review comments in dc0a105:

  • test: test_missing_key_returns_helpful_error now clears SERPBASE_API_KEY via monkeypatch, so it's deterministic regardless of the runner's environment
  • README: the API-key note now mentions GoogleSearchScraper/SERPBASE_API_KEY alongside IMDBScraper/OMDB_API_KEY
  • docstrings/errors: removed hardcoded trial limits and pricing; now point to https://serpbase.dev for current terms
  • error path: _build_result now includes source_urls in metadata on envelope errors, matching the success path and IMDBScraper's behavior
  • lint: ruff format compliance fixed (the failing CI check)

Full local run: 493 passed, 6 skipped; ruff check and ruff format --check both clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants