Skip to content

Commit cefc78c

Browse files
authored
Merge pull request #295 from autoscrape-labs/feat/scroll
Add ScrollAPI class for realistic scrolling
2 parents 7328ae2 + 0345b03 commit cefc78c

File tree

9 files changed

+1180
-19
lines changed

9 files changed

+1180
-19
lines changed

docs/en/features/automation/human-interactions.md

Lines changed: 199 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ One of the key differentiators between successful automation and easily-detected
1111
- **Mouse movement simulation**: Realistic cursor paths with bezier curves
1212
- **Mouse delta events**: Natural acceleration and deceleration patterns
1313
- **Hover behavior**: Realistic delays and movement when hovering
14-
- **Scroll patterns**: Human-like scroll speeds and momentum with a dedicated `scroll()` method
1514
- **Timing variations**: Randomized delays to avoid predictable patterns
1615

1716
These features leverage CDP and JavaScript capabilities for maximum realism.
@@ -223,6 +222,202 @@ asyncio.run(fast_vs_realistic_input())
223222
!!! info "Advanced Keyboard Control"
224223
For comprehensive keyboard control documentation, including special keys, key combinations, modifiers, and complete key reference tables, see **[Keyboard Control](keyboard-control.md)**.
225224

225+
## Realistic Page Scrolling
226+
227+
Pydoll provides a dedicated scroll API that waits for scroll completion before proceeding, making your automations more realistic and reliable.
228+
229+
### Basic Directional Scrolling
230+
231+
Use the `scroll.by()` method to scroll the page in any direction with precise control:
232+
233+
```python
234+
import asyncio
235+
from pydoll.browser.chromium import Chrome
236+
from pydoll.constants import ScrollPosition
237+
238+
async def basic_scrolling():
239+
async with Chrome() as browser:
240+
tab = await browser.start()
241+
await tab.go_to('https://example.com/long-page')
242+
243+
# Scroll down 500 pixels (with smooth animation)
244+
await tab.scroll.by(ScrollPosition.DOWN, 500, smooth=True)
245+
246+
# Scroll up 300 pixels
247+
await tab.scroll.by(ScrollPosition.UP, 300, smooth=True)
248+
249+
# Scroll right (useful for horizontal scroll pages)
250+
await tab.scroll.by(ScrollPosition.RIGHT, 200, smooth=True)
251+
252+
# Scroll left
253+
await tab.scroll.by(ScrollPosition.LEFT, 200, smooth=True)
254+
255+
# Instant scroll (no animation)
256+
await tab.scroll.by(ScrollPosition.DOWN, 1000, smooth=False)
257+
258+
asyncio.run(basic_scrolling())
259+
```
260+
261+
### Scrolling to Specific Positions
262+
263+
Navigate quickly to the top or bottom of the page:
264+
265+
```python
266+
import asyncio
267+
from pydoll.browser.chromium import Chrome
268+
269+
async def scroll_to_positions():
270+
async with Chrome() as browser:
271+
tab = await browser.start()
272+
await tab.go_to('https://example.com/article')
273+
274+
# Read the beginning of the article
275+
await asyncio.sleep(2.0)
276+
277+
# Smoothly scroll to the bottom
278+
await tab.scroll.to_bottom(smooth=True)
279+
280+
# Pause at the bottom
281+
await asyncio.sleep(1.5)
282+
283+
# Return to the top
284+
await tab.scroll.to_top(smooth=True)
285+
286+
asyncio.run(scroll_to_positions())
287+
```
288+
289+
!!! tip "Smooth vs Instant"
290+
- **smooth=True**: Uses browser's smooth animation and waits for `scrollend` event
291+
- **smooth=False**: Instant scrolling for maximum speed when realism isn't critical
292+
293+
### Human-Like Scrolling Patterns
294+
295+
!!! info "Future Enhancement: Built-in Realistic Scrolling"
296+
Currently, you must manually implement random scrolling patterns. Future versions will include a `realistic=True` parameter that automatically adds natural variations in scroll distances, speeds, and pauses to mimic human reading behavior.
297+
298+
Simulate natural reading and navigation behavior:
299+
300+
```python
301+
import asyncio
302+
import random
303+
from pydoll.browser.chromium import Chrome
304+
from pydoll.constants import ScrollPosition
305+
306+
async def human_like_scrolling():
307+
"""Simulate natural scrolling patterns while reading an article."""
308+
async with Chrome() as browser:
309+
tab = await browser.start()
310+
await tab.go_to('https://example.com/article')
311+
312+
# User starts reading from top
313+
await asyncio.sleep(random.uniform(2.0, 4.0))
314+
315+
# Gradually scroll while reading
316+
for _ in range(random.randint(5, 8)):
317+
# Varied scroll distances (simulates reading speed)
318+
scroll_distance = random.randint(300, 600)
319+
await tab.scroll.by(
320+
ScrollPosition.DOWN,
321+
scroll_distance,
322+
smooth=True
323+
)
324+
325+
# Pause to "read" content
326+
await asyncio.sleep(random.uniform(2.0, 5.0))
327+
328+
# Quick scroll to check the end
329+
await tab.scroll.to_bottom(smooth=True)
330+
await asyncio.sleep(random.uniform(1.0, 2.0))
331+
332+
# Scroll back to top to re-read something
333+
await tab.scroll.to_top(smooth=True)
334+
335+
asyncio.run(human_like_scrolling())
336+
```
337+
338+
### Scrolling Elements into View
339+
340+
Use `scroll_into_view()` to ensure elements are visible before taking page screenshots:
341+
342+
```python
343+
import asyncio
344+
from pydoll.browser.chromium import Chrome
345+
346+
async def scroll_for_screenshots():
347+
"""Scroll elements into view before capturing page screenshots."""
348+
async with Chrome() as browser:
349+
tab = await browser.start()
350+
await tab.go_to('https://example.com/product')
351+
352+
# Scroll to pricing section before taking full page screenshot
353+
pricing_section = await tab.find(id="pricing")
354+
await pricing_section.scroll_into_view()
355+
await tab.take_screenshot(path="page_with_pricing.png")
356+
357+
# Scroll to reviews section before screenshot
358+
reviews = await tab.find(class_name="reviews")
359+
await reviews.scroll_into_view()
360+
await tab.take_screenshot(path="page_with_reviews.png")
361+
362+
# Scroll to footer to capture complete page state
363+
footer = await tab.find(tag_name="footer")
364+
await footer.scroll_into_view()
365+
await tab.take_screenshot(path="page_with_footer.png")
366+
367+
# Note: click() already scrolls automatically, so no need for:
368+
# await button.scroll_into_view() # Unnecessary!
369+
# await button.click() # This already scrolls the button into view
370+
371+
asyncio.run(scroll_for_screenshots())
372+
```
373+
374+
### Handling Infinite Scroll Content
375+
376+
Implement scrolling patterns to load lazy-loaded content:
377+
378+
```python
379+
import asyncio
380+
from pydoll.browser.chromium import Chrome
381+
from pydoll.constants import ScrollPosition
382+
383+
async def infinite_scroll_loading():
384+
"""Load content on infinite scroll pages."""
385+
async with Chrome() as browser:
386+
tab = await browser.start()
387+
await tab.go_to('https://example.com/feed')
388+
389+
items_loaded = 0
390+
max_scrolls = 10
391+
392+
for scroll_num in range(max_scrolls):
393+
# Scroll to bottom to trigger loading
394+
await tab.scroll.to_bottom(smooth=True)
395+
396+
# Wait for content to load
397+
await asyncio.sleep(random.uniform(2.0, 3.0))
398+
399+
# Check if new items were loaded
400+
items = await tab.find(class_name="feed-item", find_all=True)
401+
new_count = len(items)
402+
403+
if new_count == items_loaded:
404+
print("No more content to load")
405+
break
406+
407+
items_loaded = new_count
408+
print(f"Scroll {scroll_num + 1}: {items_loaded} items loaded")
409+
410+
# Small scroll up (human behavior)
411+
if random.random() > 0.7:
412+
await tab.scroll.by(ScrollPosition.UP, 200, smooth=True)
413+
await asyncio.sleep(random.uniform(0.5, 1.0))
414+
415+
asyncio.run(infinite_scroll_loading())
416+
```
417+
418+
!!! success "Automatic Completion Waiting"
419+
Unlike `execute_script("window.scrollBy(...)")` which returns immediately, the `scroll` API uses CDP's `awaitPromise` parameter to wait for the browser's `scrollend` event. This ensures your subsequent actions only execute after scrolling completely finishes.
420+
226421
## Combining Techniques for Maximum Realism
227422

228423
### Complete Form Filling Example
@@ -371,9 +566,7 @@ async def natural_user_simulation(tab):
371566
await asyncio.sleep(random.uniform(1.0, 3.0))
372567

373568
# User scrolls down to see more
374-
# Currently: Manual JavaScript scroll (instant, not realistic)
375-
# Future: Dedicated scroll() method with human-like momentum and acceleration
376-
await tab.execute_script("window.scrollBy(0, 300)")
569+
await tab.scroll.by(ScrollPosition.DOWN, 300, smooth=True)
377570
await asyncio.sleep(random.uniform(0.5, 1.5))
378571

379572
# User finds and clicks button
@@ -403,11 +596,10 @@ async def advanced_stealth_automation():
403596
await tab.go_to('https://example.com/sensitive-page')
404597
await asyncio.sleep(random.uniform(2.0, 4.0))
405598

406-
# Scroll realistically (current manual approach)
407-
# Future versions will have a dedicated scroll() method with momentum
599+
# Scroll realistically with the dedicated API
408600
for _ in range(random.randint(2, 4)):
409601
scroll_amount = random.randint(200, 500)
410-
await tab.execute_script(f"window.scrollBy(0, {scroll_amount})")
602+
await tab.scroll.by(ScrollPosition.DOWN, scroll_amount, smooth=True)
411603
await asyncio.sleep(random.uniform(0.8, 2.0))
412604

413605
# Find element with timeout (simulating user search)

0 commit comments

Comments
 (0)