跳转至

浏览器域

浏览器域是 Pydoll 无WebDriver架构的核心。该组件通过 Chrome 开发者工具协议(CDP)直接与浏览器实例交互,无需依赖传统 WebDriver,从而在性能和可靠性方面实现显著提升。

graph LR
    A[Pydoll API] --> B[Browser Domain]
    B <--> C[Chrome DevTools Protocol]
    C <--> D[Browser Process]

    subgraph "Internal Components"
        B --> E[Connection Handler]
        B --> F[Process Manager]
        B --> G[Options Manager]
        B --> H[Proxy Manager]
        B --> I[Temp Directory Manager]
    end

技术架构

浏览器域的核心实现为一个抽象基类(Browser),该基类为所有浏览器实现定义了基础契约。具体浏览器类(如 Chrome 和 Edge)继承自该基类,在共享通用架构的同时,提供各自特有的浏览器行为。

# Abstract base class (simplified)
class Browser(ABC):
    def __init__(self, options_manager: BrowserOptionsManager, connection_port: Optional[int] = None):
        # Initialize components
        # ...

    @abstractmethod
    def _get_default_binary_location(self) -> str:
        """Must be implemented by subclasses"""
        pass

    async def start(self, headless: bool = False) -> Tab:
        # Start browser process
        # Establish CDP connection
        # Return initial tab for interaction
        # ...

# Implementation for Chrome
class Chrome(Browser):
    def _get_default_binary_location(self) -> str:
        # Return path to Chrome binary
        # ...

这种抽象设计使 Pydoll 能够通过统一接口支持多种浏览器,而各具体实现则负责处理浏览器特有的细节,例如可执行文件定位、命令行参数配置以及协议差异适配。

核心使用模式

浏览器域遵循一套统一的初始化、标签页管理及资源清理模式。需注意,start() 方法现已直接返回一个 Tab 实例:

import asyncio
from pydoll.browser.chromium import Chrome

async def simple_browser_example():
    # Create and start a browser instance
    browser = Chrome()
    tab = await browser.start()  # Returns Tab directly

    try:
        # Navigate and interact with the tab
        await tab.go_to("https://example.com")

        # Perform operations with the tab
        title = await tab.execute_script("return document.title")
        print(f"Page title: {title}")

    finally:
        # Always ensure the browser is properly closed
        await browser.stop()

# Run the async example
asyncio.run(simple_browser_example())

Context Manager Usage

For cleaner resource management, use the context manager pattern:

async def context_manager_example():
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to("https://example.com")
        # The browser is automatically closed when exiting the context

asyncio.run(context_manager_example())

浏览器实现层级结构

浏览器域采用清晰的继承体系,在确保代码复用的同时支持针对不同浏览器的定制化实现。

classDiagram
    class Browser {
        <<Abstract>>
        +__init__(options_manager, connection_port)
        +start(headless) Tab
        +stop()
        +new_tab(url, browser_context_id) Tab
        +create_browser_context() str
        #_get_default_binary_location()*
    }

    class Chrome {
        +_get_default_binary_location()
    }

    class Edge {
        +_get_default_binary_location()
    }

    Browser <|-- Chrome : extends
    Browser <|-- Edge : extends

该架构使Pydoll能够通过统一接口支持多种浏览器类型。每个具体实现(如Chrome、Edge)只需提供浏览器特定的细节(如可执行文件定位),同时从基础Browser类继承强大的核心功能。

初始化参数

Browser域在初始化时接受两个主要参数,分别控制浏览器的不同行为特性:

选项管理器参数

options_manager参数接收一个BrowserOptionsManager实例,用于处理浏览器选项的初始化和配置:

from pydoll.browser.chromium import Chrome
from pydoll.browser.interfaces import BrowserOptionsManager

# The options manager is typically handled internally by browser implementations
browser = Chrome()  # Uses default ChromiumOptionsManager internally

选项管理器负责: - 以合理的默认值初始化浏览器选项 - 添加所需的CDP参数 - 管理浏览器特定配置

内部实现说明

大多数用户无需直接与选项管理器交互,因为像Chrome这样的浏览器实现在内部会提供其专用的管理器。不过高级用户可以为特殊配置创建自定义选项管理器。

连接端口参数

connection_port参数定义用于CDP WebSocket连接的端口号:

# Specify exact port for connection
browser = Chrome(connection_port=9222)

该参数具有两种用途:

  1. 浏览器启动场景:指定浏览器应开启哪个端口用于CDP通信
  2. 连接已有浏览器场景:定义使用外部浏览器实例时需要连接的端口号

端口可用性提示

若未指定端口,Pydoll会在9223至9322范围内随机选择一个可用端口。若您的环境存在防火墙或网络限制,可能需要显式设置一个可访问的端口。

内部组件

Browser域通过协调多个专业组件来实现其功能:

连接处理器

ConnectionHandler通过Chrome DevTools协议建立并维持与浏览器的通信。它在WebSocket连接之上提供了抽象层,负责处理命令执行、响应解析和事件订阅。

该组件是Pydoll架构的核心部分,我们将在专门的"连接域"章节进行详细阐述。

浏览器进程管理器

BrowserProcessManager负责管理浏览器进程的生命周期:

class BrowserProcessManager:
    def start_browser_process(self, binary, port, arguments):
        # Launch browser executable with proper arguments
        # Monitor process startup
        # ...

    def stop_process(self):
        # Terminate browser process
        # Cleanup resources
        # ...

这种关注点分离的设计确保了浏览器进程管理与协议通信的解耦,使代码更易于维护和测试。

临时目录管理器

TempDirectoryManager负责为浏览器用户数据创建临时目录并进行清理管理:

class TempDirectoryManager:
    def create_temp_dir(self):
        # Create temporary directory for browser user data
        # Return directory handle
        # ...

    def cleanup(self):
        # Remove temporary directories
        # Clean up resources
        # ...

该组件能妥善管理浏览器临时数据并进行自动清理,有效防止长时间自动化测试过程中出现磁盘空间问题。

代理管理器

ProxyManager负责配置浏览器的代理设置:

class ProxyManager:
    def __init__(self, options):
        # Parse proxy settings from options
        # ...

    def get_proxy_credentials(self):
        # Extract authentication details
        # Format proxy configuration
        # ...

该组件对于需要代理轮换或身份验证的自动化网页抓取/测试场景至关重要。

生命周期与上下文管理

Browser域实现了Python的异步上下文管理协议(__aenter____aexit__),可自动完成资源清理工作:

async def scrape_data():
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to('https://example.com')
        # Work with tab...
        # Browser automatically closes when exiting the context

这种设计模式能确保即使自动化过程中出现异常,浏览器进程也能被正确终止,从而避免资源泄漏。

启动浏览器并获取初始标签页

browser = Chrome()
tab = await browser.start()  # Returns Tab instance
await tab.go_to("https://example.com")

创建额外的标签页

# Create additional tabs
tab2 = await browser.new_tab("https://github.com")
tab3 = await browser.new_tab()  # Empty tab

# Work with multiple tabs
await tab.go_to("https://example.com")
await tab2.go_to("https://github.com")

Multi-Tab Automation

You can work with multiple tabs simultaneously:

async def multi_tab_example():
    browser = Chrome()
    tab1 = await browser.start()

    # Create and work with multiple tabs
    await tab1.go_to("https://example.com")

    tab2 = await browser.new_tab("https://github.com")

    # Get information from both tabs
    title1 = await tab1.execute_script("return document.title")
    title2 = await tab2.execute_script("return document.title")

    print(f"Tab 1: {title1}")
    print(f"Tab 2: {title2}")

    await browser.stop()

浏览器上下文管理

理解浏览器上下文

浏览器上下文是Pydoll最强大的功能之一,用于创建隔离的浏览环境。可以将浏览器上下文视为同一浏览器进程内完全独立的会话——类似于无痕浏览窗口,但具备编程控制能力。

每个浏览器上下文都维护自己独立的: - Cookies和会话存储:与其他上下文完全隔离 - 本地存储和IndexedDB:每个上下文拥有独立的数据存储 - 缓存系统:各上下文拥有独立的缓存机制 - 权限设置:支持上下文级别的权限授予 - 网络配置:包括独立的代理设置 - 认证状态:登录会话具有上下文特异性

graph TB
    A[Browser Process] --> B[Default Context]
    A --> C[Context 1]
    A --> D[Context 2]

    B --> B1[Tab 1]
    B --> B2[Tab 2]

    C --> C1[Tab 3]

    D --> D1[Tab 4]

为什么要使用浏览器上下文?

浏览器上下文对以下自动化场景至关重要:

  1. 多账号测试:同时测试不同用户账号且互不干扰
  2. A/B测试:并行比较不同的用户体验
  3. 地理位置测试:为每个上下文配置不同的代理设置
  4. 会话隔离:防止测试场景间的交叉污染
  5. 并行抓取:使用不同配置同时抓取多个网站

创建与管理上下文

# Create isolated browser context
context_id = await browser.create_browser_context()

# Create tab in specific context
tab = await browser.new_tab("https://example.com", browser_context_id=context_id)

# Get all browser contexts
contexts = await browser.get_browser_contexts()
print(f"Active contexts: {contexts}")

# Delete context (closes all associated tabs)
await browser.delete_browser_context(context_id)

默认上下文与自定义上下文

每个浏览器启动时会包含一个默认上下文,该上下文由browser.start()返回的初始标签页构成。您可以根据需要创建更多自定义上下文:

browser = Chrome()
default_tab = await browser.start()  # Uses default context

# Create custom context
custom_context_id = await browser.create_browser_context()
custom_tab = await browser.new_tab("https://example.com", browser_context_id=custom_context_id)

# Both tabs are completely isolated from each other
await default_tab.go_to("https://site1.com")
await custom_tab.go_to("https://site2.com")

实战示例:多账号测试

以下是一个同时测试多个用户账号的实际应用案例:

async def test_multiple_accounts():
    browser = Chrome()
    await browser.start()

    # Test data for different accounts
    accounts = [
        {"username": "user1@example.com", "password": "pass1"},
        {"username": "user2@example.com", "password": "pass2"},
        {"username": "admin@example.com", "password": "admin_pass"}
    ]

    contexts_and_tabs = []

    # Create isolated context for each account
    for i, account in enumerate(accounts):
        context_id = await browser.create_browser_context()
        tab = await browser.new_tab("https://app.example.com/login", browser_context_id=context_id)

        # Login with account credentials
        await tab.find(tag_name="input", name="username").type_text(account["username"])
        await tab.find(tag_name="input", name="password").type_text(account["password"])
        await tab.find(tag_name="button", type="submit").click()

        contexts_and_tabs.append((context_id, tab, account["username"]))

    # Now test different scenarios with each account simultaneously
    for context_id, tab, username in contexts_and_tabs:
        # Each tab maintains its own login session
        await tab.go_to("https://app.example.com/dashboard")
        user_info = await tab.find(class_name="user-info").text
        print(f"User {username} dashboard: {user_info}")

    # Cleanup: delete all contexts
    for context_id, _, _ in contexts_and_tabs:
        await browser.delete_browser_context(context_id)

    await browser.stop()

上下文专属代理配置

每个浏览器上下文均可设置独立的代理参数,这使其成为地理位置测试或IP轮换的理想选择:

# Create context with specific proxy
context_id = await browser.create_browser_context(
    proxy_server="http://proxy.example.com:8080",
    proxy_bypass_list="localhost,127.0.0.1"
)

# All tabs in this context will use the specified proxy
tab = await browser.new_tab("https://example.com", browser_context_id=context_id)

高级上下文管理

上下文生命周期管理

async def manage_context_lifecycle():
    browser = Chrome()
    await browser.start()

    # Create multiple contexts for different purposes
    contexts = {}

    # Context for US region testing
    us_context = await browser.create_browser_context(
        proxy_server="http://us-proxy.example.com:8080"
    )
    contexts['us'] = us_context

    # Context for EU region testing  
    eu_context = await browser.create_browser_context(
        proxy_server="http://eu-proxy.example.com:8080"
    )
    contexts['eu'] = eu_context

    # Context for admin testing (no proxy)
    admin_context = await browser.create_browser_context()
    contexts['admin'] = admin_context

    try:
        # Use contexts for parallel testing
        us_tab = await browser.new_tab("https://api.example.com/geo", browser_context_id=contexts['us'])
        eu_tab = await browser.new_tab("https://api.example.com/geo", browser_context_id=contexts['eu'])
        admin_tab = await browser.new_tab("https://admin.example.com", browser_context_id=contexts['admin'])

        # Each tab will have different IP/location
        us_location = await us_tab.execute_script("return fetch('/api/location').then(r => r.json())")
        eu_location = await eu_tab.execute_script("return fetch('/api/location').then(r => r.json())")

        print(f"US Context Location: {us_location}")
        print(f"EU Context Location: {eu_location}")

    finally:
        # Clean up all contexts
        for region, context_id in contexts.items():
            await browser.delete_browser_context(context_id)
            print(f"Deleted {region} context")

        await browser.stop()

上下文存储隔离

async def demonstrate_storage_isolation():
    browser = Chrome()
    await browser.start()

    # Create two contexts
    context1 = await browser.create_browser_context()
    context2 = await browser.create_browser_context()

    # Create tabs in each context
    tab1 = await browser.new_tab("https://example.com", browser_context_id=context1)
    tab2 = await browser.new_tab("https://example.com", browser_context_id=context2)

    # Set different data in localStorage for each context
    await tab1.execute_script("localStorage.setItem('user', 'Alice')")
    await tab2.execute_script("localStorage.setItem('user', 'Bob')")

    # Verify isolation - each context has its own storage
    user1 = await tab1.execute_script("return localStorage.getItem('user')")
    user2 = await tab2.execute_script("return localStorage.getItem('user')")

    print(f"Context 1 user: {user1}")  # Alice
    print(f"Context 2 user: {user2}")  # Bob

    # Clean up
    await browser.delete_browser_context(context1)
    await browser.delete_browser_context(context2)
    await browser.stop()

目标管理

获取浏览器中所有活动目标(标签页、服务工作线程等)的相关信息:

# Get all targets
targets = await browser.get_targets()

# Filter for page targets only
pages = [t for t in targets if t.get('type') == 'page']

for page in pages:
    print(f"Target ID: {page['targetId']}")
    print(f"URL: {page['url']}")
    print(f"Title: {page.get('title', 'No title')}")

窗口管理

Browser域提供了控制浏览器窗口的方法:

# Get the current window ID
window_id = await browser.get_window_id()

# Set window bounds (position and size)
await browser.set_window_bounds({
    'left': 100,
    'top': 100,
    'width': 1024,
    'height': 768
})

# Maximize the window
await browser.set_window_maximized()

# Minimize the window
await browser.set_window_minimized()

窗口管理使用场景

窗口管理特别适用于以下情况: - 设置精确的窗口尺寸以获得一致的截图效果 - 在多显示器配置中定位窗口位置 - 开发期间创建可见的、用户友好的自动化流程

Browser域提供了浏览器全局Cookie管理的方法:

# Set cookies at the browser level
cookies_to_set = [
    {
        "name": "session_id",
        "value": "global_session_123",
        "domain": "example.com",
        "path": "/",
        "secure": True,
        "httpOnly": True
    }
]
await browser.set_cookies(cookies_to_set)

# Get all cookies from the browser
all_cookies = await browser.get_cookies()
print(f"Number of cookies: {len(all_cookies)}")

# Delete all cookies from the browser
await browser.delete_all_cookies()
# Create browser context
context_id = await browser.create_browser_context()

# Set cookies for specific context
await browser.set_cookies(cookies_to_set, browser_context_id=context_id)

# Get cookies from specific context
context_cookies = await browser.get_cookies(browser_context_id=context_id)

# Delete cookies from specific context
await browser.delete_all_cookies(browser_context_id=context_id)

浏览器与标签页Cookie管理区别

  • 浏览器级Cookie(使用上述方法)适用于浏览器中的所有标签页或特定上下文
  • 标签页级Cookie(使用tab.set_cookies())仅作用于特定标签页

请根据自动化需求选择合适的范围。

下载管理

配置浏览器或特定上下文的下载行为:

# Set a custom download path
download_path = "/path/to/downloads"
await browser.set_download_path(download_path)

# Advanced download configuration
await browser.set_download_behavior(
    behavior=DownloadBehavior.ALLOW,
    download_path=download_path,
    events_enabled=True  # Enable download progress events
)

# Context-specific download configuration
context_id = await browser.create_browser_context()
await browser.set_download_behavior(
    behavior=DownloadBehavior.ALLOW,
    download_path="/path/to/context/downloads",
    browser_context_id=context_id
)

权限管理

为自动化测试授予或重置浏览器权限:

from pydoll.constants import PermissionType

# Grant permissions globally
await browser.grant_permissions([
    PermissionType.GEOLOCATION,
    PermissionType.NOTIFICATIONS,
    PermissionType.CAMERA
])

# Grant permissions for specific origin
await browser.grant_permissions(
    [PermissionType.GEOLOCATION],
    origin="https://example.com"
)

# Grant permissions for specific context
context_id = await browser.create_browser_context()
await browser.grant_permissions(
    [PermissionType.MICROPHONE],
    browser_context_id=context_id
)

# Reset all permissions to defaults
await browser.reset_permissions()

事件系统概述

Browser域提供了启用和监控各类事件的方法,包括enable_fetch_events()以及用于注册事件回调的on()方法。

请求拦截

# Enable request interception
await browser.enable_fetch_events(handle_auth_requests=True)

# Register event handler for intercepted requests
async def handle_request(event):
    request_id = event['params']['requestId']
    url = event['params']['request']['url']

    if 'analytics' in url:
        # Block analytics requests
        await browser.fail_request(request_id, NetworkErrorReason.BLOCKED_BY_CLIENT)
    else:
        # Continue other requests
        await browser.continue_request(request_id)

await browser.on('Fetch.requestPaused', handle_request)

自定义响应处理

async def fulfill_custom_response(event):
    request_id = event['params']['requestId']

    # Fulfill with custom response
    await browser.fulfill_request(
        request_id=request_id,
        response_code=200,
        response_headers=[{'name': 'Content-Type', 'value': 'application/json'}],
        response_body={'message': 'Custom response from Pydoll'}
    )

await browser.on('Fetch.requestPaused', fulfill_custom_response)

浏览器与标签页事件作用域

在浏览器级别启用事件(例如browser.enable_fetch_events())时,这些事件会全局性应用于浏览器中的所有标签页。相比之下,在标签页级别启用事件(例如tab.enable_fetch_events())则仅影响特定标签页。

这一区别对性能和资源管理至关重要。当需要监控所有标签页的活动时,请在浏览器级别启用事件;若仅关注特定标签页的事件,则应在标签页级别启用。

事件系统详细文档

事件系统是Pydoll架构的核心组件,我们将在专门章节进行详细介绍。内容包括事件类型、处理模式以及高级事件驱动技术。

代理配置

Pydoll支持为浏览器连接配置代理。这对于网页抓取、测试地域特定内容或绕过基于IP的速率限制非常有用:

from pydoll.browser.chromium import Chrome
from pydoll.browser.options import ChromiumOptions

options = ChromiumOptions()

# Configure a proxy
options.add_argument('--proxy-server=http://proxy.example.com:8080')

# For proxies requiring authentication
browser = Chrome(options=options)
tab = await browser.start()

# Pydoll automatically handles proxy authentication challenges
await tab.go_to("https://example.com")

私有代理认证

Pydoll可自动处理私有代理认证:

  1. 检测到代理认证挑战时,Pydoll会进行拦截
  2. 从配置选项中自动应用代理凭证
  3. 透明完成认证流程
  4. 自动化操作不间断继续执行

相较于传统浏览器自动化方案,这使得使用认证代理变得更加简便。

总结

Browser域作为Pydoll架构的基础,通过Chrome DevTools协议为浏览器实例提供了强大的控制接口。理解其功能和设计模式后,您可以构建比传统WebDriver方案更可靠、高效的复杂浏览器自动化流程。

凭借清晰的抽象层、全面的事件系统、基于标签页的架构以及对浏览器进程的直接控制,Pydoll能在保持简单直观API的同时,支持高级自动化场景的实现。