Skip to content

核心概念

Pydoll 建立在几个设计决策之上,它们塑造了你编写每个脚本的方式:无 webdriver、同时提供同步和异步形式的 API、拟人化交互,以及事件系统。本页在可动手的层面上解释每一项,好让后续的任务指南更容易理解。

无 webdriver

Pydoll 通过 Chrome DevTools Protocol(CDP)直接连接浏览器,这正是你打开检查器时驱动 Chrome DevTools 的同一套协议。中间没有 webdriver 可执行文件,所以没有东西需要下载,也不用去排查“chromedriver 只支持 Chrome 版本 X”这类不匹配的问题。

graph LR
    subgraph P["Pydoll"]
        direction LR
        P1["你的代码"] --> P2["Pydoll"] --> P3["浏览器 (CDP)"]
    end
    subgraph S["Selenium"]
        direction LR
        S1["你的代码"] --> S2["WebDriver 客户端"] --> S3["chromedriver"] --> S4["浏览器"]
    end

当你启动浏览器时,Pydoll 会用一个远程调试端口拉起你已经安装的那个 Chrome,并向它的 CDP 端点打开一个 WebSocket:

from pydoll.sync import Chrome

def main():
    with Chrome() as browser:
        tab = browser.start()
        tab.go_to('https://quotes.toscrape.com')

main()
import asyncio

from pydoll import Chrome


async def main():
    async with Chrome() as browser:
        tab = await browser.start()
        await tab.go_to('https://quotes.toscrape.com')

asyncio.run(main())

你不用管理端口、连接或浏览器进程;start() 会做这些,而 with 代码块会在你用完后停止浏览器。

browser 和 tab 对象

两个对象覆盖了你大部分的操作。browser(Chrome 或 Edge)是你启动的进程。由 browser.start() 返回的 tab 才是你实际操作的对象:导航、元素查找、截图,页面上的一切都通过它进行。

with Chrome() as browser:
    tab = browser.start()          # 第一个 tab
    tab.go_to('https://quotes.toscrape.com')

    second = browser.new_tab()     # 从 browser 打开更多 tab
    second.go_to('https://books.toscrape.com')
async with Chrome() as browser:
    tab = await browser.start()          # 第一个 tab
    await tab.go_to('https://quotes.toscrape.com')

    second = await browser.new_tab()     # 从 browser 打开更多 tab
    await second.go_to('https://books.toscrape.com')

管理多个标签页请看 标签页,隔离会话请看 浏览器上下文。

同步与异步

Pydoll 以两种形式提供同一套 API。从 pydoll.sync 导入,每个调用都会阻塞到浏览器响应为止,脚本从上到下顺序阅读,无需管理事件循环。从 pydoll 导入,同样的类就是协程:你在 async def 函数里 await 每个调用,并用 asyncio.run() 启动程序。同步形式由异步形式生成,所以两者在方法、参数和默认值上永远不会有差异,本文档的每个示例都同时给出两种写法。对于已经存在的代码还有第三扇门:改一行导入,Playwright 脚本就能跑在 Pydoll 上,参见带上你的 Playwright 脚本。

异步形式的优势在于并发。导航和元素等待大部分时间都处于空闲,所以 asyncio.gather 可以让它们同时进行,而不是一个接一个。同步形式用线程达到同样的效果,因为它的调用可以安全地从多个线程同时发起:

from concurrent.futures import ThreadPoolExecutor

from pydoll.sync import Chrome


def title_of(browser, url):
    tab = browser.new_tab(url)
    title = tab.title()
    tab.close()
    return title


def main():
    urls = [
        'https://quotes.toscrape.com/page/1/',
        'https://quotes.toscrape.com/page/2/',
        'https://quotes.toscrape.com/page/3/',
    ]
    with Chrome() as browser:
        browser.start()
        with ThreadPoolExecutor() as pool:
            titles = list(pool.map(lambda url: title_of(browser, url), urls))
        print(titles)

main()
import asyncio

from pydoll import Chrome


async def title_of(browser, url):
    tab = await browser.new_tab(url)
    title = await tab.title()
    await tab.close()
    return title


async def main():
    urls = [
        'https://quotes.toscrape.com/page/1/',
        'https://quotes.toscrape.com/page/2/',
        'https://quotes.toscrape.com/page/3/',
    ]
    async with Chrome() as browser:
        await browser.start()
        titles = await asyncio.gather(*(title_of(browser, url) for url in urls))
        print(titles)

asyncio.run(main())

三个页面并发加载,所以整体耗时大约等于最慢的单个页面,而不是三者之和。

刚接触异步 Python?

如果对 async、await 和 gather 还不熟悉,请先读 异步 Python 实战。它只讲够用的 asyncio,足以让你从容读完这些指南的其余部分。

拟人化交互

默认情况下,点击会落在元素中心,输入则一个键接一个键地发送,快到浏览器能接受的程度。传入 humanize=True,Pydoll 就会让光标沿一条曲线路径移动后再点击,并以可变的节奏输入,其中偶尔还会出现被纠正的手误:

search = tab.find(id='search')
search.type_text('web scraping', humanize=True)
search.click(humanize=True)
search = await tab.find(id='search')
await search.type_text('web scraping', humanize=True)
await search.click(humanize=True)

拟人化是逐次交互按需开启的,所以你可以在会盯着行为看的站点上启用它,而在只看重原始速度的地方跳过它。计时模型请看 拟人化交互,完整的输入 API 请看 键盘 和 鼠标。

事件驱动

你可以订阅浏览器事件、在它们触发时运行回调,而不必在循环里轮询页面。这正是你捕获网络流量、对导航做出反应,或等待某个特定请求的方式:

import time
from functools import partial

from pydoll.sync import Chrome, NetworkEvent

def on_request(tab, event):
    url = event['params']['request']['url']
    if '/api/' in url:
        print(f'API call: {url}')

def main():
    with Chrome() as browser:
        tab = browser.start()

        tab.enable_network_events()
        tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, partial(on_request, tab))

        tab.go_to('https://quotes.toscrape.com')
        time.sleep(2)

main()
import asyncio
from functools import partial

from pydoll import Chrome, NetworkEvent


async def on_request(tab, event):
    url = event['params']['request']['url']
    if '/api/' in url:
        print(f'API call: {url}')


async def main():
    async with Chrome() as browser:
        tab = await browser.start()

        await tab.enable_network_events()
        await tab.on(NetworkEvent.REQUEST_WILL_BE_SENT, partial(on_request, tab))

        await tab.go_to('https://quotes.toscrape.com')
        await asyncio.sleep(2)

asyncio.run(main())

只启用你用到的事件域,用完就把它们关掉。完整的模型请看 事件,流量捕获请看 网络监控。

适用于各种 Chromium 浏览器

同一套 API 可以驱动任何 Chromium 浏览器。Chrome 是首要目标;Edge 有完整支持;其他 Chromium 构建则通过把 binary_location 指向它们来使用。

from pydoll.sync import Chrome, ChromiumOptions, Edge

# Chrome
with Chrome() as browser:
    tab = browser.start()

# Edge
with Edge() as browser:
    tab = browser.start()

# 任何其他 Chromium 构建(Brave、Vivaldi、Opera ……)
options = ChromiumOptions()
options.binary_location = '/path/to/brave-browser'
with Chrome(options=options) as browser:
    tab = browser.start()
from pydoll import Chrome, ChromiumOptions, Edge

# Chrome
async with Chrome() as browser:
    tab = await browser.start()

# Edge
async with Edge() as browser:
    tab = await browser.start()

# 任何其他 Chromium 构建(Brave、Vivaldi、Opera ……)
options = ChromiumOptions()
options.binary_location = '/path/to/brave-browser'
async with Chrome(options=options) as browser:
    tab = await browser.start()

下一步