diff --git a/backend/services/scraper_backends/playwright_hotel_page.py b/backend/services/scraper_backends/playwright_hotel_page.py index c6b24b2..ec9d760 100644 --- a/backend/services/scraper_backends/playwright_hotel_page.py +++ b/backend/services/scraper_backends/playwright_hotel_page.py @@ -20,6 +20,7 @@ Rate plan variants per room type (typical): import asyncio import logging +import random import re import uuid from datetime import date @@ -34,6 +35,34 @@ from services import proxy as proxy_util logger = logging.getLogger(__name__) +_USER_AGENTS = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0', +] + +_VIEWPORTS = [ + {'width': 1920, 'height': 1080}, + {'width': 1366, 'height': 768}, + {'width': 1440, 'height': 900}, + {'width': 1536, 'height': 864}, + {'width': 1280, 'height': 800}, + {'width': 1600, 'height': 900}, +] + +# Injected into every new context to mask headless/webdriver signals. +_STEALTH_SCRIPT = """ +Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); +Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]}); +Object.defineProperty(navigator, 'languages', {get: () => ['en-GB', 'en']}); +window.chrome = {runtime: {}}; +""" + # JS that extracts all rate plan rows from the room availability table. # Runs inside the page after the room table has loaded. # @@ -179,7 +208,13 @@ class PlaywrightHotelPageBackend(ScraperBackend): if not self._browser: self._browser = await self._pw.chromium.launch( headless=True, - args=['--no-sandbox', '--disable-dev-shm-usage'], + args=[ + '--no-sandbox', + '--disable-dev-shm-usage', + '--disable-blink-features=AutomationControlled', + '--disable-infobars', + '--disable-extensions', + ], ) async def _get_context(self) -> BrowserContext: @@ -191,16 +226,21 @@ class PlaywrightHotelPageBackend(ScraperBackend): except Exception: pass session_id = uuid.uuid4().hex[:12] if self._proxy_enabled() else None + ua = random.choice(_USER_AGENTS) + viewport = random.choice(_VIEWPORTS) self._context = await self._browser.new_context( - user_agent=( - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' - 'AppleWebKit/537.36 (KHTML, like Gecko) ' - 'Chrome/125.0.0.0 Safari/537.36' - ), + user_agent=ua, + viewport=viewport, + locale='en-GB', + timezone_id='Europe/London', **self._proxy_kwargs(session_id), ) + await self._context.add_init_script(_STEALTH_SCRIPT) self._requests_on_context = 0 - logger.debug("Opened new browser context" + (f" (proxy session {session_id})" if session_id else "")) + logger.debug( + f"Opened new browser context ua=…{ua[-30:]} viewport={viewport['width']}x{viewport['height']}" + + (f" proxy_session={session_id}" if session_id else "") + ) return self._context async def _rotate_context(self):