Randomise browser fingerprint per context to evade bot detection

Add UA rotation (8 real Chrome UAs), random viewport, en-GB locale,
Europe/London timezone, stealth browser args to suppress automation
signals, and a per-context init script masking navigator.webdriver,
plugins, and chrome runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-10 01:08:49 +00:00
parent 83b46bee8f
commit 52455328e7

View file

@ -20,6 +20,7 @@ Rate plan variants per room type (typical):
import asyncio import asyncio
import logging import logging
import random
import re import re
import uuid import uuid
from datetime import date from datetime import date
@ -34,6 +35,34 @@ from services import proxy as proxy_util
logger = logging.getLogger(__name__) 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. # JS that extracts all rate plan rows from the room availability table.
# Runs inside the page after the room table has loaded. # Runs inside the page after the room table has loaded.
# #
@ -179,7 +208,13 @@ class PlaywrightHotelPageBackend(ScraperBackend):
if not self._browser: if not self._browser:
self._browser = await self._pw.chromium.launch( self._browser = await self._pw.chromium.launch(
headless=True, 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: async def _get_context(self) -> BrowserContext:
@ -191,16 +226,21 @@ class PlaywrightHotelPageBackend(ScraperBackend):
except Exception: except Exception:
pass pass
session_id = uuid.uuid4().hex[:12] if self._proxy_enabled() else None 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( self._context = await self._browser.new_context(
user_agent=( user_agent=ua,
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' viewport=viewport,
'AppleWebKit/537.36 (KHTML, like Gecko) ' locale='en-GB',
'Chrome/125.0.0.0 Safari/537.36' timezone_id='Europe/London',
),
**self._proxy_kwargs(session_id), **self._proxy_kwargs(session_id),
) )
await self._context.add_init_script(_STEALTH_SCRIPT)
self._requests_on_context = 0 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 return self._context
async def _rotate_context(self): async def _rotate_context(self):