๐ Pydoll: Async Web Automation in Python!
Pydoll is revolutionizing browser automation! Unlike other solutions, it eliminates the need for webdrivers,
providing a smooth and reliable automation experience with native asynchronous performance.
Installation โข
Quick Start โข
Core Components โข
What's New โข
Advanced Features
โจ Key Features
๐น Zero Webdrivers! Say goodbye to webdriver compatibility nightmares
๐น Native Captcha Bypass! Smoothly handles Cloudflare Turnstile and reCAPTCHA v3*
๐น Async Performance for lightning-fast automation
๐น Human-like Interactions that mimic real user behavior
๐น Powerful Event System for reactive automations
๐น Multi-browser Support including Chrome and Edge
๐ฅ Installation
pip install pydoll-python
โก Quick Start
Get started with just a few lines of code:
import asyncio
from pydoll.browser.chrome import Chrome
from pydoll.constants import By
async def main():
async with Chrome() as browser:
await browser.start()
page = await browser.get_page()
await page.go_to('https://example-with-cloudflare.com')
button = await page.find_element(By.CSS_SELECTOR, 'button')
await button.click()
asyncio.run(main())
Need to configure your browser? Easy!
from pydoll.browser.chrome import Chrome
from pydoll.browser.options import Options
options = Options()
options.add_argument('--proxy-server=username:password@ip:port')
options.binary_location = '/path/to/your/browser'
async with Chrome(options=options) as browser:
await browser.start()
๐ What's New
Version 1.4.0 comes packed with amazing new features:
๐ก๏ธ Automatic Cloudflare Turnstile Captcha Handling
Seamlessly bypass Cloudflare Turnstile captchas with two powerful approaches:
Approach 1: Context Manager (Synchronous with main execution)
This approach waits for the captcha to be handled before continuing execution:
import asyncio
from pydoll.browser import Chrome
async def example_with_context_manager():
browser = Chrome()
await browser.start()
page = await browser.get_page()
print("Using context manager approach...")
async with page.expect_and_bypass_cloudflare_captcha(time_to_wait_captcha=5):
await page.go_to('https://2captcha.com/demo/cloudflare-turnstile')
print("Page loaded, waiting for captcha to be handled...")
print("Captcha handling completed, now we can continue...")
await asyncio.sleep(3)
await browser.stop()
if __name__ == '__main__':
asyncio.run(example_with_context_manager())
Approach 2: Enable/Disable (Background processing)
This approach handles captchas in the background without blocking execution:
import asyncio
from pydoll.browser import Chrome
async def example_with_enable_disable():
browser = Chrome()
await browser.start()
page = await browser.get_page()
print("Using enable/disable approach...")
await page.enable_auto_solve_cloudflare_captcha(time_to_wait_captcha=5)
await page.go_to('https://2captcha.com/demo/cloudflare-turnstile')
print("Page loaded, captcha will be handled in the background...")
await asyncio.sleep(5)
await page.disable_auto_solve_cloudflare_captcha()
print("Auto-solving disabled")
await browser.stop()
if __name__ == '__main__':
asyncio.run(example_with_enable_disable())
Which approach should you choose?
Not sure which method to use? Let's simplify:
Approach 1: Context Manager (Like a traffic light)
async with page.expect_and_bypass_cloudflare_captcha():
await page.go_to('https://protected-site.com')
How it works:
- The code pauses at the end of the context manager until the captcha is resolved
- Ensures any code after the
async with
block only executes after the captcha is handled
When to use it:
- When you need to be certain the captcha is solved before continuing
- In sequential operations where the next steps depend on successful login
- In situations where timing is crucial (like purchasing tickets, logging in, etc.)
- When you prefer simpler, straightforward code
Practical example: Logging into a protected site
async with page.expect_and_bypass_cloudflare_captcha():
await page.go_to('https://site-with-captcha.com/login')
await page.find_element(By.ID, 'username').type_keys('user123')
await page.find_element(By.ID, 'password').type_keys('password123')
await page.find_element(By.ID, 'login-button').click()
Approach 2: Enable/Disable (Like a background assistant)
callback_id = await page.enable_auto_solve_cloudflare_captcha()
How it works:
- It's like having an assistant working in the background while you do other things
- Doesn't block your main code - continues executing immediately
- The captcha will be handled automatically when it appears, without pausing your script
- You need to manage timing yourself (by adding delays if necessary)
When to use it:
- When you're navigating across multiple pages and want continuous protection
- In scripts that perform many independent tasks in parallel
- For "exploratory" navigation where exact timing isn't crucial
- In long-running automation scenarios where you want to leave the handler "turned on"
Practical example: Continuous navigation across multiple pages
await page.enable_auto_solve_cloudflare_captcha()
await page.go_to('https://site1.com')
await page.go_to('https://site2.com')
await page.go_to('https://site3.com')
await page.disable_auto_solve_cloudflare_captcha()
โ ๏ธ Tip for the Enable/Disable approach: Consider adding small delays before interacting with critical elements, to give time for the captcha to be solved in the background:
await page.enable_auto_solve_cloudflare_captcha()
await page.go_to('https://protected-site.com')
await asyncio.sleep(3)
await page.find_element(By.ID, 'important-button').click()
Understanding Cloudflare Bypass Parameters
Both captcha bypass methods accept several parameters that let you customize how the captcha detection and solving works:
custom_selector | Custom CSS selector to locate the captcha element | (By.CLASS_NAME, 'cf-turnstile') | When the captcha has a non-standard HTML structure or class name |
time_before_click | Time to wait (in seconds) before clicking the captcha element | 2 | When the captcha element isn't immediately interactive or requires time to load properly |
time_to_wait_captcha | Maximum time (in seconds) to wait for the captcha element to be found | 5 | When pages load slowly or when captcha widgets take longer to appear |
Example with custom parameters:
async with page.expect_and_bypass_cloudflare_captcha(
custom_selector=(By.ID, 'custom-captcha-id'),
time_before_click=3.5,
time_to_wait_captcha=10
):
await page.go_to('https://site-with-slow-captcha.com')
When to adjust time_before_click
:
- The captcha widget is complex and takes time to initialize
- You notice the click happens too early and fails to register
- The page is loading slowly due to network conditions
- JavaScript on the page needs time to fully initialize the widget
When to adjust time_to_wait_captcha
:
- The page takes longer to load completely
- The captcha appears after an initial loading delay
- You're experiencing timeout errors with the default value
- When working with slower connections or complex pages
๐ Connect to Existing Browser
Connect to an already running Chrome/Edge instance without launching a new browser process:
import asyncio
from pydoll.browser import Chrome
async def main():
browser = Chrome(connection_port=1234)
page = await browser.connect()
await page.go_to('https://google.com')
asyncio.run(main())
This is perfect for:
- Debugging scripts without restarting the browser
- Connecting to remote debugging sessions
- Working with custom browser configurations
- Integrating with existing browser instances
๐ค Advanced Keyboard Control
Full keyboard simulation thanks to @cleitonleonel:
import asyncio
from pydoll.browser.chrome import Chrome
from pydoll.browser.options import Options
from pydoll.common.keys import Keys
from pydoll.constants import By
async def main():
async with Chrome() as browser:
await browser.start()
page = await browser.get_page()
await page.go_to('https://example.com')
input_field = await page.find_element(By.CSS_SELECTOR, 'input')
await input_field.click()
await input_field.type_keys("hello@example.com", interval=0.2)
await input_field.key_down(Keys.SHIFT)
await input_field.send_keys("UPPERCASE")
await input_field.key_up(Keys.SHIFT)
await input_field.send_keys(Keys.ENTER)
await input_field.send_keys(Keys.PAGEDOWN)
asyncio.run(main())
๐ File Upload Support
@yie1d brings seamless file uploads:
file_input = await page.find_element(By.XPATH, '//input[@type="file"]')
await file_input.set_input_files('path/to/file.pdf')
await file_input.set_input_files(['file1.pdf', 'file2.jpg'])
async with page.expect_file_chooser(files='path/to/file.pdf'):
upload_button = await page.find_element(By.ID, 'upload-button')
await upload_button.click()
๐ Microsoft Edge Support
Now with Edge browser support thanks to @Harris-H:
import asyncio
from pydoll.browser import Edge
from pydoll.browser.options import EdgeOptions
async def main():
options = EdgeOptions()
async with Edge(options=options) as browser:
await browser.start()
page = await browser.get_page()
await page.go_to('https://example.com')
asyncio.run(main())
๐ฏ Core Components
Pydoll offers three main interfaces for browser automation:
Browser Interface
The Browser interface provides global control over the entire browser instance:
async def browser_demo():
async with Chrome() as browser:
await browser.start()
pages = [await browser.get_page() for _ in range(3)]
await browser.set_window_maximized()
await browser.set_cookies([{
'name': 'session',
'value': '12345',
'domain': 'example.com'
}])
Key Browser Methods
async start() | ๐ฅ Launch your browser and prepare for automation | await browser.start() |
async stop() | ๐ Close the browser gracefully when finished | await browser.stop() |
async get_page() | โจ Get an existing page or create a new one | page = await browser.get_page() |
async new_page(url='') | ๐ Create a new page in the browser | page_id = await browser.new_page() |
async get_page_by_id(page_id) | ๐ Find and control a specific page by ID | page = await browser.get_page_by_id(id) |
async get_targets() | ๐ฏ List all open pages in the browser | targets = await browser.get_targets() |
async set_window_bounds(bounds) | ๐ Size and position the browser window | await browser.set_window_bounds({'width': 1024}) |
async set_window_maximized() | ๐ช Maximize the browser window | await browser.set_window_maximized() |
async get_cookies() | ๐ช Get all browser cookies | cookies = await browser.get_cookies() |
async set_cookies(cookies) | ๐ง Set custom cookies for authentication | await browser.set_cookies([{...}]) |
async delete_all_cookies() | ๐งน Clear all cookies for a fresh state | await browser.delete_all_cookies() |
async set_download_path(path) | ๐ Configure where downloaded files are saved | await browser.set_download_path('/downloads') |
async connect() | ๐ Connect to an existing browser instance | page = await browser.connect() |
Tab Switching & Management
Want to switch between tabs or pages? It's super easy! First, get all your targets:
targets = await browser.get_targets()
You'll get something like this:
[
{
'targetId': 'F4729A95E0E4F9456BB6A853643518AF',
'type': 'page',
'title': 'New Tab',
'url': 'chrome://newtab/',
'attached': False,
'canAccessOpener': False,
'browserContextId': 'C76015D1F1C690B7BC295E1D81C8935F'
},
{
'targetId': '1C44D55BEEE43F44C52D69D8FC5C3685',
'type': 'iframe',
'title': 'chrome-untrusted://new-tab-page/one-google-bar?paramsencoded=',
'url': 'chrome-untrusted://new-tab-page/one-google-bar?paramsencoded=',
'attached': False,
'canAccessOpener': False,
'browserContextId': 'C76015D1F1C690B7BC295E1D81C8935F'
}
]
Then just pick the page you want:
target = next(target for target in targets if target['title'] == 'New Tab')
And switch to it:
new_tab_page = await browser.get_page_by_id(target['targetId'])
Now you can control this page as if it were the only one open! Switch between tabs effortlessly by keeping references to each page.
Page Interface
The Page interface lets you control individual browser tabs and interact with web content:
async def page_demo():
page = await browser.get_page()
await page.go_to('https://example.com')
await page.refresh()
url = await page.current_url
html = await page.page_source
await page.get_screenshot('screenshot.png')
await page.print_to_pdf('page.pdf')
title = await page.execute_script('return document.title')
Key Page Methods
async go_to(url, timeout=300) | ๐ Navigate to a URL with loading detection | await page.go_to('https://example.com') |
async refresh() | ๐ Reload the current page | await page.refresh() |
async close() | ๐ช Close the current tab | await page.close() |
async current_url | ๐งญ Get the current page URL | url = await page.current_url |
async page_source | ๐ Get the page's HTML content | html = await page.page_source |
async get_screenshot(path) | ๐ธ Save a screenshot of the page | await page.get_screenshot('shot.png') |
async print_to_pdf(path) | ๐ Convert the page to a PDF document | await page.print_to_pdf('page.pdf') |
async has_dialog() | ๐ Check if a dialog is present | if await page.has_dialog(): |
async accept_dialog() | ๐ Dismiss alert and confirmation dialogs | await page.accept_dialog() |
async execute_script(script, element) | โก Run JavaScript code on the page | await page.execute_script('alert("Hi!")') |
async get_network_logs(matches=[]) | ๐ธ๏ธ Monitor network requests | logs = await page.get_network_logs() |
async find_element(by, value) | ๐ Find an element on the page | el = await page.find_element(By.ID, 'btn') |
async find_elements(by, value) | ๐ Find multiple elements matching a selector | items = await page.find_elements(By.CSS, 'li') |
async wait_element(by, value, timeout=10) | โณ Wait for an element to appear | await page.wait_element(By.ID, 'loaded', 5) |
async expect_and_bypass_cloudflare_captcha(custom_selector=None, time_before_click=2, time_to_wait_captcha=5) | ๐ก๏ธ Context manager that waits for captcha to be solved | async with page.expect_and_bypass_cloudflare_captcha(): |
async enable_auto_solve_cloudflare_captcha(custom_selector=None, time_before_click=2, time_to_wait_captcha=5) | ๐ค Enable automatic Cloudflare captcha solving | await page.enable_auto_solve_cloudflare_captcha() |
async disable_auto_solve_cloudflare_captcha() | ๐ Disable automatic Cloudflare captcha solving | await page.disable_auto_solve_cloudflare_captcha() |
WebElement Interface
The WebElement interface provides methods to interact with DOM elements:
async def element_demo():
button = await page.find_element(By.CSS_SELECTOR, 'button.submit')
input_field = await page.find_element(By.ID, 'username')
button_text = await button.get_element_text()
is_button_enabled = button.is_enabled
input_value = input_field.value
await button.scroll_into_view()
await input_field.type_keys("user123")
await button.click()
Key WebElement Methods
value | ๐ฌ Get the value of an input element | value = input_field.value |
class_name | ๐จ Get the element's CSS classes | classes = element.class_name |
id | ๐ท๏ธ Get the element's ID attribute | id = element.id |
is_enabled | โ
Check if the element is enabled | if button.is_enabled: |
async bounds | ๐ Get the element's position and size | coords = await element.bounds |
async inner_html | ๐งฉ Get the element's inner HTML content | html = await element.inner_html |
async get_element_text() | ๐ Get the element's text content | text = await element.get_element_text() |
get_attribute(name) | ๐ Get any attribute from the element | href = link.get_attribute('href') |
async scroll_into_view() | ๐๏ธ Scroll the element into viewport | await element.scroll_into_view() |
async click(x_offset=0, y_offset=0) | ๐ Click the element with optional offsets | await button.click() |
async click_using_js() | ๐ฎ Click using JavaScript for hidden elements | await overlay_button.click_using_js() |
async send_keys(text) | โจ๏ธ Send text to input fields | await input.send_keys("text") |
async type_keys(text, interval=0.1) | ๐จโ๐ป Type text with realistic timing | await input.type_keys("hello", 0.2) |
async get_screenshot(path) | ๐ท Take a screenshot of the element | await error.get_screenshot('error.png') |
async set_input_files(files) | ๐ค Upload files with file inputs | await input.set_input_files('file.pdf') |
๐ Advanced Features
Event System
Pydoll's powerful event system lets you react to browser events in real-time:
from pydoll.events.page import PageEvents
from pydoll.events.network import NetworkEvents
from functools import partial
async def on_page_loaded(event):
print(f"๐ Page loaded: {event['params'].get('url')}")
await page.enable_page_events()
await page.on(PageEvents.PAGE_LOADED, on_page_loaded)
async def on_request(page, event):
url = event['params']['request']['url']
print(f"๐ Request to: {url}")
await page.enable_network_events()
await page.on(NetworkEvents.REQUEST_WILL_BE_SENT, partial(on_request, page))
from pydoll.events.dom import DomEvents
await page.enable_dom_events()
await page.on(DomEvents.DOCUMENT_UPDATED, lambda e: print("DOM updated!"))
Request Interception
Pydoll gives you the power to intercept and modify network requests before they're sent! This allows you to customize headers or modify request data on the fly.
Basic Request Modification
The request interception system lets you monitor and modify requests before they're sent:
from pydoll.events.fetch import FetchEvents
from pydoll.commands.fetch import FetchCommands
from functools import partial
async def request_interceptor(page, event):
request_id = event['params']['requestId']
url = event['params']['request']['url']
print(f"๐ Intercepted request to: {url}")
await page._execute_command(
FetchCommands.continue_request(
request_id=request_id
)
)
await page.enable_fetch_events()
await page.on(FetchEvents.REQUEST_PAUSED, partial(request_interceptor, page))
Inject authentication or tracking headers into specific requests:
async def auth_header_interceptor(page, event):
request_id = event['params']['requestId']
url = event['params']['request']['url']
if '/api/' in url:
original_headers = event['params']['request'].get('headers', {})
custom_headers = {
**original_headers,
'Authorization': 'Bearer your-token-123',
'X-Custom-Track': 'pydoll-automation'
}
await page._execute_command(
FetchCommands.continue_request(
request_id=request_id,
headers=custom_headers
)
)
else:
await page._execute_command(
FetchCommands.continue_request(
request_id=request_id
)
)
await page.enable_fetch_events()
await page.on(FetchEvents.REQUEST_PAUSED, partial(auth_header_interceptor, page))
Modifying Request Body
Change POST data before it's sent:
async def modify_request_body(page, event):
request_id = event['params']['requestId']
url = event['params']['request']['url']
method = event['params']['request'].get('method', '')
if method == 'POST' and 'submit-form' in url:
original_body = event['params']['request'].get('postData', '{}')
new_body = '{"modified": true, "data": "enhanced-by-pydoll"}'
print(f"โ๏ธ Modifying POST request to: {url}")
await page._execute_command(
FetchCommands.continue_request(
request_id=request_id,
post_data=new_body
)
)
else:
await page._execute_command(
FetchCommands.continue_request(
request_id=request_id
)
)
await page.enable_fetch_events()
await page.on(FetchEvents.REQUEST_PAUSED, partial(modify_request_body, page))
Filtering Request Types
You can focus on specific types of requests to intercept:
await page.enable_fetch_events(resource_type='xhr')
await page.enable_fetch_events(resource_type='document')
await page.enable_fetch_events(resource_type='image')
Available resource types include: document
, stylesheet
, image
, media
, font
, script
, texttrack
, xhr
, fetch
, eventsource
, websocket
, manifest
, other
.
Concurrent Automation
Process multiple pages simultaneously for maximum efficiency:
async def process_page(url):
page = await browser.get_page()
await page.go_to(url)
return await page.get_element_text()
urls = ['https://example1.com', 'https://example2.com', 'https://example3.com']
results = await asyncio.gather(*(process_page(url) for url in urls))
๐ก Best Practices
Maximize your Pydoll experience with these tips:
โ
Embrace async patterns throughout your code for best performance
โ
Use specific selectors (IDs, unique attributes) for reliable element finding
โ
Implement proper error handling with try/except blocks around critical operations
โ
Leverage the event system instead of polling for state changes
โ
Properly close resources with async context managers
โ
Wait for elements instead of fixed sleep delays
โ
Use realistic interactions like type_keys()
to avoid detection
๐ค Contributing
We'd love your help making Pydoll even better! Check out our contribution guidelines to get started. Whether it's fixing bugs, adding features, or improving documentation - all contributions are welcome!
Please make sure to:
- Write tests for new features or bug fixes
- Follow coding style and conventions
- Use conventional commits for pull requests
- Run lint and test checks before submitting
๐ฎ Coming Soon
Get ready for these upcoming features in Pydoll:
๐น Proxy Rotation - Seamless IP switching for extended scraping sessions
๐น Shadow DOM Access - Navigate and interact with Shadow Root elements
Stay tuned and star the repository to get updates when these features are released!
๐ Professional Support
Need specialized help with your automation projects? I offer professional services for those who need:
- ๐ง Custom Integration - Integrate Pydoll into your existing systems
- ๐ Performance Optimization - Make your automation scripts faster and more reliable
- ๐ก๏ธ Bypass Solutions - Help with complex captcha or anti-bot challenges
- ๐ Training & Consultation - Learn advanced techniques and best practices
- ๐ผ Enterprise Support - Priority assistance for business-critical applications
Contact:
๐ License
Pydoll is licensed under the MIT License.
Pydoll โ Making browser automation magical! โจ