前军教程网

中小站长与DIV+CSS网页布局开发技术人员的首选CSS学习平台

Playwright现代爬虫:多浏览器到自动等待 - 让爬虫开发丝滑起来!

嘿,大家好!还在为爬虫开发中各种浏览器兼容性问题、烦人的等待代码、动态加载内容搞得头大吗?

今天,咱们聊一个爬虫界的”新秀”——Playwright!这家伙由微软出品,目标就是让你摆脱过去的痛苦,体验现代化的Web自动化。

想象一下,一个工具就能搞定 Chrome、Firefox、Safari 三大主流浏览器,写一次代码,到处运行,还自带”智能等待”,让你告别 time.sleep() 的尴尬。是不是有点小激动?

这就是 Playwright!它不仅是个测试工具,更是我们爬虫工程师手里的一把利器。它能帮你轻松搞定那些用传统方法难啃的网站,尤其是那些大量使用 JavaScript 动态渲染的页面。

别急,咱们一步步来,看看 Playwright 到底有多”香”!

一、告别选择困难症:Playwright 是什么,为什么选它?

简单说,Playwright 就是一个能让你用 Python(当然也支持其他语言)代码控制浏览器的库。

但它和老牌的 Selenium 有啥不一样?优势在哪?

  • 跨浏览器,真香!:一套代码,通吃 Chromium (Chrome/Edge)、Firefox、WebKit (Safari)。再也不用为不同浏览器写不同的适配代码了。
  • 自动等待,省心!:这是 Playwright 的大杀器!它能自动等待元素加载完成、变得可交互,极大减少了我们手动写各种 wait 的麻烦。代码更干净,运行更稳定!
  • 原生异步,快!:基于 Python 的 asyncio,天生适合高并发的网络请求,性能杠杠的。
  • 网络拦截,强!:可以直接拦截、修改甚至”伪造”网络请求和响应。想屏蔽广告、图片加速加载?或者直接抓取API数据?小菜一碟!
  • 环境隔离,稳!:BrowserContext 就像浏览器的”无痕模式”,可以创建完全独立的环境,互不干扰,非常适合多任务并发或模拟不同用户。

听起来不错?那咱们得先把它”请”到电脑里。

安装很简单:

pip install playwright
playwright install # 这会把 Chromium, Firefox, WebKit 的驱动都装上
# 或者只想装 Chrome? playwright install chrome

装好了?那我们先来理解几个它的核心”零件”。


看图说话:

1. Playwright: 整个库的入口

2. BrowserType: 指定你想用哪个浏览器内核(Chromium, Firefox, WebKit)

3. Browser: 一个浏览器实例,比如你打开的一个 Chrome 窗口

4. BrowserContext: 一个独立的浏览器环境,包含自己的 Cookie、缓存等。可以开多个 Context

5. Page: 就是浏览器里的一个标签页,我们主要的交互对象

理解了这些,我们就可以开始和网页”互动”了。

二、上手实操:让浏览器听你的话!

Playwright 提供了同步和异步两种玩法。简单脚本用同步也行,但要想发挥它的真正威力,或者在复杂的项目里用,强烈推荐 异步(asyncio)

下面我们用异步的方式跑个简单的例子,打开网页看看标题:

import asyncio
from playwright.async_api import async_playwright
async def run():
async with async_playwright() as p:
# 启动 Chromium 浏览器,headless=False 表示显示浏览器界面
browser = await p.chromium.launch(headless=False)
# 创建一个浏览器上下文
context = await browser.new_context()
# 在上下文中打开一个新页面
page = await context.new_page()
# 访问目标网址
await page.goto("https://www.someone.com/") # 
# 获取页面标题并打印
print(f"页面标题是: {await page.title()}")
# 关闭浏览器
await browser.close()
if __name__ == "__main__":
asyncio.run(run())

是不是很简单?async with 帮你自动管理资源,await 处理异步操作。

当然,光打开网页还不够,我们得能找到页面上的元素,然后点点点、填填填。

这就轮到 Playwright 的另一个核心——Locator(定位器)出场了!

定位元素:快准狠找到目标

Locator 是 Playwright 定位元素的”法宝”,它不仅支持常见的 CSS 选择器、XPath,还有更语义化的方式:

# CSS 选择器 (最常用)
button = page.locator("button#submit-button")
inputs = page.locator(".form-input")

# XPath
links = page.locator("//a[@class='nav-link']")

# 按角色(更推荐,更稳定)
login_button = page.get_by_role("button", name="登录") # 找到文本是"登录"的按钮

# 按文本内容
welcome_message = page.get_by_text("欢迎来到")

# 按输入框的标签文字
password_input = page.get_by_label("密码")

# 按输入框的占位符提示
username_input = page.get_by_placeholder("请输入用户名")

找到元素后,交互就简单了:

# 点击
await login_button.click()
# 输入文本 (推荐用 fill,它会先清空再输入)
await username_input.fill("我的用户名")
await password_input.fill("我的密码123")
# 模拟按键
await password_input.press("Enter") # 输完密码按回车
# 处理复选框
await page.locator("#agree-terms").check()
# await page.locator("#agree-terms").uncheck()
# 处理下拉框
await page.locator("#select-city").select_option("beijing") # 选择 value="beijing" 的选项
# 滚动到元素可见
await page.locator(".footer").scroll_into_view_if_needed()

重点来了!你发现没,我们好像没写任何 wait 代码?

三、智能等待:告别 time.sleep() 的艺术

这绝对是 Playwright 最让人舒服的地方!

以前用 Selenium,是不是经常要写

 WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "myDynamicElement"))) 

还得小心翼翼调整等待时间?

Playwright 说:大部分情况,你不用管!

当你调用 locator.click()、locator.fill() 这些交互动作时,Playwright 会 自动 等待:

  1. 元素出现在 DOM 中。
  2. 元素变得可见(没有被遮挡)。
  3. 元素变得稳定(没有动画或位置变化)。
  4. 元素可以接收事件(不是 disabled 状态)。

这一切都是内置的,默认开启!你的代码会变得非常清爽。


当然,自动等待不是万能的。有些特殊情况,我们还是需要 显式等待

  • 等待某个元素满足特定状态:比如,等待一个提示框消失。
# 等待元素从 DOM 中消失 (最多等 5 秒)
await page.locator("#loading-spinner").wait_for(state="hidden", timeout=5000)
  • 等待页面加载状态:等所有网络请求都差不多结束了。
# 等待到网络空闲状态
await page.wait_for_load_state("networkidle")
  • 等待特定选择器出现:比 locator.wait_for 更直接。
await page.wait_for_selector("#results-table", state="visible")
  • 硬等待(不推荐):实在没办法了才用。
await page.wait_for_timeout(3000) # 等待 3 秒
  • 等待页面触发特定事件:比如等待一个下载开始。
async with page.expect_download() as download_info:
await page.locator("#download-button").click() # 点击下载按钮
download = await download_info.value
await download.save_as("downloaded_file.zip")
print(f"文件已下载: {download.path()}")

理解自动等待和显式等待的界限,就能写出既简洁又健壮的爬虫代码。

四、网络魔术师:拦截请求,为所欲为

想不想在浏览器加载网页的时候”动动手脚”?比如:

  • 屏蔽烦人的广告图片、CSS 文件,让爬虫只关注数据,跑得飞快?
  • 直接拦截 API 请求,拿到干净的 JSON 数据,省去解析 HTML 的麻烦?
  • 模拟服务器返回错误,测试你的爬虫错误处理逻辑?

Playwright 的网络拦截功能 (page.route) 让这一切成为可能!

基本用法:告诉 Playwright 你想拦截哪些 URL,以及怎么处理。

import re # 需要导入正则表达式模块

# ... 在 page 创建之后 ...

# 定义一个处理函数
async def handle_route(route, request):
    # 如果是图片或 CSS 文件,直接阻止加载
    if request.resource_type in ["image", "stylesheet", "font"]:
        print(f"已阻止加载: {request.url}")
        await route.abort()
    # 如果是某个特定的 API 请求
    elif "api/get_user_data" in request.url:
        print(f"拦截到用户数据 API: {request.url}")
        # 可以选择伪造一个响应
        mock_response = {"user_id": 123, "name": "我是伪造数据", "is_vip": True}
        await route.fulfill(
            status=200,
            content_type="application/json",
            body=json.dumps(mock_response) # 需要 import json
        )
        # 或者修改请求头后继续发送
        # headers = {**request.headers, "X-My-Token": "mysecret"}
        # await route.continue_(headers=headers)
    else:
        # 其他请求正常放行
        await route.continue_()

# 设置拦截规则,这里用正则表达式匹配所有 URL
await page.route("**/*", handle_route) # **/* 匹配所有请求

# 现在当你调用 page.goto() 或页面内部发起请求时,handle_route 就会被触发
print("开始加载页面,网络拦截已启用...")
await page.goto("https://example.com") # 访问的页面会受到拦截规则影响

# 不需要拦截了,可以取消
# await page.unroute("**/*", handle_route)


Playwright网络请求拦截处理流程


这个功能非常强大,尤其是在爬取那些前后端分离、依赖大量 API 获取数据的网站时,可以直接跳过渲染,效率极高!

五、实战演练:搞定动态加载的论坛回帖

光说不练假把式!咱们来模拟一个常见的场景:爬取一个需要登录、并且回帖是往下滚动动态加载出来的论坛。

目标:

  1. 自动登录论坛。
  2. 访问指定帖子。
  3. 屏蔽图片/CSS加快速度。
  4. 模拟滚动,加载所有回帖。
  5. 提取帖子标题和所有回帖内容。



import asyncio
from playwright.async_api import async_playwright, expect
import re
import json # 用于处理可能的 JSON 响应或伪造数据

async def scrape_forum_post():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False) # 显示浏览器界面方便观察
        context = await browser.new_context(
            # 伪装一下 User Agent,更像真人
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36"
        )
        page = await context.new_page()

        forum_url = "https://example-forum.com" # 【注意】替换成你的目标论坛 URL
        login_url = f"{forum_url}/login"
        target_post_url = f"{forum_url}/post/12345" # 【注意】替换成你要爬的帖子 URL
        username = "your_real_username" # 【注意】替换成你的真实用户名
        password = "your_real_password" # 【注意】替换成你的真实密码

        try:
            # --- 步骤 1: 登录 ---
            print("尝试登录...")
            await page.goto(login_url)
            await page.locator("#username").fill(username)
            await page.locator("#password").fill(password)
            await page.locator("button[type='submit']").click()

            # 等待登录成功(比如,检查用户名是否出现在页面右上角)
            await expect(page.locator("#user-profile-link")).to_be_visible(timeout=10000)
            print("登录成功!")

            # --- 步骤 2: 拦截非必要请求 ---
            async def block_images_css(route, request):
                if request.resource_type in ["image", "stylesheet", "font"]:
                    await route.abort()
                else:
                    await route.continue_()
            await page.route(re.compile(r"\.(css|png|jpg|jpeg|gif|svg)(\?.*)?$"), block_images_css)
            print("已启用图片/CSS拦截。")

            # --- 步骤 3: 访问目标帖子 ---
            print(f"正在访问帖子: {target_post_url}")
            await page.goto(target_post_url, wait_until="domcontentloaded") # 等 DOM 加载完就行,资源会被拦截

            # --- 步骤 4: 动态加载回帖 ---
            print("开始加载回帖...")
            load_more_selector = "button.load-more-replies" # 【注意】替换成实际的"加载更多"按钮选择器
            scroll_attempts = 0
            max_scroll_attempts = 10 # 设置一个最大尝试次数,防止无限循环

            while scroll_attempts < max_scroll_attempts: try: load_more_button='page.locator(load_more_selector)' await load_more_button.wait_forstate='visible' timeout='3000)' await load_more_button.click print await page.wait_for_timeout1500 scroll_attempts except exception as e: timeouterror printf: e break else: printf max_scroll_attempts --- 5: --- print... post_title_element='page.locator("h1.post-title")' post_title='await' post_title_element.text_content if await post_title_element.count> 0 else "标题未找到"

            replies_data = []
            reply_item_selector = "div.reply-item" # 【注意】替换成实际的单个回帖容器选择器
            reply_locators = page.locator(reply_item_selector)
            reply_count = await reply_locators.count()
            print(f"找到 {reply_count} 条回帖。")

            for i in range(reply_count):
                reply = reply_locators.nth(i)
                try:
                    # 【注意】替换成实际的回帖作者、内容、时间戳选择器
                    user_locator = reply.locator(".reply-author a")
                    content_locator = reply.locator(".reply-content p")
                    time_locator = reply.locator(".reply-timestamp")

                    user = await user_locator.text_content() if await user_locator.count() > 0 else "匿名"
                    content = await content_locator.inner_text() if await content_locator.count() > 0 else "" # inner_text 保留换行
                    timestamp = await time_locator.get_attribute("title") if await time_locator.count() > 0 else "" # 假设时间在 title 属性

                    replies_data.append({
                        'user': user.strip(),
                        'content': content.strip(),
                        'timestamp': timestamp.strip()
                    })
                except Exception as e:
                    print(f"提取第 {i+1} 条回帖时出错: {e}")

            print("\n--- 提取结果 ---")
            print(f"标题: {post_title.strip()}")
            print(f"共提取 {len(replies_data)} 条回帖。")
            # 简单打印前几条看看
            for idx, r in enumerate(replies_data[:3]):
                print(f"  回帖 {idx+1}: {r['user']} @ {r['timestamp']} 说: {r['content'][:50]}...") # 截断内容

        except Exception as e:
            print(f"发生严重错误: {e}")
            # 出错了截个图看看当时页面啥情况
            await page.screenshot(path="error_screenshot.png", full_page=True)
            print("错误截图已保存为 error_screenshot.png")
        finally:
            print("关闭浏览器...")
            await browser.close()

if __name__ == "__main__":
    # 在实际运行前,确保替换上面【注意】标记的地方
    # asyncio.run(scrape_forum_post())
    print("请确保已替换代码中【注意】标记的 URL、选择器和登录凭据后再运行!")

案例总结:

这个例子展示了 Playwright 的综合能力:登录、拦截、动态加载处理、数据提取。关键在于:

  1. 精准定位:找到正确的登录框、按钮、回帖元素的选择器是成功的基石。
  2. 耐心等待:虽然 Playwright 能自动等待,但像登录跳转、动态加载完成这类逻辑,还是需要 expect 或 wait_for 来确保流程正确。
  3. 错误处理:try...except...finally 是必须的,保证出错时能知道原因,并优雅关闭浏览器。
  4. 实事求是:代码里的选择器 (#username, .load-more-replies 等) 需要你根据实际目标网站的 HTML 结构去替换。

六、不止于此:Playwright 还能玩出什么花样?

Playwright 的能耐远不止上面这些:

  • 设备模拟:想看看你的爬虫在 iPhone 或 Android 上表现如何?一行代码搞定!
context = await browser.new_context(**p.devices['iPhone 13 Pro'])
page = await context.new_page()
await page.goto("https://m.toutiao.com") # 打开移动版头条
await page.screenshot(path="iphone_toutiao.png")
  • 多页面/多环境管理:BrowserContext 是个好东西!你可以开多个独立的上下文,模拟多个用户同时在线,或者一个爬商品,一个爬评论,互不影响。

  • 截图与录屏:page.screenshot() 可以截取整个页面或指定元素。甚至可以录制操作视频,方便调试找问题。
# 录制操作过程
await context.tracing.start(screenshots=True, snapshots=True, sources=True)
# ... 执行你的爬虫操作 ...
await context.tracing.stop(path = "trace.zip")
# 然后在命令行运行 playwright show-trace trace.zip 查看详细过程
  • 文件上传下载:处理需要上传文件或下载文件的场景也相对简单。
  • 集成其他框架:可以和 Scrapy、FastAPI 等异步框架无缝集成,构建更强大的爬虫系统或自动化工具。

避坑指南:

  • 安装问题:playwright install 有时会因为网络问题失败。可以尝试设置代理,或者去官网手动下载浏览器驱动放到指定位置。
  • 同步 vs 异步:新手可能会混淆。记住,用了 async def,里面就要用 await;如果不用 async def,就用 Playwright 的同步 API (from playwright.sync_api import sync_playwright)。
  • 被检测风险:虽然 Playwright 比 Selenium 隐藏得好一些,但也不是完全无法检测。对于反爬严格的网站,还是需要结合更换 User-Agent、设置代理 IP、处理 Cookie 等常规手段。

七、总结:拥抱 Playwright,爬虫开发起飞!

总的来说,Playwright 绝对是现代 Web 爬虫和自动化开发的一大利器。

它的 跨浏览器支持、强大的自动等待、原生异步性能、灵活的网络拦截,以及不断完善的 设备模拟、多上下文管理 等功能,让开发者能够更高效、更稳定地应对越来越复杂的 Web 环境。

如果你还在被老旧的爬虫工具折腾,或者你的目标网站大量使用 JS 动态渲染,那么强烈建议你试试 Playwright!它可能会让你感叹:“原来爬虫还可以这么写!”

当然,没有任何工具是完美的。掌握 Playwright 的同时,也要了解它的局限性,并结合其他反反爬策略,才能在爬虫的道路上走得更远。

好了,今天关于 Playwright 的分享就到这里。希望这个”新朋友”能帮你在自动化和数据采集的道路上,少走弯路,多点效率!

你用过 Playwright 吗?或者有什么爬虫难题?欢迎在评论区留言交流!

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言