"""Historical scrape: 12 months of 2025 + Jan/Feb 2026 for realization + sold_ready. The DOM.RF API accepts repYear/repMonth params and returns historical state — already verified by probe. Output: data/raw/domrf_history/
//.json Usage: PYTHONIOENCODING=utf-8 python data/sql/40_scrape_history.py """ import asyncio, json, os, re, sys from urllib.parse import urlsplit, parse_qs, urlencode, urlunsplit from playwright.async_api import async_playwright ROOT = os.path.join(os.path.dirname(__file__), '..', 'raw', 'domrf_history') BASE = 'https://xn--80az8a.xn--d1aqf.xn--p1ai' # (year, month) pairs to backfill. 2025 full year + 2026 Jan/Feb. # 2026-03 already has snapshot_date=2026-04-27 in domrf_realization. PERIODS = [(2025, m) for m in range(1, 13)] + [(2026, 1), (2026, 2)] REGIONS = list(range(1, 100)) # Rosstat-style 1..99 def safe_filename(url): parts = urlsplit(url) base = parts.path.split('/api/')[-1].strip('/') base = re.sub(r'[^A-Za-z0-9_\-]', '_', base)[:120] if parts.query: q = re.sub(r'[^A-Za-z0-9_\-]', '_', parts.query)[:80] base = f'{base}__{q}' return base + '.json' async def warm_up(page, url): """Goto a section page so Chromium passes WAF for that subdomain segment.""" await page.goto(BASE + url, wait_until='domcontentloaded', timeout=60_000) try: await page.wait_for_load_state('networkidle', timeout=30_000) except Exception: pass await page.wait_for_timeout(5000) async def fetch_save(page, url, out_dir, saved): full = url if url.startswith('http') else BASE + url try: resp = await page.evaluate( '''async (u) => { const r = await fetch(u, {credentials: 'include'}); const t = await r.text(); return {s: r.status, ct: r.headers.get('content-type')||'', t}; }''', full, ) except Exception as e: print(f' ERR {full}: {e}') return False if not resp or resp.get('s', 0) != 200: return False text = resp.get('t') or '' if len(text) < 20: return False fname = safe_filename(full) with open(os.path.join(out_dir, fname), 'w', encoding='utf-8') as f: f.write(text) saved.append((fname, len(text))) return True async def scrape_realization_period(page, year, month): out_dir = os.path.join(ROOT, 'realization', f'{year}-{month:02d}') os.makedirs(out_dir, exist_ok=True) saved = [] base = '/аналитика/api/rpp' # RF-level total (no regionCode) for ep in ('total', 'housing', 'readyYear'): await fetch_save(page, f'{base}/{ep}?repMonth={month}&repYear={year}', out_dir, saved) await fetch_save( page, f'{base}/developer?typeSquare=total&repMonth={month}&repYear={year}' f'&developerOrder=totalSquare:desc', out_dir, saved, ) # Per-region for rcode in REGIONS: for ep in ('total', 'housing', 'readyYear'): await fetch_save( page, f'{base}/{ep}?regionCode={rcode}&repMonth={month}&repYear={year}', out_dir, saved, ) for ts in ('total', 'living'): await fetch_save( page, f'{base}/developer?typeSquare={ts}®ionCode={rcode}' f'&repMonth={month}&repYear={year}&developerOrder=totalSquare:desc', out_dir, saved, ) print(f' realization {year}-{month:02d}: {len(saved)} files') return len(saved) async def scrape_sold_ready_period(page, year, month): out_dir = os.path.join(ROOT, 'sold_ready', f'{year}-{month:02d}') os.makedirs(out_dir, exist_ok=True) saved = [] base = '/portal-analytics/api/ready-construction' for ep_path in [ f'/index?repYear={year}&repMonth={month}', f'/charts?repYear={year}&repMonth={month}', f'/filters?repYear={year}&repMonth={month}', f'/ready-year-charts?repYear={year}&repMonth={month}', '/dynamics?dynamicChartType=readyPercChart,soldPercChart,soldReadyPercChart,squareSum', ]: await fetch_save(page, base + ep_path, out_dir, saved) print(f' sold_ready {year}-{month:02d}: {len(saved)} files') return len(saved) async def main(): os.makedirs(ROOT, exist_ok=True) async with async_playwright() as pw: browser = await pw.chromium.launch(headless=True) ctx = await browser.new_context( user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 'AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/147.0.0.0 Safari/537.36', locale='ru-RU', viewport={'width': 1920, 'height': 1080}, ) page = await ctx.new_page() # Warm up realization page first print('Warming up: realization page') await warm_up(page, '/аналитика/реализация_строящихся_квартир') # Realization for all periods (uses /аналитика/api/rpp/*) for year, month in PERIODS: await scrape_realization_period(page, year, month) # Switch to sold_ready warm-up print('Warming up: sold_ready page') await warm_up(page, '/аналитика/распроданность-стройготовность?repYear=2026&repMonth=3') for year, month in PERIODS: await scrape_sold_ready_period(page, year, month) await browser.close() if __name__ == '__main__': asyncio.run(main())