gendesign/data/sql/40_scrape_history.py
2026-04-27 13:05:36 +03:00

151 lines
5.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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/<section>/<YYYY-MM>/<endpoint>.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}&regionCode={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())