Scrape 2GIS (2gis.ru, .kz, .ae) local business search by city and query. Get name, category, address, coordinates, rating, review count, opening hours, plus optional phone, website and e-mail for every place.
First implementation: 2GIS search over the server-rendered result pages
(/<city>/search/<query>[/page/<n>]), reading the embedded
var initialState = JSON.parse('…') blob (data.entity.profile).
The page only renders for current browser fingerprints; chrome131 / firefox133
get a ~12 KB "update your browser" redirect to /museum. The client detects it,
logs a wrong-profile warning, rotates to a current profile and never reports it
as zero results.
2GIS serves pages 1-5 only; page 6+ silently returns page 1 again. The loop
checks the server-reported currentPage and stops with pager_cap, so no
duplicate rows are emitted. Depth comes from more cities and queries.
includeContacts (off by default) opens each place page for phones, website
and e-mail — the search blob carries no contacts.
src/models.py (ActorInput, ResultRow), src/client.py (curl-cffi,
profile rotation, retry/backoff), src/parser.py (JS-string unescape, state
and row parsing), src/scraper.py (city x query x page loop, fault isolation,
run-wide dedupe), src/main.py (SDK wiring, PPE).
Fixtures are trimmed real captures (scripts/capture_fixtures.py).