Translaterpeed / app /scraper /human_simulator.py
Ruhivig65's picture
Upload 5 files
14345e3 verified
"""
============================================
Human Behavior Simulator
- Random delays between actions
- Mouse movement patterns
- Random scrolling
- Realistic typing speed
- Makes the bot look like a real human reading
============================================
"""
import asyncio
import random
import logging
import math
from typing import Optional
from playwright.async_api import Page
from app.config import settings
logger = logging.getLogger(__name__)
class HumanSimulator:
"""
Simulates human behavior on web pages.
Why this matters:
- Websites track mouse movement patterns
- Constant timing = bot detection
- No scrolling = suspicious
- Instant navigation = not human
"""
def __init__(self, page: Page):
self.page = page
# ============================================
# Random Delays
# ============================================
async def random_delay(
self,
min_seconds: Optional[float] = None,
max_seconds: Optional[float] = None,
reason: str = "general",
):
"""
Wait for a random amount of time.
Like a human naturally pauses between actions.
"""
min_s = min_seconds or settings.MIN_DELAY_SECONDS
max_s = max_seconds or settings.MAX_DELAY_SECONDS
# Use a slightly weighted random (humans tend toward shorter waits)
delay = random.uniform(min_s, max_s)
# Occasionally add a longer "thinking" pause (10% chance)
if random.random() < 0.10:
delay += random.uniform(2.0, 5.0)
logger.debug(f" 💭 Extended thinking pause ({reason})")
logger.debug(f" ⏳ Waiting {delay:.1f}s ({reason})")
await asyncio.sleep(delay)
async def short_delay(self, reason: str = "short action"):
"""Quick delay: 0.5 to 2 seconds."""
await self.random_delay(0.5, 2.0, reason)
async def medium_delay(self, reason: str = "medium action"):
"""Medium delay: 2 to 5 seconds."""
await self.random_delay(2.0, 5.0, reason)
async def long_delay(self, reason: str = "long action"):
"""Long delay: 5 to 10 seconds."""
await self.random_delay(5.0, 10.0, reason)
async def reading_delay(self, text_length: int):
"""
Simulate reading time based on text length.
Average human reads ~250 words per minute.
We'll go faster since we're "speed reading."
"""
word_count = text_length // 5 # Rough estimate: 5 chars per word
# Speed reading: ~500-800 WPM
reading_speed = random.uniform(500, 800)
read_time = (word_count / reading_speed) * 60 # seconds
# Cap the reading time (don't wait too long)
read_time = min(read_time, 8.0)
read_time = max(read_time, 1.0)
logger.debug(f" 📖 Reading pause: {read_time:.1f}s for ~{word_count} words")
await asyncio.sleep(read_time)
# ============================================
# Mouse Movements
# ============================================
async def move_mouse_naturally(self, target_x: int, target_y: int):
"""
Move mouse to target coordinates with natural curve.
Humans don't move mouse in straight lines.
Uses bezier-like curve with random intermediate points.
"""
try:
# Get current viewport size
viewport = self.page.viewport_size
if not viewport:
viewport = {"width": 1920, "height": 1080}
# Start from a random position (simulating current mouse position)
start_x = random.randint(100, viewport["width"] - 100)
start_y = random.randint(100, viewport["height"] - 100)
# Generate intermediate points for a natural curve
steps = random.randint(10, 25)
for i in range(steps):
progress = (i + 1) / steps
# Add slight curve/wobble
wobble_x = random.randint(-3, 3)
wobble_y = random.randint(-2, 2)
# Ease-in-out interpolation
eased = self._ease_in_out(progress)
current_x = int(start_x + (target_x - start_x) * eased + wobble_x)
current_y = int(start_y + (target_y - start_y) * eased + wobble_y)
# Clamp to viewport
current_x = max(0, min(current_x, viewport["width"]))
current_y = max(0, min(current_y, viewport["height"]))
await self.page.mouse.move(current_x, current_y)
# Tiny delay between mouse moves
await asyncio.sleep(random.uniform(0.01, 0.04))
logger.debug(f" 🖱️ Mouse moved to ({target_x}, {target_y})")
except Exception as e:
logger.debug(f"Mouse movement skipped: {e}")
@staticmethod
def _ease_in_out(t: float) -> float:
"""Ease-in-out function for natural movement curves."""
if t < 0.5:
return 2 * t * t
else:
return 1 - (-2 * t + 2) ** 2 / 2
async def random_mouse_movement(self):
"""
Make a random mouse movement on the page.
Like a human idly moving their mouse while reading.
"""
try:
viewport = self.page.viewport_size
if not viewport:
return
x = random.randint(100, viewport["width"] - 100)
y = random.randint(100, viewport["height"] - 100)
await self.move_mouse_naturally(x, y)
except Exception:
pass # Mouse movements are optional, don't crash
# ============================================
# Scrolling
# ============================================
async def scroll_down_naturally(self, scroll_amount: Optional[int] = None):
"""
Scroll down the page naturally.
Humans scroll in chunks, not smoothly.
"""
try:
if scroll_amount is None:
scroll_amount = random.randint(200, 600)
# Scroll in small chunks
remaining = scroll_amount
while remaining > 0:
chunk = min(remaining, random.randint(50, 150))
await self.page.mouse.wheel(0, chunk)
remaining -= chunk
# Small pause between scroll chunks
await asyncio.sleep(random.uniform(0.05, 0.15))
logger.debug(f" 📜 Scrolled down {scroll_amount}px")
except Exception as e:
logger.debug(f"Scroll failed: {e}")
async def scroll_to_bottom_gradually(self):
"""
Scroll to bottom of page gradually, like a reader.
"""
try:
# Get page height
page_height = await self.page.evaluate("document.body.scrollHeight")
viewport_height = await self.page.evaluate("window.innerHeight")
current_scroll = await self.page.evaluate("window.scrollY")
remaining = page_height - current_scroll - viewport_height
while remaining > 50:
scroll_chunk = random.randint(200, 500)
await self.scroll_down_naturally(min(scroll_chunk, remaining))
# Pause between scrolls (reading)
await asyncio.sleep(random.uniform(0.5, 2.0))
# Occasionally do a random mouse movement while scrolling
if random.random() < 0.3:
await self.random_mouse_movement()
current_scroll = await self.page.evaluate("window.scrollY")
remaining = page_height - current_scroll - viewport_height
logger.debug(" 📜 Scrolled to bottom of page")
except Exception as e:
logger.debug(f"Scroll to bottom failed: {e}")
async def scroll_up_slightly(self):
"""
Occasionally scroll up a little, like re-reading a paragraph.
"""
try:
scroll_up = random.randint(-200, -50)
await self.page.mouse.wheel(0, scroll_up)
logger.debug(f" 📜 Scrolled up slightly ({scroll_up}px)")
except Exception:
pass
# ============================================
# Typing
# ============================================
async def type_like_human(self, selector: str, text: str):
"""
Type text character by character with human-like speed.
Includes occasional typos and corrections (optional).
"""
try:
# Click on the input field first
await self.page.click(selector)
await asyncio.sleep(random.uniform(0.3, 0.8))
for char in text:
await self.page.keyboard.type(char)
# Random delay between keystrokes
# Humans type at 40-80 WPM
delay = random.uniform(0.05, 0.20)
# Occasionally pause longer (thinking)
if random.random() < 0.05:
delay += random.uniform(0.3, 0.8)
await asyncio.sleep(delay)
logger.debug(f" ⌨️ Typed {len(text)} characters into '{selector}'")
except Exception as e:
logger.warning(f"Human typing failed, using fast fill: {e}")
await self.page.fill(selector, text)
# ============================================
# Combined Human Actions
# ============================================
async def simulate_page_arrival(self):
"""
Simulate what a human does when arriving at a new page:
1. Wait for page to visually load
2. Small mouse movement
3. Brief pause (looking at the page)
"""
await self.short_delay("page arrival - initial look")
if random.random() < 0.5:
await self.random_mouse_movement()
await self.short_delay("page arrival - orientation")
async def simulate_reading_chapter(self):
"""
Simulate reading a chapter:
1. Scroll down gradually
2. Occasional mouse movements
3. Occasional scroll up (re-reading)
4. Reading delay at the end
"""
# Scroll through the content
await self.scroll_to_bottom_gradually()
# Maybe scroll up to re-read something
if random.random() < 0.2:
await self.scroll_up_slightly()
await self.short_delay("re-reading")
# Final reading pause
await self.medium_delay("finishing chapter")
async def simulate_before_click(self, x: int, y: int):
"""
What a human does before clicking a button:
1. Look at the button (pause)
2. Move mouse toward it
3. Small hesitation
4. Click
"""
await self.short_delay("looking at button")
await self.move_mouse_naturally(x, y)
await asyncio.sleep(random.uniform(0.2, 0.5)) # Hesitation