Python爬虫之路(14)--playwright浏览器自动化

playwright

前言

​ 你有没有在用 Selenium 抓网页的时候,体验过那种「明明点了按钮,它却装死不动」的痛苦?或者那种「刚加载完页面,它又刷新了」的抓狂?别担心,你不是一个人------那是 Selenium 在和现代前端技术硬刚,结果被 JS 动态渲染按在地上摩擦。

​ 于是,我转身投入了 Playwright 的怀抱。

​ 这个由微软亲儿子团队打造的自动化框架,一上来就自带"全家桶":Chromium、Firefox、WebKit 全支持,还能像忍者一样拦截请求、伪装自己、模拟手机、欺骗验证码......你说你是网站的反爬系统?对不起,它已经绕过你了。

​ 更重要的是:Playwright 不用我一边调试一边祈祷"这个元素会不会加载出来",它会耐心地等着网页准备好,就像个懂事的小助手。

​ 所以,如果你发现我全程没提 Selenium,那不是我忘了它,而是......我只是选择了更适合现代网页的那一位。

palywright(python)官网

playwright 中文文档


一. Playwright 的特点

  • Playwright 支持当前所有主流浏览器,包括 Chrome 和 Edge(基于 Chromium)、Firefox、Safari(基于 WebKit) ,提供完善的自动化控制的 API。
  • Playwright 支持移动端页面测试,使用设备模拟技术可以使我们在移动 Web 浏览器中测试响应式 Web 应用程序。
  • Playwright 支持所有浏览器的 Headless 模式和非 Headless 模式的测试。
  • Playwright 的安装和配置非常简单,安装过程中会自动安装对应的浏览器和驱动,不需要额外配置 WebDriver 等。
  • Playwright 提供了自动等待相关的 API,当页面加载的时候会自动等待对应的节点加载,大大简化了 API 编写复杂度。

本节我们就来了解下 Playwright 的使用方法。


二.Playwright 与selemium的区别

自动化多浏览器更强

  • Playwright 原生支持 Chromium、Firefox、WebKit(Safari 内核);
  • Selenium 也支持多浏览器,但配置复杂、兼容性差一些。并且在配置内核时容易出现浏览器自动升级内核导致与selemium不适配无法正常启动项目。

对现代网页支持更好

  • Playwright 更好地处理 单页应用(SPA)、动态加载内容(JS 渲染);
  • 它可以自动等待页面元素、网络请求完成,避免使用 sleep 等土办法。

操作简单、等待机制智能

  • Playwright 的 waitForSelector 和自动等待机制,能自动识别页面何时准备好;
  • Selenium 中常常需要显式 WebDriverWait,操作相对繁琐。

更原生地控制浏览器行为

  • 可以拦截请求、修改请求/响应、模拟网络环境等:

    python 复制代码
    await page.route("**/*", lambda route: route.abort())
  • 在绕过反爬机制时非常有用(如移除监控脚本、模拟慢速网络等)。

支持无头/有头模式灵活切换

  • Playwright 在无头和有头模式之间切换非常平滑,而且在无头模式下表现更稳定;
  • Selenium 的无头模式在某些浏览器上可能会出现差异行为。

更强的并发与多页面控制

  • 支持多标签页、多浏览器上下文并发,适合大规模数据抓取;
  • 对资源隔离也更好(cookie/session 等可分开)。

API 更现代化、开发体验更好

  • Playwright 的异步接口更符合现代 Python(或 JS/TS)开发习惯;
  • 文档清晰、内置调试功能更丰富(如 codegen 工具)。

Selenium 仍有的一些优势

  • 社区老牌、生态大,很多成熟的库依赖它;
  • 如果目标网站是老旧结构(非 SPA),Selenium 依然非常够用;
  • Java 生态用户更多(Selenium 是最早就支持 Java 的);

总结一句话

Playwright 更适合现代网页、动态内容丰富的爬虫项目;而 Selenium 则更适合传统页面、对稳定性要求更高的老项目。


三. 安装依赖

要使用 Playwright,需要 Python 3.7 版本及以上,请确保 Python 的版本符合要求。

要安装 Playwright,可以直接使用 pip3,命令如下:

cmd 复制代码
pip3 install playwright

安装完成之后需要进行一些初始化操作:

cmd 复制代码
playwright install

这时候 Playwrigth 会安装 Chromium, Firefox and WebKit 浏览器并配置一些驱动,我们不必关心中间配置的过程,Playwright 会为我们配置好。

四.基础使用

1)playwright启动

palywright的使用方法有两种,一种是同步模式,一种是异步模式

  • 同步模式(sync_api:一步一步来,像早期排队买奶茶,前一个的奶茶没做完,下一个不能开始点单。
  • 异步模式(async_api:一边下单,一边刷抖音,等奶茶好了系统通知你。
同步版本(sync_playwright串行执行)
python 复制代码
from playwright.sync_api import sync_playwright

def get_title_sync(urls):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()

        for url in urls:
            page.goto(url)
            print(f"{url} -> {page.title()}")

        browser.close()

urls = ["https://www.baidu.com", "https://www.bing.com", "https://www.sougou.com"]
get_title_sync(urls)

特点

  • 一个页面加载完、取完标题后,才处理下一个;
  • 多个页面加载时间相加,比较慢;
  • 简单易懂,但效率低。
异步方式(async_playwright并发执行)
python 复制代码
import asyncio
from playwright.async_api import async_playwright

async def get_title(page, url):
    await page.goto(url)
    title = await page.title()
    print(f"{url} -> {title}")

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        context = await browser.new_context()
        
        urls = ["https://www.baidu.com", "https://www.bing.com", "https://www.sougou.com"]
        pages = [await context.new_page() for _ in urls]
        
        # 同时并发访问所有页面
        tasks = [get_title(page, url) for page, url in zip(pages, urls)]
        await asyncio.gather(*tasks)

        await browser.close()

asyncio.run(main())

特点:

  • 所有网页同时打开并加载标题;
  • 利用异步 + 并发,速度飞快;
  • 对于大型爬虫任务,效率提升非常明显。

对比结果

同步版本大概是:

swift 复制代码
百度 -> 用时 2 秒
Bing -> 用时 2 秒
sougou -> 用时 2 秒
总计约 6 秒

而异步版本是:

swift 复制代码
百度/Bing/sougou 几乎同时完成
总计约 2 秒

适用场景对比
特性 同步 Playwright 异步 Playwright
上手难度 ✅ 简单 ❗ 稍复杂
并发能力 ❌ 差 ✅ 优秀
写法风格 传统脚本风格 现代异步协程风格
推荐使用场景 小型任务、调试、单页面操作 大规模抓取、多个任务并发执行

2)browser、context、page的联系

在上面的演示示例中出现了browser,context,page。本小节将先讲解三者的联系。

对象 简介
browser 启动的浏览器实例(比如打开了一个 Chrome)
context 类似于一个独立的「浏览器用户配置环境」,有独立的 cookie、session 等
page 一个具体的标签页/网页

三者的层级关系(图解式)
swift 复制代码
Browser(浏览器)
└── Context(浏览器上下文 / 用户环境)
    ├── Page(标签页 / 页面)
    └── Page

你可以有:

  • 一个 browser 启动多个 context
  • 每个 context 打开多个 page

类比一下:浏览器、用户、标签页
Playwright 对象 类比 说明
browser 整个浏览器程序 比如你打开了 Chrome 浏览器
context 一个浏览器用户 你在 Chrome 里登录了不同账户(环境独立)
page 一个标签页 每个页面就是你开的一个 tab(百度首页,B站首页...)

实战中举个例子
python 复制代码
import asyncio
from playwright.async_api import async_playwright
async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        context = await browser.new_context()
        page = await context.new_page()
        await page.goto("https://baidu.com")
        title = await page.title()
        print(title)
asyncio.run(main())

解释:

  • browser:你打开了一个新的浏览器(比如 Chromium);
  • context:你创建了一个「用户环境」,相当于一个新的匿名窗口;
  • page:你在这个窗口中打开了一个标签页,加载了页面。

为什么要用 context

因为它带来更好的隔离性模拟多个用户的能力

比如:

  • 模拟多个用户登录不同账户 → 每个用户一个 context
  • 多线程爬虫时不想 cookie/session 冲突 → 每个线程独立开 context
  • 防止共享本地存储/缓存 → context 是清洁的小环境

进阶场景

比如爬虫时这样用:

python 复制代码
for user in users:
    context = await browser.new_context(storage_state=user["cookies"])
    page = await context.new_page()
    await page.goto("https://target.com/profile")

这样就能并行模拟多个用户(B站的用户1,B站的用户2...不会因为用户2的登陆导致用户1的相关信息被覆盖或丢失),互不干扰、效率超高


3)基础使用

1.创建浏览器对象
  • 同步模式

    python 复制代码
    # Can be "msedge", "chrome-beta", "msedge-beta", "msedge-dev", etc.
    browser = playwright.chromium.launch(channel="chrome",headless=True)
    python 复制代码
    from playwright.sync_api import sync_playwright
    
    with sync_playwright() as p:
        for browser in [p.chromium, p.firefox, p.webkit]:
            browser = browser.launch(headless=False)
            page = browser.new_page()
            page.goto('https://www.baidu.com')
            page.screenshot(path=f'screenshot-{browser.name}.png')
            print(page.title())
            browser.close()
  • 异步模式

    python 复制代码
    # 异步
    # Can be "msedge", "chrome-beta", "msedge-beta", "msedge-dev", etc.
    browser = await playwright.chromium.launch(channel="chrome",headless=True)
    python 复制代码
    import asyncio
    from playwright.async_api import async_playwright
    
    async def get_title(page, url):
        try:
            await page.goto(url)
            title = await page.title()
            print(f"url: {url} -> {title}")
        except Exception as e:
            print(f"Error fetching {url}: {e}")
    
    async def main():
        async with async_playwright() as p:
            # 启动三种浏览器内核
            browsers = [
                await p.chromium.launch(headless=True),
                await p.firefox.launch(headless=True),
                await p.webkit.launch(headless=True)
            ]
    
            # 为每个浏览器创建独立上下文和页面
            contexts = [await browser.new_context() for browser in browsers]
            pages = [await context.new_page() for context in contexts]
    
            # 设置 URL
            url = "http://www.baidu.com"
    
            # 为每个页面分配任务(同一个 URL)
            tasks = [get_title(page, url) for page in pages]
            await asyncio.gather(*tasks)
    
            # 关闭所有浏览器
            for browser in browsers:
                await browser.close()
    
    # 运行主函数
    asyncio.run(main())

参数

  • channel: 可以选择不同的浏览器版本,msedge就是Microsoft Edge浏览器,同时还支持beta版本。在大部分情况下,使用默认的chrome浏览器足够了。
  • headless: 是否开启无头模式(True开启无头模式不显示浏览器,False显示浏览器)

2.代码生成(本小节来自崔庆才老师的内容)

Playwright 还有一个强大的功能,那就是可以录制我们在浏览器中的操作并将代码自动生成出来,有了这个功能,我们甚至都不用写任何一行代码,这个功能可以通过 playwright 命令行调用 codegen 来实现,我们先来看看 codegen 命令都有什么参数,输入如下命令:

cmd 复制代码
playwright codegen --help

结果类似如下:

cmd 复制代码
Usage: npx playwright codegen [options] [url]

open page and generate code for user actions

Options:
  -o, --output <file name>     saves the generated script to a file
  --target <language>          language to use, one of javascript, python, python-async, csharp (default: "python")
  -b, --browser <browserType>  browser to use, one of cr, chromium, ff, firefox, wk, webkit (default: "chromium")
  --channel <channel>          Chromium distribution channel, "chrome", "chrome-beta", "msedge-dev", etc
  --color-scheme <scheme>      emulate preferred color scheme, "light" or "dark"
  --device <deviceName>        emulate device, for example  "iPhone 11"
  --geolocation <coordinates>  specify geolocation coordinates, for example "37.819722,-122.478611"
  --load-storage <filename>    load context storage state from the file, previously saved with --save-storage
  --lang <language>            specify language / locale, for example "en-GB"
  --proxy-server <proxy>       specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"
  --save-storage <filename>    save context storage state at the end, for later use with --load-storage
  --timezone <time zone>       time zone to emulate, for example "Europe/Rome"
  --timeout <timeout>          timeout for Playwright actions in milliseconds (default: "10000")
  --user-agent <ua string>     specify user agent string
  --viewport-size <size>       specify browser viewport size in pixels, for example "1280, 720"
  -h, --help                   display help for command

Examples:

  $ codegen
  $ codegen --target=python
  $ codegen -b webkit https://example.com

可以看到这里有几个选项,比如

  • -o 代表输出的代码文件的名称;
  • ---target 代表使用的语言,默认是 python,即会生成同步模式的操作代码,如果传入 python-async 就会生成异步模式的代码;
  • -b 代表的是使用的浏览器,默认是 Chromium
  • ---device 可以模拟使用手机浏览器,比如 iPhone 11
  • ---lang 代表设置浏览器的语言
  • ---timeout 可以设置页面加载超时时间。

好,了解了这些用法,那我们就来尝试启动一个 Firefox 浏览器,然后将操作结果输出到 script.py 文件,命令如下:

cmd 复制代码
playwright codegen -o 3_2.py -b chromium

这时候就弹出了一个 Firefox 浏览器,同时右侧会输出一个脚本窗口,实时显示当前操作对应的代码。

我们可以在浏览器中做任何操作,比如打开http://www.baidu.com,通过图中的检索元素可以找到对应元素的标记,点击后还可以通过右侧locator进行识别

我们现在在输入框输入NBA排名可以看见浏览器中还会高亮显示我们正在操作的页面节点,右侧的窗口如图所示:

操作完毕之后,关闭浏览器,Playwright 会生成一个 3_2.py 文件,内容如下:

python 复制代码
import re
from playwright.sync_api import Playwright, sync_playwright, expect


def run(playwright: Playwright) -> None:
    browser = playwright.chromium.launch(headless=False)
    context = browser.new_context()
    page = context.new_page()
    page.goto("http://www.baidu.com/")
    page.locator("#kw").click()
    page.locator("#kw").fill("NBA")
    page.locator("#kw").press("CapsLock")
    page.locator("#kw").fill("NBA排名")
    page.goto("http://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=nba%E6%8E%92%E5%90%8D&fenlei=256&rsv_pq=0x8cc30d85063c7a80&rsv_t=e6c02Rfwvst%2F6FN9HVCbwx4kcnD9jZsz4W28bEWoUHac4rIlfbJS1AhduX1a&rqlang=en&rsv_dl=ib&rsv_sug3=11&rsv_sug1=2&rsv_sug7=100")
    page.close()

    # ---------------------
    context.close()
    browser.close()


with sync_playwright() as playwright:
    run(playwright)

可以看到这里生成的代码和我们之前写的示例代码几乎差不多,而且也是完全可以运行的,运行之后就可以看到它又可以复现我们刚才所做的操作了。

所以,有了这个功能,我们甚至都不用编写任何代码,只通过简单的可视化点击就能把代码生成出来,可谓是非常方便了!


3.常见元素操作

本小节内容将以异步模式书写,且url为http://www.baidu.com

1.页面截图
python 复制代码
await page.screenshot(
    path="./baidu.png",     # 保存路径
    full_page=False,      # 是否截图整个页面(默认只截当前视口)
    clip={                # 指定截图区域(x, y, width, height)
        "x": 100,
        "y": 200,
        "width": 500,
        "height": 400
    },
    type="png",           # 图片类型:"png"(默认)或 "jpeg"
    quality=80,           # 图片质量(仅 jpeg 有效,0-100)
    omit_background=True  # 透明背景(适用于 PNG 截图)
)

如果你想只截图某个具体元素(比如某个按钮、标题、div),可以这样:

python 复制代码
await page.goto("https://baidu.com")
logo = await page.query_selector('//*[@id="s_lg_img"]')
await logo.screenshot(path="baidu_logo.png")

2.常见的元素操作(填充文字,点击等)

填充文字(fill()

python 复制代码
await page.fill('input[name="username"]', 'my_username')
await page.fill('//input[@type="password"]', 'my_password')  # XPath 用法

点击元素(click()

python 复制代码
await page.click('button[type="submit"]')
await page.click('//a[contains(text(), "登录")]')  # XPath 定位

⚠️ Playwright 会自动等待元素可见、可点击,不需要写 time.sleep()


选中下拉框选项(select_option()

python 复制代码
await page.select_option('select#city', 'shanghai')  # value值
await page.select_option('//select[@id="lang"]', label="中文")  # 根据文本

勾选/取消勾选复选框(check() / uncheck()

python 复制代码
await page.check('input[type="checkbox"]')
await page.uncheck('input[type="checkbox"]')

上传文件(set_input_files()

python 复制代码
await page.set_input_files('input[type="file"]', 'myfile.pdf')

提交表单

Playwright 没有专门的 submit() 方法,一般点击提交按钮即可:

python 复制代码
await page.click('button[type="submit"]')

或者用 JS 触发提交:

python 复制代码
await page.eval_on_selector('form', 'form => form.submit()')

获取文本内容(text_content()

python 复制代码
text = await page.text_content('h1')
print("页面标题:", text)

获取标签属性值(get_attribute()

python 复制代码
href = await page.get_attribute('a', 'href')
print("链接地址:", href)

判断元素是否存在

python 复制代码
el = await page.query_selector('div.alert')
if el:
    print("警告提示框存在!")

判断元素是否可见(需要 is_visible()

python 复制代码
is_visible = await page.is_visible('div#popup')
print("是否可见:", is_visible)

清除输入框内容

python 复制代码
await page.fill('input[name="email"]', '')  # 直接填空字符串

小结
操作 方法名
填文字 fill()
点击 click()
下拉选择 select_option()
勾选 check() / uncheck()
上传文件 set_input_files()
获取文本 text_content()
获取属性 get_attribute()
判断存在 query_selector()
判断可见 is_visible()

3.css选择器(内容来自崔庆才老师)

ps常用知识:id -- # class -- .

​ 前面我们注意到 click 和 fill 等方法都传入了一个字符串,这些字符串有的符合 CSS 选择器的语法,有的又是 text= 开头的,感觉似乎没太有规律的样子,它到底支持怎样的匹配规则呢?下面我们来了解下。

​ 传入的这个字符串,我们可以称之为 Element Selector,它不仅仅支持 CSS 选择器、XPath,Playwright 还扩展了一些方便好用的规则,比如直接根据文本内容筛选,根据节点层级结构筛选等等。

文本选择

文本选择支持直接使用 text= 这样的语法进行筛选,示例如下:

python 复制代码
page.click("text=Log in")

这就代表选择文本是 Log in 的节点,并点击。

CSS 选择器

CSS 选择器之前也介绍过了,比如根据 id 或者 class 筛选:

python 复制代码
page.click("button")
page.click("#nav-bar .contact-us-item")

根据特定的节点属性筛选:

python 复制代码
page.click("[data-test=login-button]")
page.click("[aria-label='Sign in']")

CSS 选择器 + 文本

我们还可以使用 CSS 选择器结合文本值进行海选,比较常用的就是 has-text 和 text,前者代表包含指定的字符串,后者代表字符串完全匹配,示例如下:

python 复制代码
page.click("article:has-text('Playwright')")
page.click("#nav-bar :text('Contact us')")

第一个就是选择文本中包含 Playwright 的 article 节点,第二个就是选择 id 为 nav-bar 节点中文本值等于 Contact us 的节点。

CSS 选择器 + 节点关系

还可以结合节点关系来筛选节点,比如使用 has 来指定另外一个选择器,示例如下:

python 复制代码
page.click(".item-description:has(.item-promo-banner)")

比如这里选择的就是选择 class 为 item-description 的节点,且该节点还要包含 class 为 item-promo-banner 的子节点。

另外还有一些相对位置关系,比如 right-of 可以指定位于某个节点右侧的节点,示例如下:

python 复制代码
page.click("input:right-of(:text('Username'))")

这里选择的就是一个 input 节点,并且该 input 节点要位于文本值为 Username 的节点的右侧。


4.与xpath结合(本人更熟悉xpath)

点击按钮

python 复制代码
await page.click('xpath=//button[text()="提交"]')
await page.click('xpath=//a[contains(text(), "登录")]')

填写表单

python 复制代码
await page.fill('xpath=//input[@name="username"]', 'my_user')
await page.fill('xpath=//input[@type="password"]', '123456')

获取元素文本

python 复制代码
text = await page.text_content('xpath=//h1')
print("标题:", text)

获取属性值

python 复制代码
href = await page.get_attribute('xpath=//a[text()="更多"]', 'href')

判断元素是否存在

python 复制代码
el = await page.query_selector('xpath=//div[@class="alert"]')
if el:
    print("警告框存在")

遍历多个元素

python 复制代码
elements = await page.query_selector_all('xpath=//ul/li')
for el in elements:
    text = await el.text_content()
    print(text)

选择下拉框

python 复制代码
await page.select_option('xpath=//select[@id="lang"]', label="中文")

上传文件

python 复制代码
await page.set_input_files('xpath=//input[@type="file"]', 'resume.pdf')

截图指定元素

python 复制代码
element = await page.query_selector('xpath=//div[@id="banner"]')
await element.screenshot(path="./banner.png")

等待某元素出现(自动超时)

python 复制代码
await page.wait_for_selector('xpath=//div[@class="loading-done"]')

XPath 表达式小抄
作用 XPath 示例
精确匹配标签属性 //input[@name="email"]
模糊匹配文本 //a[contains(text(), "登录")]
文本等于 //button[text()="提交"]
多层级路径 //div[@class="user"]/span
获取第一个元素 //ul/li[1]
获取最后一个元素 //ul/li[last()]
多属性组合选择 //input[@type="text" and @placeholder]
自定义属性 //div[@data-role="header"]

5.持久化保留登陆状态(cookie内容)

案例来自于开源项目MediaCrawler(欢迎大家star):

python 复制代码
#关键函数
        browser_context = await chromium.launch_persistent_context(
            user_data_dir=user_data_dir,
            accept_downloads=True,
            headless=headless,
            proxy=playwright_proxy,  # type: ignore
            viewport={"width": 1920, "height": 1080},
            user_agent=user_agent
        )

Cookie常用处理函数

python 复制代码
def convert_cookies(cookies: Optional[List[Cookie]]) -> Tuple[str, Dict]:
    if not cookies:
        return "", {}
    cookies_str = ";".join([f"{cookie.get('name')}={cookie.get('value')}" for cookie in cookies])
    cookie_dict = dict()
    for cookie in cookies:
        cookie_dict[cookie.get('name')] = cookie.get('value')
    return cookies_str, cookie_dict

def convert_str_cookie_to_dict(cookie_str: str) -> Dict:
    cookie_dict: Dict[str, str] = dict()
    if not cookie_str:
        return cookie_dict
    for cookie in cookie_str.split(";"):
        cookie = cookie.strip()
        if not cookie:
            continue
        cookie_list = cookie.split("=")
        if len(cookie_list) != 2:
            continue
        cookie_value = cookie_list[1]
        if isinstance(cookie_value, list):
            cookie_value = "".join(cookie_value)
        cookie_dict[cookie_list[0]] = cookie_value
    return cookie_dict
python 复制代码
#使用方法--登陆过后
cookie_str, cookie_dict = convert_cookies(await browser_context.cookies())
headers["Cookie"] = cookie_str

完整实例

python 复制代码
# 是否开启无头模式
HEADLESS = False

# 平台
PLATFORM = "bilibili"

# 是否保存登录状态
SAVE_LOGIN_STATE = True

# 用户浏览器缓存的浏览器文件配置
USER_DATA_DIR = "%s_user_data_dir"  # %s will be replaced by platform name

import os
import sys
import asyncio
from typing import Optional,Dict,List,Tuple
from playwright.async_api import async_playwright
from playwright.async_api import (BrowserContext, BrowserType, Page, async_playwright,Cookie)


async def main():
    async with async_playwright() as p:
         chromium = p.chromium
         browser_context = await launch_browser(chromium,None, None, headless=False)


async def launch_browser(
        chromium: BrowserType,
        playwright_proxy: Optional[Dict], # [{"http":"http://ip:port","https":"https://ip:port"},... ...]
        user_agent: Optional[str],  # ua列表
        headless: bool = True   # 无头模式
) -> BrowserContext:
    """
    launch browser and create browser context
    :param chromium: chromium browser
    :param playwright_proxy: playwright proxy
    :param user_agent: user agent
    :param headless: headless mode
    :return: browser context
    """

    if SAVE_LOGIN_STATE:
        # feat issue #14
        # we will save login state to avoid login every time
        user_data_dir = os.path.join(os.getcwd(), "browser_data",
                                     config.USER_DATA_DIR % PLATFORM)  # type: ignore
        browser_context = await chromium.launch_persistent_context(
            user_data_dir=user_data_dir,
            accept_downloads=True,
            headless=headless,
            proxy=playwright_proxy,  # type: ignore
            viewport={"width": 1920, "height": 1080},
            user_agent=user_agent
        )
        return browser_context
    else:
        # type: ignore
        browser = await chromium.launch(headless=headless, proxy=playwright_proxy)
        browser_context = await browser.new_context(
            viewport={"width": 1920, "height": 1080},
            user_agent=user_agent
        )
        return browser_context

def convert_cookies(cookies: Optional[List[Cookie]]) -> Tuple[str, Dict]:
    if not cookies:
        return "", {}
    cookies_str = ";".join([f"{cookie.get('name')}={cookie.get('value')}" for cookie in cookies])
    cookie_dict = dict()
    for cookie in cookies:
        cookie_dict[cookie.get('name')] = cookie.get('value')
    return cookies_str, cookie_dict


def convert_str_cookie_to_dict(cookie_str: str) -> Dict:
    cookie_dict: Dict[str, str] = dict()
    if not cookie_str:
        return cookie_dict
    for cookie in cookie_str.split(";"):
        cookie = cookie.strip()
        if not cookie:
            continue
        cookie_list = cookie.split("=")
        if len(cookie_list) != 2:
            continue
        cookie_value = cookie_list[1]
        if isinstance(cookie_value, list):
            cookie_value = "".join(cookie_value)
        cookie_dict[cookie_list[0]] = cookie_value
    return cookie_dict

if __name__ == '__main__':
    try:
        # asyncio.run(main())
        asyncio.get_event_loop().run_until_complete(main())
    except KeyboardInterrupt:
        sys.exit()
6.js文件注入(防自动化检测),js代码运行

js文件注入

python 复制代码
# stealth.min.js is a js script to prevent the website from detecting the crawler.
await self.browser_context.add_init_script(path="libs/stealth.min.js")

完整示例

python 复制代码
import asyncio
import sys
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        context = await browser.new_context()
        await context.add_init_script("./3_5_6_stealth.min.js")
        page = await context.new_page()
        
        await page.goto("https://baidu.com")
        print(f"title:{await page.title()}")
        
if __name__ == '__main__':
    try:
        # asyncio.run(main())
        asyncio.get_event_loop().run_until_complete(main())
    except KeyboardInterrupt:
        sys.exit()

运行js代码

python 复制代码
async def run_js_code():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        context = await browser.new_context(storage_state="storage.json")
        page = await context.new_page()

        await page.goto("https://www.bilibili.com")

        title = await page.evaluate("() => document.title")
        print(f"页面标题是:{title}")

        # 模拟修改 DOM
        await page.evaluate("""
            () => {
                let h = document.createElement('h1');
                h.innerText = "✨ Hello from injected JS!";
                h.style.color = "red";
                document.body.prepend(h);
            }
        """)

        await browser.close()
总结
功能 方法或函数
保存登录状态 BrowserType.launch_persistent_context(path=...)
注入js文件(初始) context.add_init_script("./3_5_6_stealth.min.js")
注入 JS 文件 page.add_script_tag(content=...)
执行 JS 代码片段 page.evaluate("() => { ... }")

7.事件监听(内容来自崔庆才老师)

Page 对象提供了一个 on 方法,它可以用来监听页面中发生的各个事件,比如 close、console、load、request、response 等等。

比如这里我们可以监听 response 事件,response 事件可以在每次网络请求得到响应的时候触发,我们可以设置对应的回调方法获取到对应 Response 的全部信息,示例如下:

python 复制代码
from playwright.sync_api import sync_playwright

def on_response(response):
    print(f'Statue {response.status}: {response.url}')

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.on('response', on_response)
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    browser.close()

这里我们在创建 Page 对象之后,就开始监听 response 事件,同时将回调方法设置为 on_response,on_response 对象接收一个参数,然后把 Response 的状态码和链接都输出出来了。

运行之后,可以看到控制台输出结果如下:

python 复制代码
Statue 200: https://spa6.scrape.center/
Statue 200: https://spa6.scrape.center/css/app.ea9d802a.css
Statue 200: https://spa6.scrape.center/js/app.5ef0d454.js
Statue 200: https://spa6.scrape.center/js/chunk-vendors.77daf991.js
Statue 200: https://spa6.scrape.center/css/chunk-19c920f8.2a6496e0.css
...
Statue 200: https://spa6.scrape.center/css/chunk-19c920f8.2a6496e0.css
Statue 200: https://spa6.scrape.center/js/chunk-19c920f8.c3a1129d.js
Statue 200: https://spa6.scrape.center/img/logo.a508a8f0.png
Statue 200: https://spa6.scrape.center/fonts/element-icons.535877f5.woff
Statue 301: https://spa6.scrape.center/api/movie?limit=10&offset=0&token=NGMwMzFhNGEzMTFiMzJkOGE0ZTQ1YjUzMTc2OWNiYTI1Yzk0ZDM3MSwxNjIyOTE4NTE5
Statue 200: https://spa6.scrape.center/api/movie/?limit=10&offset=0&token=NGMwMzFhNGEzMTFiMzJkOGE0ZTQ1YjUzMTc2OWNiYTI1Yzk0ZDM3MSwxNjIyOTE4NTE5
Statue 200: https://p0.meituan.net/movie/da64660f82b98cdc1b8a3804e69609e041108.jpg@464w_644h_1e_1c
Statue 200: https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg@464w_644h_1e_1c
....
Statue 200: https://p1.meituan.net/movie/b607fba7513e7f15eab170aac1e1400d878112.jpg@464w_644h_1e_1c

注意:这里省略了部分重复的内容。

可以看到,这里的输出结果其实正好对应浏览器 Network 面板中所有的请求和响应内容,和下图是一一对应的:

这个网站我们之前分析过,其真实的数据都是 Ajax 加载的,同时 Ajax 请求中还带有加密参数,不好轻易获取。

但有了这个方法,这里如果我们想要截获 Ajax 请求,岂不是就非常容易了?

改写一下判定条件,输出对应的 JSON 结果,改写如下:

python 复制代码
from playwright.sync_api import sync_playwright

def on_response(response):
    if '/api/movie/' in response.url and response.status == 200:
        print(response.json())

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.on('response', on_response)
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    browser.close()

控制台输入如下:

python 复制代码
{'count': 100, 'results': [{'id': 1, 'name': '霸王别姬', 'alias': 'Farewell My Concubine', 'cover': 'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c', 'categories': ['剧情', '爱情'], 'published_at': '1993-07-26', 'minute': 171, 'score': 9.5, 'regions': ['中国大陆', '中国香港']},
...
'published_at': None, 'minute': 103, 'score': 9.0, 'regions': ['美国']}, {'id': 10, 'name': '狮子王', 'alias': 'The Lion King', 'cover': 'https://p0.meituan.net/movie/27b76fe6cf3903f3d74963f70786001e1438406.jpg@464w_644h_1e_1c', 'categories': ['动画', '歌舞', '冒险'], 'published_at': '1995-07-15', 'minute': 89, 'score': 9.0, 'regions': ['美国']}]}

简直是得来全不费工夫,我们直接通过这个方法拦截了 Ajax 请求,直接把响应结果拿到了,即使这个 Ajax 请求有加密参数,我们也不用关心,因为我们直接截获了 Ajax 最后响应的结果,这对数据爬取来说实在是太方便了。

另外还有很多其他的事件监听,这里不再一一介绍了,可以查阅官方文档,参考类似的写法实现。


8.网络劫持(内容来自崔庆才老师)

最后再介绍一个实用的方法 route,利用 route 方法,我们可以实现一些网络劫持和修改操作,比如修改 request 的属性,修改 response 响应结果等。

看一个实例:

python 复制代码
from playwright.sync_api import sync_playwright
import re

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()

    def cancel_request(route, request):
        route.abort()

    page.route(re.compile(r"(\.png)|(\.jpg)"), cancel_request)
    page.goto("https://spa6.scrape.center/")
    page.wait_for_load_state('networkidle')
    page.screenshot(path='no_picture.png')
    browser.close()

这里我们调用了 route 方法,第一个参数通过正则表达式传入了匹配的 URL 路径,这里代表的是任何包含 .png.jpg 的链接,遇到这样的请求,会回调 cancel_request 方法处理,cancel_request 方法可以接收两个参数,一个是 route,代表一个 CallableRoute 对象,另外一个是 request,代表 Request 对象。这里我们直接调用了 route 的 abort 方法,取消了这次请求,所以最终导致的结果就是图片的加载全部取消了。

观察下运行结果,如图所示:

可以看到图片全都加载失败了。

这个设置有什么用呢?其实是有用的,因为图片资源都是二进制文件,而我们在做爬取过程中可能并不想关心其具体的二进制文件的内容,可能只关心图片的 URL 是什么,所以在浏览器中是否把图片加载出来就不重要了。所以如此设置之后,我们可以提高整个页面的加载速度,提高爬取效率。

另外,利用这个功能,我们还可以将一些响应内容进行修改,比如直接修改 Response 的结果为自定义的文本文件内容。

首先这里定义一个 HTML 文本文件,命名为 custom_response.html,内容如下:

html 复制代码
<!DOCTYPE html>
<html>
  <head>
    <title>Hack Response</title>
  </head>
  <body>
    <h1>Hack Response</h1>
  </body>
</html>

代码编写如下:

python 复制代码
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()

    def modify_response(route, request):
        route.fulfill(path="./custom_response.html")

    page.route('/', modify_response)
    page.goto("https://spa6.scrape.center/")
    browser.close()

这里我们使用 route 的 fulfill 方法指定了一个本地文件,就是刚才我们定义的 HTML 文件,运行结果如下:

可以看到,Response 的运行结果就被我们修改了,URL 还是不变的,但是结果已经成了我们修改的 HTML 代码。

所以通过 route 方法,我们可以灵活地控制请求和响应的内容,从而在某些场景下达成某些目的。


8.页面下滑

滚动到底(页面底部)

执行 JavaScript

python 复制代码
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")

这个语句会立刻把页面滚动到最底部。


滚动到某个元素

python 复制代码
await page.locator('#footer').scroll_into_view_if_needed()

scroll_into_view_if_needed() 是 Playwright 的高级封装,会自动判断元素是否在视口内。


模拟鼠标滚轮滚动

如果你想模拟"人手动下滑"的过程,可以用鼠标滚轮(playwright 的 mouse.wheel()):

python 复制代码
await page.mouse.wheel(0, 1000)  # 横向滚动为 0,纵向滚动 1000 像素

你可以循环调用,实现「逐步加载」。


自动滑动加载全内容(适用于瀑布流网站)

比如抓取 B 站、微博这类瀑布流页面,可以模拟"滑到底,加载,再滑..."的逻辑:

python 复制代码
async def auto_scroll(page):
    await page.evaluate("""
        async () => {
            await new Promise((resolve) => {
                let totalHeight = 0;
                const distance = 200;
                const timer = setInterval(() => {
                    const scrollHeight = document.body.scrollHeight;
                    window.scrollBy(0, distance);
                    totalHeight += distance;

                    if (totalHeight >= scrollHeight){
                        clearInterval(timer);
                        resolve();
                    }
                }, 100);
            });
        }
    """)

然后这样调用它:

python 复制代码
await auto_scroll(page)

滚动一个特定的容器(非整个页面)

如果滚动的不是整个页面,而是某个滚动区域(比如带滚条的 <div>):

python 复制代码
await page.evaluate('''() => {
    const scrollable = document.querySelector('.scroll-box');
    scrollable.scrollTop = scrollable.scrollHeight;
}''')

总结
场景 推荐方法
直接滚动到底部 evaluate("scrollTo(...)")
滚动到特定元素 locator(...).scroll_into_view_if_needed()
模拟用户滚轮操作 page.mouse.wheel()
自动滚动加载内容(瀑布流) 自定义 JS 脚本 + evaluate()
滚动容器而非页面 通过 JS 精准选择容器滚动

补充

3)-6,3)-7中讲解了事件监听和网络劫持两种方法,在使用过程中需要注意一个地方,要在await browser.close()前添加await asyncio.sleep(2000),这样才能保证事件监听和网络劫持的持久抓取,不然容易出现页面内容更新,但是无法捕获新信息。比如将await asyncio.sleep(2000)替换为input('输入回车结束任务')来避免直接关闭浏览器,这样的写法就无法保证任务监听和网络劫持的持久抓取。

python 复制代码
        page = await context.new_page()
        page.on("response", on_response)
				await page.route(re.compile(r"(\.png)|(\.jpg)"), cancel_request)
				"""
				其他逻辑的具体实现
				"""
        await asyncio.sleep(2000)
        await browser.close()

结语

​ 在深入学习了Playwright之后,你会发现它几乎能够应对各种复杂的爬虫任务。尽管它在速度上可能不如直接通过API接口获取数据那样快速,但在当今网络环境下,大多数网站都设置了各种反爬机制和加密参数,想要通过代码直接处理这些难题往往会变得异常繁琐。而Playwright凭借其强大的自动化功能以及对部分人工操作的支持,能够让你以一种简单高效的方式完成爬虫任务,轻松绕过那些复杂的反爬限制。

​ 本文源码: Python爬虫之路 https://github.com/rosyrain/spider-course lesson14 中。欢迎各位Follow/Star/Fork ( •̀ ω •́ )✧


有任何问题欢迎大家的评论和指正。再次声明,本专栏只做技术探讨,严谨商用,恶意攻击等。

这是我的 GitHub 主页:Rosyrain (github.com) https://github.com/rosyrain,里面有一些我学习时候的笔记或者代码。本专栏的文档和源码存到spider-course的仓库下。

欢迎大家Follow/Star/Fork三连。

参考文献

相关推荐
成功人chen某40 分钟前
配置VScodePython环境Python was not found;
开发语言·python
2301_786964361 小时前
EXCEL Python 实现绘制柱状线型组合图和树状图(包含数据透视表)
python·microsoft·excel
skd89991 小时前
小蜗牛拨号助手用户使用手册
python
小生凡一1 小时前
搜索引擎工作原理|倒排索引|query改写|CTR点击率预估|爬虫
爬虫·搜索引擎
「QT(C++)开发工程师」2 小时前
STM32 | FreeRTOS 递归信号量
python·stm32·嵌入式硬件
CodeJourney.2 小时前
基于MATLAB的生物量数据拟合模型研究
人工智能·爬虫·算法·matlab·信息可视化
史迪仔01122 小时前
[python] Python单例模式:__new__与线程安全解析
开发语言·python·单例模式
胡耀超2 小时前
18.自动化生成知识图谱的多维度质量评估方法论
人工智能·python·自动化·知识图谱·数据科学·逻辑学·质量评估
一只专注api接口开发的技术猿2 小时前
企业级电商数据对接:1688 商品详情 API 接口开发与优化实践
大数据·前端·爬虫
三块钱07942 小时前
【原创】基于视觉大模型gemma-3-4b实现短视频自动识别内容并生成解说文案
开发语言·python·音视频