Skip to content

Rewrite as a member of the 2scraper family (v0.1.0) - #1

Merged
jehrr merged 6 commits into
mainfrom
rewrite
Sep 14, 2026
Merged

jehrr merged 6 commits into
mainfrom
rewrite

Conversation

@jehrr

@jehrr jehrr commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Replaces the previous four-file scraper with a member of the 2scraper family:
one parser, one page-state policy, one writer, three browser engines plus a
browserless client, and the family's row schema and exit codes.

Nothing of dubizzle_core.py / dubizzle_playwright.py /
dubizzle_selenium.py / dubizzle_pyppeteer.py survives. The old flags
(--categories, --max-pages, --output, --details) are gone.

What the site turned out to be

Captured 15 pages across six verticals and two languages before writing any
parser, then ran it live. The measurements that changed the design:

JSON-LD ItemList motors ✅, property ✅, classified / jobs / community: none
So the primary path is the SSR payload (__NEXT_DATA__, Redux action listings/fetchListingDataForQuery/fulfilled), universal across all six
JSON-LD is an enrichment — the only source for brand, seller_name, in_stock
joined on the ad's locale-stripped path, not its URL
What a block is the exit's country. A refusal can be HTTP 200
Ads per page 25 (motors/classified/jobs/community), 35 (both property indexes)
price null by design jobs 0/25, community 1/25

Two of those were nearly shipped wrong and were caught by measuring:

  • The Arabic JSON-LD join. An Arabic page publishes /ar/motors/… in its
    JSON-LD while its payload's absolute_url.ar is byte-identical to its
    .en. Keyed on the full URL the join matched 25 of 25 ads in English and
    0 of 25 in Arabic, silently emptying three columns while the run
    reported success. Keyed on the path it is 26/26 in both, verified by a live
    Arabic run.
  • A page past the end of a listing still renders one ad — the promoted
    Car of the Week, with a price node and a JSON-LD item. Parsing an empty
    page would have written one plausible phantom row per page a run overshot
    by. It is classified empty and not parsed.

Live verification

Every run through a UAE residential exit (country-ae Scraping Browser).

Run Result
Playwright, 2 pages used cars 52 rows, exit 0, complete
Playwright, 2 pages apartments for rent 70 rows, exit 0, complete
Playwright, 3 pages televisions 75 rows, exit 0, complete
Playwright, 1 page used cars in Arabic 26 rows, brand 26/26, exit 0
pyppeteer, 2 pages televisions 50 rows, exit 0, complete

273 rows in total; every sku unique, every page+position pair unique,
every priced row carrying AED and no unpriced row carrying one.

Selenium's content path was not live-verified, and the README says so
rather than implying otherwise: the machine this was built on could not give
a local chromedriver a UAE exit, and Selenium cannot use the authenticated
Scraping Browser endpoint the other two engines use. Its refusal path was
exercised end to end — exit 2 on a credentialled --cdp-endpoint with the
reason, and on a refused address a retry, exit 3, a debug dump and a refusal
to overwrite previous good output.

The browserless scraper_api_client.py does not get into this site, also
measured: four requests, all returning the "Pardon Our Interruption" page,
including two routed through a country-ae Scraping Browser with --cdp-url.
It reports exit 3 honestly rather than writing rows it did not get.

Defects found in inherited code (only a live run saw these)

  • pyppeteer crashed with a 40-line asyncio traceback when the Scraping
    Browser refused a connection, where Playwright printed one sentence and
    exited 5.
  • pyppeteer and Selenium crashed with AttributeError: 'NoneType' object has no attribute 'current' on a blocked page with no proxy pool. The branch was
    unreachable in the repo they came from (no-pool block budget 0); it is 1
    here, so it ran on the first try.
  • --locale defaulted to id-ID, inherited through two repos from a site in
    Indonesia.
  • page_flow.BLOCK_RETRIES_WITH_POOL was documented policy that nothing
    read. It is now the default for --proxy-block-retries.
  • A logger.warning with one placeholder and two arguments printed its raw
    template on a real run — Python's logging swallows that and carries on.
  • The repo's own secret check flagged this site's public ad identifiers (a
    32-hex string is both an API key's shape and every dubizzle ad's id), so it
    failed on its own repository.

Testing

510 offline checks, no engine library required. Two new ones earned their
place during this work and were verified by reverting the fix they catch:

  • every logger call's % placeholders counted against its arguments, by AST
    — needs no branch to execute;
  • every public name in a shared module checked for a consumer outside it.

Fixtures are cut from real captures by make_fixtures.py, which refuses to
write anything unless each trim parses identically to the untrimmed
original, column for column, and the scrub removed the agents' names, the
per-seller UUIDs, the Algolia key and the Sentry instrumentation.

Not done

  • The Docker image was not built locally (no Docker on this machine). CI
    builds and runs it, and the offline suite checks its COPY list against
    the entrypoint's real import graph.
  • The canary was not dispatched — it needs a repository secret.
  • Repo description, topics and homepage still need setting.

@jehrr
jehrr marked this pull request as ready for review September 14, 2026 15:03
@jehrr
jehrr merged commit f0b9ac1 into main Sep 14, 2026
8 checks passed
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.

1 participant