From c67f439413595d1866b33f8d73905956e204e41b Mon Sep 17 00:00:00 2001 From: jehrr <79864894+jehrr@users.noreply.github.com> Date: Wed, 23 Sep 2026 13:57:03 +0000 Subject: [PATCH 1/2] Remove donor-site leftovers (Akamai/headless, shop/product modes, euro/IDR, auctions) - --help: scraper_api_client "auction scraper" description and /p/ URL help; diff_runs --price-tolerance-pct "quotes IDR"; playwright --pages "infinitely scrolling search". - playwright --concurrency warning: "refuses a headless browser ... by answering NOTHING at all" -> refuses exits outside the UAE. - Engines, captcha_solver, output_writer: Akamai 394-byte headless refusal, --mode product/shop, unaddressable search, /p/ hub prose replaced with this repo's measured Imperva/UAE facts or deleted; contradictory "HEADFUL by default" comments removed; unused shop_facts field removed. - diff_runs: euro/18 locales/auction prose -> AED, two languages, Car of the Week lifecycle; farfetch-style example filenames -> cars. - CONTRIBUTING: site-change section rewritten for __NEXT_DATA__ / JSON-LD join / DOM fallback; datacentre note corrected; pyppeteer crash attributed to the sibling it happened in. - Issue templates: donor options (reserve price, favorite_count, suggested lots, 100 cap, headless refused) and orphan lines removed; bug report example prefix/count fixed. - .gitignore/.dockerignore: dubizzle_listings.* -> dubizzle_products.*. - smoke_test: CATAWIKI_* env names -> DUBIZZLE_*; EUR comment -> AED. - README: last badge "runs without an account" -> "needs a UAE exit". - CHANGELOG: new [Unreleased] section. Co-Authored-By: Claude Opus 5.5 --- .dockerignore | 3 +- .github/ISSUE_TEMPLATE/bug_report.yml | 4 +- .github/ISSUE_TEMPLATE/site_changed.yml | 25 ++++---- .gitignore | 4 +- CHANGELOG.md | 38 ++++++++++++ CONTRIBUTING.md | 72 +++++++++------------- README.md | 2 +- captcha_solver.py | 23 ++++--- diff_runs.py | 46 ++++++-------- output_writer.py | 80 +++++++++---------------- playwright_scraper.py | 73 ++++++++-------------- puppeteer_scraper.py | 39 ++++-------- scraper_api_client.py | 20 +++---- selenium_scraper.py | 37 ++++-------- smoke_test.py | 15 ++--- 15 files changed, 209 insertions(+), 272 deletions(-) diff --git a/.dockerignore b/.dockerignore index 9b99008..cee7160 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,7 +11,8 @@ __pycache__/ .venv/ venv/ env/ -dubizzle_listings.* +dubizzle_products.* +dubizzle_products_*.* *_debug.html *_debug.png live_results/ diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 13b6a04..e50286d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -92,7 +92,7 @@ body: placeholder: | python3 playwright_scraper.py \ --url "https://uae.dubizzle.com/motors/used-cars/" \ - --pages 3 --out grills + --pages 3 --out cars validations: required: true @@ -112,7 +112,7 @@ body: attributes: label: What you expected instead placeholder: >- - 96 products, as the README says a category page yields. + 25 ads a page, as the README says a motors listing yields. validations: required: true diff --git a/.github/ISSUE_TEMPLATE/site_changed.yml b/.github/ISSUE_TEMPLATE/site_changed.yml index edd796e..d48cb8b 100644 --- a/.github/ISSUE_TEMPLATE/site_changed.yml +++ b/.github/ISSUE_TEMPLATE/site_changed.yml @@ -21,27 +21,24 @@ body: instead `brand`, `seller_name` or `in_stock` are empty while everything else is fine, it is the page's JSON-LD ItemList or the path-keyed join to it, not the payload. - not carry it: bids arrive at hydration. - So "no rows at all" and "rows with no prices" are two different + So "no rows at all" and "no brand / seller_name" are two different breakages with two different fixes, and `price_source` on the rows - tells you which happened: `next_data+dom` is healthy, `next_data` - means nothing hydrated. + tells you which source each row's price came from: `next_data+jsonld` + where the page publishes both, `next_data` on the verticals with no + ItemList, `dom` on the fallback path. - type: dropdown id: what_broke attributes: label: What stopped matching options: - - No rows at all on a page that visibly has lots (the payload moved) - - Rows come out with no prices (the card, or its price class, moved) + - No rows at all on a page that visibly has ads (the payload moved) + - Rows come out with no prices on motors, property or classified - brand / seller_name / in_stock empty (the JSON-LD join moved) - - A null price with no reserve_price_set flag (the invariant broke) - - favorite_count empty on every row (the card's counter moved) - - Pagination stops early, repeats a page, or runs past the 100 cap + - Pagination stops early, repeats a page, or runs past the site's totalPages - One vertical returns nothing while the others are fine - The Arabic pages lost brand / seller_name / in_stock - - A no-results search returns 24 suggested lots as if they were real - Something else validations: required: true @@ -52,8 +49,8 @@ body: label: URL description: >- The exact URL, its locale prefix, and how the browser was reached. - Please say whether the run was headful or over --cdp-endpoint: a - headless run is refused by the site and is not a parser problem. + Please say which country the exit was in: an exit outside the UAE is + refused by the site and is not a parser problem. placeholder: "https://uae.dubizzle.com/motors/used-cars/ (English, headless Chromium over --cdp-endpoint, exit: UAE residential)" validations: required: true @@ -74,8 +71,8 @@ body: here (`AED` in one, the digits in `[data-testid="listing-price"]`), so both are needed. - For a missing-column problem: which vertical, and whether the - page has a JSON-LD ItemList at all - ("Current bid", "Aktuelles Gebot", ...) and the locale. + page has a JSON-LD ItemList at all, and the locale (`/ar/` or + none). render: text validations: required: true diff --git a/.gitignore b/.gitignore index 3474a3a..4011e13 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,8 @@ # real captures, verified to parse identically to them and scrubbed; # it has to be IN the repo or the suite cannot run on a clean clone. !fixtures_generated.json -dubizzle_listings.* -dubizzle_listings_*.* +dubizzle_products.* +dubizzle_products_*.* # --- the live-test harness's working directory --------------------------- # `--dump-html PATH` writes PATH.page1, PATH.page2, ... — FILES, not a diff --git a/CHANGELOG.md b/CHANGELOG.md index c9494fb..f4bb145 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,44 @@ user would notice, the release notes say so first. classifier. It now reads `http_code`, falling back to `status` only if that is an integer. After the fix, one live call (`--wait-text AED` on the canary's used-cars listing, no `--cdp-url`) answered HTTP 200 (no 422) with an integer upstream status, 200; the body was a 1,156-byte Incapsula interstitial, correctly reported as blocked (exit 3), as the module docstring already says for the API's own exit. +- **Prose and strings inherited from the sites this repo was copied from.** + Several comments, docstrings and user-visible strings still described + another site's behaviour as if it were dubizzle's. Rewritten to what this + repo's own README measures, or deleted where it has no equivalent: + - `--help` text: `scraper_api_client.py` called itself an "auction + scraper" and described a scrolling grid and a `/p//…` URL shape; + `diff_runs.py --price-tolerance-pct` said the site quotes IDR; the + Playwright engine's `--pages` help described an infinitely scrolling + search page. + - The Playwright engine's `--concurrency` warning said dubizzle refuses a + headless browser by answering nothing at all. It refuses exits outside + the UAE. + - The engines, `captcha_solver.py` and `output_writer.py` described an + Akamai refusal of HEADLESS browsers (a 394-byte "Access Denied"), a + `--mode product` / `--mode shop`, a search that cannot be paginated and + a `/p/` discovery hub. None of those exist here: the refusal is + Imperva's, triggered by the exit's country, and the only mode is + `listing`. A contradictory "HEADFUL by default" comment is removed from + all three engines and the suite (the default is, and was, headless). + - `diff_runs.py` said the site quotes euro on 18 locales and described + auctions and bids. It quotes AED in two languages; what moves without a + price change is the Car of the Week slot. + - An unused `shop_facts` field is removed from all three engines. +- `CONTRIBUTING.md`'s "Reporting a site change" described another site's + selectors and page kinds; rewritten for the SSR payload, the JSON-LD join + and the DOM fallback this repo actually reads. Its note that a datacentre + run gets no response at all is corrected to the measured Imperva refusal. +- The site-change issue template carried another site's options (reserve + prices, favourite counts, suggested lots, a 100-page cap) and said a + headless run is refused. The bug-report template's example used another + repo's output prefix and product count. +- `.gitignore` / `.dockerignore` ignored `dubizzle_listings.*`, which no + engine writes; the default `--out` prefix is `dubizzle_products`. +- The suite's env-placeholder and proxy-line checks used another repo's + variable names; they now use `DUBIZZLE_CDP_ENDPOINT` and `DUBIZZLE_PROXY`. +- README: the last badge read "runs without an account", which is not true + here — a plain run needs a UAE exit. It now says so. + ## [0.1.1] — 2026-09-16 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f0fa6c..7ae8ec8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,38 +42,25 @@ not the check caught it. dubizzle changing its markup is the normal way this stops working, and it has its own issue template. The detail that saves the most time is WHICH -anchor broke, because on this site there is no structured data on a listing -page to fall back on — measured zero `application/ld+json`, zero -`__NEXT_DATA__` and zero Apollo state across six captures — so the DOM is -not the primary path by preference, it is the only one. - -1. **The grid container.** `[data-testid="divSRPContentProducts"]` on a - search page, `[data-ssr="productsCategoryL2/L3SSR"]` on a category - listing. If one of these moves the run reports 0 rows and exit 4, which - is loud. -2. **The tile marker.** `[data-testid="imgLeg-c"]` on a search page (one per - tile), `[data-testid="divProductWrapper"]` inside - `a[data-testid="lnkProductContainer"]` on a category listing. -3. **The reading ORDER inside the tile** — badge, title, price, was-price, - rating, sold, shop, location. The field reads rest on it, deliberately, - because the classes around each field are build hashes: - `` is the title today. If dubizzle - reorders a tile, `title` and the prices are what break. -4. **`span.flip`**, the shop name and the shop's city in that order, exactly - two per search tile. - -The one place structured data does exist is a DETAIL page's -`window.__cache` Apollo blob, which is where `--mode product` reads the real -product id, the exact sold count and the review count. +source broke, because a row here is read from more than one: + +1. **The SSR payload** — `