gendesign/data/sql/50_scrape_kn_flats.py
lekss361 79070bfe99 add interactive parser of наш.дом.рф ЖК+flats with stealth Playwright
- Discovered real endpoints via chrome-devtools: /сервисы/api/kn/object
  (offset/limit/place/objStatus) + /portal-kn/api/sales/portal/table.
  Server-side filter ignored — local filter by developer.companyGroup.
- Pure Playwright in-context fetch (httpx blocked by ServicePipe TLS-fp).
- Saved fingerprint state in data/playwright_state.json for server reuse.
- Celery beat (configurable cron + jitter) + admin trigger endpoint
  + /admin/scrape UI page.
- Schema migration: UNIQUE(id, snapshot_date) for versioned snapshots,
  kn_scrape_runs journal.
- Smoke-tested on PRINZIP: 28 ЖК / 826 квартир as expected.
2026-04-27 17:58:40 +03:00

94 lines
3 KiB
Python

"""CLI for ad-hoc naш.дом.рф kn-API sweeps.
Usage:
PYTHONIOENCODING=utf-8 python data/sql/50_scrape_kn_flats.py --regions 66
python data/sql/50_scrape_kn_flats.py --regions 66 --developers 6208_0 --headed
"""
from __future__ import annotations
import argparse
import asyncio
import logging
import os
import sys
from pathlib import Path
# allow `from app.services.scrapers.domrf_kn import ...` when run from repo root
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "backend"))
from app.services.scrapers.domrf_kn import run_region_sweep # noqa: E402
def main() -> None:
parser = argparse.ArgumentParser(description="naш.дом.рф kn-API scraper (CLI)")
parser.add_argument(
"--regions",
type=int,
nargs="+",
required=True,
help="Region codes (1..89). e.g. --regions 66 77",
)
parser.add_argument(
"--developers",
type=str,
nargs="*",
help="Optional developer IDs in '<id>_0' format. e.g. --developers 6208_0",
)
parser.add_argument("--headed", action="store_true", help="Show Chromium window during bootstrap")
parser.add_argument(
"--save-state",
type=str,
default=None,
help="Path to dump Playwright storage_state.json after bootstrap (commit to git for server reuse)",
)
parser.add_argument(
"--load-state",
type=str,
default=None,
help="Path to load Playwright storage_state.json (server uses fingerprint captured locally)",
)
parser.add_argument(
"--bootstrap-only",
action="store_true",
help="Just bootstrap a session and exit — useful for capturing storage_state without scraping",
)
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
if not os.environ.get("DATABASE_URL"):
print("WARNING: DATABASE_URL not set; using default from app.core.config")
async def _run() -> None:
if args.bootstrap_only:
from app.services.scrapers.stealth import BrowserSession
async with BrowserSession(
region_code=args.regions[0],
headed=args.headed,
load_state=args.load_state,
save_state=args.save_state,
):
print(f"bootstrap-only: state saved to {args.save_state}")
return
for region in args.regions:
summary = await run_region_sweep(
region_code=region,
developers=args.developers or None,
headed=args.headed,
load_state=args.load_state,
save_state=args.save_state if region == args.regions[0] else None,
)
print(f"region={region} -> {summary}")
asyncio.run(_run())
if __name__ == "__main__":
main()