Files
links/pricemon/scraper.py
T
2026-05-11 21:28:59 +10:00

81 lines
2.7 KiB
Python

import logging
import re
from decimal import Decimal, InvalidOperation
import requests
from bs4 import BeautifulSoup
logger = logging.getLogger(__name__)
_HEADERS = {
'User-Agent': (
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/124.0.0.0 Safari/537.36'
),
'Accept-Language': 'en-AU,en;q=0.9',
}
def parse_price(text: str) -> Decimal | None:
"""Extract the first numeric price from a string."""
if not text:
return None
cleaned = text.strip().replace(',', '')
match = re.search(r'\d+\.?\d*', cleaned)
if not match:
return None
try:
return Decimal(match.group())
except InvalidOperation:
return None
def _fetch_with_requests(url: str, css_selector: str) -> tuple[str, str | None]:
"""Returns (raw_text, error). raw_text is empty string on failure."""
try:
resp = requests.get(url, headers=_HEADERS, timeout=15)
resp.raise_for_status()
soup = BeautifulSoup(resp.content, 'html.parser')
el = soup.select_one(css_selector)
if el:
return el.get_text(strip=True), None
return '', f'Selector "{css_selector}" matched no element'
except Exception as exc:
return '', str(exc)
def _fetch_with_playwright(url: str, css_selector: str) -> tuple[str, str | None]:
"""Fallback for JS-rendered pages."""
try:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(extra_http_headers={'Accept-Language': 'en-AU,en;q=0.9'})
page.goto(url, wait_until='domcontentloaded', timeout=30000)
page.wait_for_timeout(2000)
el = page.query_selector(css_selector)
text = el.inner_text() if el else ''
browser.close()
if not text:
return '', f'Selector "{css_selector}" matched no element (playwright)'
return text.strip(), None
except Exception as exc:
return '', f'Playwright error: {exc}'
def fetch_price(watcher) -> tuple[Decimal | None, str, str | None]:
"""
Returns (price, raw_text, error).
Tries requests first; falls back to Playwright if the element isn't found.
"""
raw_text, error = _fetch_with_requests(watcher.url, watcher.css_selector)
if not raw_text and not error:
logger.info(f'Falling back to Playwright for watcher {watcher.pk}')
raw_text, error = _fetch_with_playwright(watcher.url, watcher.css_selector)
price = parse_price(raw_text) if raw_text else None
logger.info(f'Watcher {watcher.pk}: raw="{raw_text}" price={price} error={error}')
return price, raw_text, error