1. 项目概述为什么我们需要 Playwriter如果你用过 Playwright肯定会被它强大的跨浏览器自动化能力所折服。但说实话它的原生 Python API 有时候用起来感觉有点“重”。一个简单的点击操作你可能需要写好几行代码来处理等待、定位和异常。对于快速脚本、数据抓取或者日常的自动化任务我们往往希望代码能更简洁、更直观最好能像写自然语言一样流畅。这就是Playwriter诞生的背景。它不是一个全新的底层驱动而是一个构建在 Playwright 之上的 Python 封装库。它的核心目标只有一个简化 API 设计。你可以把它理解为 Playwright 的“语法糖”或“友好层”。它保留了 Playwright 所有强大的内核功能——无头模式、多浏览器支持、网络拦截、设备模拟等——但通过更人性化的接口让你用更少的代码完成更多的工作。举个例子在原生 Playwright 里你可能需要这样写from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() page.goto(https://example.com) element page.locator(button.submit) element.wait_for(statevisible) element.click() browser.close()而使用 Playwriter理想情况下你可能只需要from playwriter import Browser with Browser() as browser: browser.open(https://example.com) browser.click(button.submit)看到区别了吗后者更接近我们大脑思考任务的顺序“打开浏览器 - 访问网址 - 点击按钮”。Playwriter 致力于消除那些模板化的、重复的代码让你专注于业务逻辑本身。它特别适合那些希望快速上手浏览器自动化但又不想被复杂 API 细节困扰的开发者、测试工程师和数据分析师。1.1 核心需求解析谁需要它Playwriter 主要服务于以下几类人群自动化测试工程师需要编写大量 UI 测试用例希望提升脚本编写效率和可读性减少维护成本。数据爬虫开发者面对复杂的、JavaScript 渲染的现代网页需要一个稳定可靠的浏览器环境来获取数据但又不希望爬虫代码过于冗长。RPA机器人流程自动化开发者需要模拟用户在浏览器中的操作来完成重复性办公任务如数据录入、报表下载等追求开发速度和脚本的健壮性。运维和 DevOps 工程师需要自动化执行一些 Web 端的配置、监控或检查任务。初学者和爱好者想学习浏览器自动化但被 Playwright 或 Selenium 相对陡峭的学习曲线劝退。Playwriter 提供了一个更平缓的入门路径。它的核心价值在于降低认知负担和开发成本。你不需要时刻记住page.locator(...).wait_for(...).click()这样的链式调用也不需要手动处理很多底层的异步逻辑如果你选择同步模式。Playwriter 在背后帮你处理了这些“脏活累活”。1.2 Playwriter 与 Playwright 的核心区别很多人会混淆这两者。简单来说Playwright 是引擎Playwriter 是更好用的方向盘和座椅。特性维度Playwright (原生)Playwriter (封装库)定位微软官方维护的底层浏览器自动化库。社区或个人基于 Playwright 开发的高级封装库。API 风格提供底层、精细的控制API 更接近浏览器原始操作。提供高层、简化的 API更接近自然语言和常见任务场景。学习曲线相对较陡需要理解浏览器上下文、Frame、Locator 等概念。更平缓上手快对新手友好。代码量相对较多需要更多样板代码来处理等待、异常等。显著减少通过预设的智能等待和组合操作简化代码。灵活性极高可以精细控制每一个步骤和参数。较高但可能对某些极端定制化场景有局限通常可通过“逃生舱口”访问底层 API。适用场景复杂的 E2E 测试框架、需要极致性能和控制力的自动化项目。快速脚本、数据抓取、简单的流程自动化、原型验证、新手学习。依赖关系不依赖其他高级封装库。完全依赖 Playwright是对其功能的增强和包装。注意目前根据我的知识截止日期“Playwriter” 这个名称可能更多是一个概念或社区项目的代称并非一个广为人知的、成熟的 PyPI 包。本文将以这个概念为基础探讨如何设计和使用这样一个库。如果你在 PyPI 上找不到它完全可以参考本文的思路自己动手封装一个。2. Playwriter 的核心设计哲学与架构要理解 Playwriter 怎么用最好先了解它被设计成什么样。它的核心思想是“约定优于配置”和“智能默认值”。2.1 简化 API 的关键设计隐式智能等待这是最大的简化点。在原生 Playwright 中你需要显式调用wait_for_selector,wait_for_load_state等。Playwriter 应该在所有可能发生等待的操作如click,fill,get_text背后自动注入合理的等待逻辑。例如点击前确保元素可点击填充前确保输入框可见且可编辑。链式调用与流畅接口提供类似browser.open(url).find(selector).click()的链式调用让代码读起来像一个连贯的句子。上下文管理器封装自动管理浏览器和页面的生命周期。使用with语句时自动完成启动、关闭和异常情况下的资源清理。统一的定位器策略提供一个更强大的find或locate方法它内部整合了多种定位策略CSS、XPath、文本并尝试智能选择最合适的一个或者提供一个清晰的优先级。减少样板代码自动处理常见的配置如视口大小、User-Agent、忽略 HTTPS 错误等同时允许用户轻松覆盖这些默认值。2.2 架构设想Playwriter 的内部层次一个设计良好的 Playwriter 库可能在内部是这样的结构你的脚本 ↓ Playwriter 高级API (如 Browser, Element) ↓ Playwright 原生API (如 Page, Locator) ↓ 浏览器驱动 (Chromium, Firefox, WebKit)最上层Playwriter提供高度抽象、任务导向的接口。这是你主要交互的层。中间层PlaywrightPlaywriter 的核心依赖负责所有与真实浏览器通信的重任。底层实际的浏览器进程。这种分层意味着 Playwriter 可以捕获你在高级 API 上的调用将其翻译成一系列更底层的 Playwright 操作并在其中插入额外的逻辑如等待、重试、日志记录。2.3 同步与异步的优雅处理Playwright 原生支持同步和异步两种模式。Playwriter 应该对此进行封装提供两种风格的 API或者至少让同步 API 用起来无比简单。同步模式对于大多数脚本和初学者来说同步代码更易于理解和调试。Playwriter 可以默认提供同步接口底层使用playwright.sync_api。# Playwriter 同步模式设想 from playwriter import Browser with Browser() as browser: browser.open(https://github.com) browser.fill(input[nameq], playwright) browser.press(Enter) # 模拟键盘回车 first_result browser.find(a[data-hydro-click*search_result], index0) print(first_result.text)异步模式对于高性能爬虫或需要并发控制多个页面的高级用户Playwriter 也应暴露异步接口可能通过一个单独的模块如playwriter.async_api或一个参数来启用。# Playwriter 异步模式设想 import asyncio from playwriter.async_api import AsyncBrowser async def main(): async with AsyncBrowser() as browser: await browser.open(https://github.com) # ... 其他异步操作 asyncio.run(main())实操心得在设计自己的封装时我建议优先实现同步 API因为它的用户群体最广。异步 API 可以在同步 API 稳定后再作为扩展提供。确保两者在功能上尽可能一致减少用户切换模式时的学习成本。3. Playwriter 核心 API 详解与实操让我们抛开概念看看一个理想的 Playwriter 库应该提供哪些“开箱即用”的功能。我会用假设的 API 来演示你可以将其视为一个设计蓝图或使用指南。3.1 环境安装与初始化假设我们已经通过pip install playwriter安装了这个库再次强调这是一个概念。它的安装必然包含了 Playwright 本身及其浏览器驱动。# 最基本的启动方式使用默认浏览器通常是 Chromium和无头模式 from playwriter import Browser # 方式一使用上下文管理器推荐自动清理资源 with Browser() as browser: page browser.new_page() # 创建一个新页面对象 # ... 你的操作 # 方式二手动管理生命周期 browser Browser() page browser.new_page() # ... 你的操作 browser.close() # 务必手动关闭初始化配置通常我们需要更多的控制。Playwriter 的构造函数应该接受丰富的参数。from playwriter import Browser # 常用配置示例 with Browser( browser_typechromium, # 可选chromium, firefox, webkit headlessFalse, # 显示浏览器窗口便于调试 viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) ..., ignore_https_errorsTrue, # 忽略 HTTPS 证书错误常用于测试环境 slow_mo500, # 将每个操作放慢 500 毫秒方便观察 downloads_path/path/to/downloads # 设置下载目录 ) as browser: # 现在你得到了一个配置好的浏览器实例 pass3.2 页面导航与基本操作这是自动化脚本的起点。with Browser(headlessFalse) as browser: # 打开网页 - 智能等待页面加载完成包括网络空闲 page browser.open(https://www.example.com) # 获取页面标题和URL print(f页面标题: {page.title}) print(f页面URL: {page.url}) # 刷新页面 page.reload() # 后退和前进 page.go_back() page.go_forward() # 截图 - 简化了原生API的参数 page.screenshot(example.png) # 截取可视区域 page.screenshot(example_full.png, full_pageTrue) # 截取整个页面3.3 元素定位与交互的简化这是 Playwriter 的核心价值所在。它应该让定位和操作元素变得极其简单。with Browser(headlessFalse) as browser: page browser.open(https://github.com/login) # 1. 智能定位器一个 find 方法走天下 # 它会自动尝试 CSS 选择器如果失败可能会尝试将其解释为包含该文本的元素 username_input page.find(input[namelogin]) password_input page.find(#password) # 使用 ID 选择器 signin_button page.find(input[typesubmit]) # 2. 简化表单操作 username_input.fill(your_username) password_input.fill(your_password) signin_button.click() # 点击前自动等待元素可点击 # 3. 处理复杂选择器与索引 # 找到第一个类名为 repo 的 h3 元素 first_repo page.find(h3.repo, index0) # 找到所有类名为 repo 的 h3 元素返回列表 all_repos page.find_all(h3.repo) for repo in all_repos: print(repo.text) # 4. 基于文本的定位非常实用 # 点击文本内容为 “Sign up” 的链接或按钮 page.click(textSign up) # 查找包含 “documentation” 文本的元素 doc_link page.find(contains_textdocumentation) # 5. 链式操作与等待组合 # 假设一个按钮点击后才会出现一个下拉菜单 page.find(button.menu-toggle).click().wait_for_visible(ul.dropdown-menu) # 在上面的例子中click() 返回元素自身或页面对象允许链式调用。 # wait_for_visible 是等待某个选择器在页面上可见。定位策略的智能后备一个健壮的find方法内部可能这样工作首先尝试将输入作为 CSS 选择器进行查询。如果未找到尝试将其作为 XPath 表达式如果字符串以//或.//开头。如果还未找到尝试查找包含该文本的元素 (//*[contains(text(), ‘...’)])。可以通过参数strategy‘css’|‘xpath’|‘text’来显式指定策略。3.4 处理弹窗、框架和下拉菜单现代网页充满了这些“拦路虎”。Playwriter 应该让处理它们变得轻松。with Browser(headlessFalse) as browser: page browser.open(https://some-page.com) # 1. 处理 JavaScript 弹窗 (alert, confirm, prompt) # 原生 Playwright 需要监听 dialog 事件Playwriter 可以封装为同步方法 # 假设有一个按钮触发 alert page.on_dialog_accept() # 设置自动接受下一个弹窗 page.find(#trigger-alert).click() # 或者获取弹窗文本 dialog_message page.handle_next_dialog(actionaccept) # 接受并返回消息 print(f弹窗说{dialog_message}) # 2. 处理 iframe # 简化 iframe 的切入和切出 frame page.get_frame(namelogin-frame) # 通过 name 获取 # 或者通过选择器 frame_element page.find(iframe#preview) frame frame_element.content_frame # 获取 iframe 的上下文 with frame: # 使用上下文管理器自动切换回父页面 frame.find(input.username).fill(user) # 操作结束后自动跳出 iframe 上下文 # 3. 处理下拉选择框 (select) # 原生 Playwright 需要创建 Select 对象这里可以一步到位 country_select page.find(select#country) country_select.choose(China) # 通过可见文本选择 country_select.choose(valuecn) # 通过 value 属性选择 country_select.choose(index2) # 通过索引选择 # 4. 文件上传不再是难点 # 原生 Playwright 需要用到 set_input_files且定位的是 input[typefile] page.find(input[typefile]).upload_file(/path/to/your/file.pdf)3.5 键盘、鼠标与等待操作with Browser(headlessFalse) as browser: page browser.open(https://example.com) # 键盘操作 page.find(input.search).fill(keyword).press(Enter) # 填充后按回车 page.press(body, ControlA) # 全选 (CtrlA) page.press(body, ControlC) # 复制 (CtrlC) # 鼠标操作 element page.find(div.draggable) element.hover() # 鼠标悬停 element.drag_to(page.find(div.dropzone)) # 拖放操作 # 高级等待 - Playwriter 应提供更语义化的方法 page.wait_for_url(https://example.com/success) # 等待 URL 变化 page.wait_for_element(div.success-message, statevisible, timeout10000) # 等待元素出现最多10秒 page.wait_for_load_state(networkidle) # 等待网络空闲原生API但很好用 # 显式等待在需要精确控制时使用 import time time.sleep(2) # 不推荐但有时最简单 # 更好的方式是等待某个条件 page.wait_for(lambda: page.find(.result).text 完成, timeout5000)4. 实战用 Playwriter 构建一个网页数据抓取脚本让我们通过一个完整的、贴近实际的例子来看看 Playwriter 如何简化一个常见的任务从电商网站抓取商品列表。任务从某个电商网站搜索“无线耳机”并抓取第一页结果的商品名称、价格和链接。 使用 Playwriter 抓取电商商品数据示例 假设目标网站为 https://demo-shop.example.com from playwriter import Browser import json import csv def scrape_products(search_term无线耳机, max_pages1): 抓取指定关键词的商品数据 all_products [] with Browser( browser_typechromium, headlessTrue, # 无头模式后台运行 viewport{width: 1366, height: 768}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ) as browser: # 1. 打开网站 page browser.open(https://demo-shop.example.com) # 2. 在搜索框输入关键词并搜索 search_box page.find(input.search-field) search_box.fill(search_term) search_box.press(Enter) # 3. 等待结果加载 page.wait_for_element(div.product-list, statevisible) current_page 1 while current_page max_pages: print(f正在抓取第 {current_page} 页...) # 4. 定位所有商品条目 product_items page.find_all(div.product-item) if not product_items: print(未找到商品条目。) break for item in product_items: product {} try: # 使用相对定位在 item 元素内查找子元素 product[name] item.find(h2.product-name).text.strip() product[price] item.find(span.product-price).text.strip() # 获取链接元素的 href 属性 link_element item.find(a.product-link) product[url] link_element.get_attribute(href) if link_element else N/A # 可能还有图片 img_element item.find(img.product-image) product[image] img_element.get_attribute(src) if img_element else N/A all_products.append(product) print(f 已抓取: {product[name]} - {product[price]}) except Exception as e: # 某个商品信息缺失记录日志并继续 print(f 抓取单个商品时出错: {e}) continue # 5. 尝试翻到下一页如果存在且未达到最大页数 if current_page max_pages: next_button page.find(a.next-page, timeout3000) # 给3秒找下一页按钮 if next_button: next_button.click() page.wait_for_load_state(networkidle) # 等待新页面内容加载 current_page 1 else: print(没有下一页了。) break else: break return all_products if __name__ __main__: products scrape_products(无线耳机, max_pages2) # 保存为 JSON with open(products.json, w, encodingutf-8) as f: json.dump(products, f, ensure_asciiFalse, indent2) print(f数据已保存到 products.json共 {len(products)} 条记录。) # 保存为 CSV if products: keys products[0].keys() with open(products.csv, w, newline, encodingutf-8-sig) as f: dict_writer csv.DictWriter(f, fieldnameskeys) dict_writer.writeheader() dict_writer.writerows(products) print(数据已保存到 products.csv)这个脚本展示了 Playwriter 的哪些优势简洁性没有复杂的page.locator(...).wait_for(...)链。定位和等待被融合在find和click等方法中。可读性代码几乎像在描述操作步骤“找到搜索框 - 填充 - 按回车 - 等待列表 - 循环每个商品 - 提取信息”。健壮性find方法内部隐含了等待减少了因元素未加载完成而导致的失败。异常处理也集中在业务逻辑部分。灵活性通过find_all轻松处理列表通过元素句柄的find方法进行相对定位避免了冗长且脆弱的绝对 XPath。实操心得在编写此类抓取脚本时选择器的稳定性至关重要。优先使用id、name或具有唯一性的class。避免使用依赖于页面结构顺序的索引如div:nth-child(3)因为前端微小的改动就可能破坏它。在find失败时一个好的 Playwriter 实现应该能提供清晰的错误信息指出在哪里、用什么选择器失败了。5. 高级特性与性能优化一个成熟的 Playwriter 库不会止步于基础操作。它应该封装一些 Playwright 的高级特性让它们更容易使用。5.1 网络请求拦截与模拟这是 Playwright 的王牌功能之一用于性能测试、屏蔽广告、修改响应或注入数据。from playwriter import Browser import json with Browser() as browser: # 在创建页面前后可以添加请求/响应监听器 page browser.new_page() # 拦截所有图片请求并阻止加载加速页面渲染 page.route(**/*.{png,jpg,jpeg,webp,gif,svg}, lambda route: route.abort()) # 拦截特定 API 请求并返回模拟数据 def mock_api_response(route): if /api/user in route.request.url: mock_data {name: Mock User, id: 123} route.fulfill( status200, content_typeapplication/json, bodyjson.dumps(mock_data) ) else: route.continue_() # 放行其他请求 page.route(**/api/**, mock_api_response) page.goto(https://example.com) # 此时页面加载的图片将被拦截对 /api/user 的请求将获得模拟数据5.2 设备模拟与地理位置轻松模拟移动端访问或特定地理位置。from playwriter import Browser # 模拟 iPhone 12 访问 with Browser( deviceiPhone 12 Pro, # Playwriter 内部映射到 Playwright 的设备列表 headlessFalse ) as browser: page browser.open(https://www.google.com) # 页面将以移动端视图打开 # 模拟特定地理位置和语言 with Browser( localezh-CN, geolocation{longitude: 116.397128, latitude: 39.916527}, # 北京 permissions[geolocation] # 授予地理位置权限 ) as browser: page browser.open(https://maps.example.com) # 网站将获取到中文环境和北京的地理位置5.3 并发与多页面管理对于需要同时处理多个任务的高效爬虫并发是关键。import asyncio from playwriter.async_api import AsyncBrowser async def scrape_single_page(url, browser_context): 在一个浏览器上下文中打开一个页面并执行任务 page await browser_context.new_page() await page.goto(url) title await page.title() await page.close() return {url: url, title: title} async def main(): urls [https://example.com/1, https://example.com/2, https://example.com/3] async with AsyncBrowser() as browser: # 创建一个浏览器上下文可以隔离 cookies、缓存等 context await browser.new_context() tasks [] for url in urls: task asyncio.create_task(scrape_single_page(url, context)) tasks.append(task) results await asyncio.gather(*tasks) for result in results: print(result) # asyncio.run(main())性能提示重用浏览器实例最耗时的操作是启动和关闭浏览器。对于多个任务务必重用同一个Browser实例或BrowserContext。控制并发度不要一次性打开数百个页面这会耗尽内存。使用信号量asyncio.Semaphore或线程池来限制并发数量。使用无头模式生产环境脚本务必使用headlessTrue这能节省大量 GUI 渲染资源。合理设置超时为wait_for_element、goto等操作设置合理的超时时间避免脚本因某个页面加载过慢而无限期卡住。6. 常见问题排查与调试技巧即使使用简化的 Playwriter你依然会遇到问题。以下是常见陷阱和解决思路。6.1 元素定位失败这是最常见的问题。症状ElementNotFoundError或类似的超时错误。排查步骤确认页面已加载在操作前确保使用了page.wait_for_load_state(‘networkidle’)或等待某个特定标志性元素出现。检查选择器打开浏览器的开发者工具F12在 Console 里输入document.querySelector(‘你的选择器’)测试你的 CSS 选择器是否有效。检查元素是否在iframe或shadow DOM内。如果在你需要先切入对应的上下文。元素是否是动态生成的可能需要更长的等待时间或等待特定事件。使用更稳健的定位器优先使用id、>context browser.new_context( extra_http_headers{ Accept-Language: zh-CN,zh;q0.9, Referer: https://www.google.com/ } )使用代理Browser(proxy{‘server’: ‘http://your-proxy:port’})。模拟人类行为添加随机延迟page.wait_for_timeout(random.uniform(1000, 3000))、随机滚动页面、移动鼠标轨迹等。Playwriter 可以封装一些human_like_delay()这样的辅助函数。处理验证码这是一个难题。对于简单图片验证码可以考虑集成第三方 OCR 服务。对于复杂验证码如点选、滑动可能需要人工干预或使用付费打码平台。Playwriter 可以提供一个钩子在检测到验证码时暂停脚本并等待用户手动处理。6.4 资源泄露与浏览器未关闭症状脚本运行后后台浏览器进程没有退出占用内存和 CPU。解决始终使用上下文管理器(with Browser() as browser:)这是最安全的方式。如果必须手动管理确保在try...except...finally块中在finally里调用browser.close()。检查代码逻辑确保在所有可能的退出路径包括异常上都有关闭浏览器的操作。6.5 调试日志一个设计良好的 Playwriter 应该提供详细的日志功能帮助你了解内部正在发生什么。import logging # 设置 Playwright 底层驱动的日志级别 logging.basicConfig(levellogging.INFO) # 或者如果 Playwriter 提供了自己的日志器 from playwriter import set_log_level set_log_level(DEBUG) # 打印出详细的定位、等待、操作信息 with Browser() as browser: # 现在你的操作会输出更多信息 page browser.open(https://example.com)7. 从 Playwriter 思想到自定义封装如果你在 PyPI 上找不到一个叫playwriter的完美库别灰心。本文所描述的其实就是一种对 Playwright 进行面向任务封装的最佳实践。你可以借鉴这些思想为自己或团队创建一套私有的、高度定制的工具函数或类。第一步创建基础包装类# my_playwriter.py from playwright.sync_api import sync_playwright, Page, Locator import logging logger logging.getLogger(__name__) class SmartBrowser: def __init__(self, browser_typechromium, headlessTrue, **launch_kwargs): self.playwright sync_playwright().start() self.browser self.playwright.chromium.launch(headlessheadless, **launch_kwargs) self.context self.browser.new_context() self.page self.context.new_page() logger.info(fBrowser launched (headless{headless})) def open(self, url, wait_untilnetworkidle, timeout30000): 打开页面并智能等待 logger.info(fNavigating to {url}) response self.page.goto(url, wait_untilwait_until, timeouttimeout) return self.page def find(self, selector, timeout10000, statevisible): 查找元素自动等待 logger.debug(fFinding element: {selector}) locator self.page.locator(selector) locator.wait_for(statestate, timeouttimeout) return SmartElement(locator, selector) def find_all(self, selector, timeout5000): 查找所有匹配元素 logger.debug(fFinding all elements: {selector}) self.page.wait_for_selector(selector, stateattached, timeouttimeout) locators self.page.locator(selector).all() return [SmartElement(loc, selector) for loc in locators] def close(self): 关闭浏览器 self.browser.close() self.playwright.stop() logger.info(Browser closed) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() class SmartElement: def __init__(self, locator: Locator, selector: str): self.locator locator self.selector selector def click(self, **click_kwargs): logger.info(fClicking on element: {self.selector}) self.locator.click(**click_kwargs) def fill(self, text, **fill_kwargs): logger.info(fFilling {text} into element: {self.selector}) self.locator.fill(text, **fill_kwargs) property def text(self): return self.locator.text_content() def get_attribute(self, name): return self.locator.get_attribute(name) # 使用示例 if __name__ __main__: logging.basicConfig(levellogging.INFO) with SmartBrowser(headlessFalse) as browser: page browser.open(https://www.python.org) download_link browser.find(a[href/downloads/]) download_link.click() print(fDownload page title: {page.title()})这个简单的SmartBrowser类已经实现了 Playwriter 的核心思想简化初始化、智能等待、更友好的 API。你可以在此基础上根据项目需求不断添加更多功能如处理 iframe、弹窗、文件上传等。最后一点体会工具的价值在于提升效率。无论是使用现成的 Playwriter还是自己封装目的都是让我们从繁琐的底层 API 调用中解放出来更专注于解决实际的业务问题。Playwright 本身已经非常强大而一个好的封装就像给这把利器配上一个称手的刀柄让你用起来更顺手效率倍增。在自动化这条路上不断抽象和优化重复性工作是工程师进阶的必经之路。
Playwriter:简化Playwright API的Python封装库,提升自动化效率
1. 项目概述为什么我们需要 Playwriter如果你用过 Playwright肯定会被它强大的跨浏览器自动化能力所折服。但说实话它的原生 Python API 有时候用起来感觉有点“重”。一个简单的点击操作你可能需要写好几行代码来处理等待、定位和异常。对于快速脚本、数据抓取或者日常的自动化任务我们往往希望代码能更简洁、更直观最好能像写自然语言一样流畅。这就是Playwriter诞生的背景。它不是一个全新的底层驱动而是一个构建在 Playwright 之上的 Python 封装库。它的核心目标只有一个简化 API 设计。你可以把它理解为 Playwright 的“语法糖”或“友好层”。它保留了 Playwright 所有强大的内核功能——无头模式、多浏览器支持、网络拦截、设备模拟等——但通过更人性化的接口让你用更少的代码完成更多的工作。举个例子在原生 Playwright 里你可能需要这样写from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() page.goto(https://example.com) element page.locator(button.submit) element.wait_for(statevisible) element.click() browser.close()而使用 Playwriter理想情况下你可能只需要from playwriter import Browser with Browser() as browser: browser.open(https://example.com) browser.click(button.submit)看到区别了吗后者更接近我们大脑思考任务的顺序“打开浏览器 - 访问网址 - 点击按钮”。Playwriter 致力于消除那些模板化的、重复的代码让你专注于业务逻辑本身。它特别适合那些希望快速上手浏览器自动化但又不想被复杂 API 细节困扰的开发者、测试工程师和数据分析师。1.1 核心需求解析谁需要它Playwriter 主要服务于以下几类人群自动化测试工程师需要编写大量 UI 测试用例希望提升脚本编写效率和可读性减少维护成本。数据爬虫开发者面对复杂的、JavaScript 渲染的现代网页需要一个稳定可靠的浏览器环境来获取数据但又不希望爬虫代码过于冗长。RPA机器人流程自动化开发者需要模拟用户在浏览器中的操作来完成重复性办公任务如数据录入、报表下载等追求开发速度和脚本的健壮性。运维和 DevOps 工程师需要自动化执行一些 Web 端的配置、监控或检查任务。初学者和爱好者想学习浏览器自动化但被 Playwright 或 Selenium 相对陡峭的学习曲线劝退。Playwriter 提供了一个更平缓的入门路径。它的核心价值在于降低认知负担和开发成本。你不需要时刻记住page.locator(...).wait_for(...).click()这样的链式调用也不需要手动处理很多底层的异步逻辑如果你选择同步模式。Playwriter 在背后帮你处理了这些“脏活累活”。1.2 Playwriter 与 Playwright 的核心区别很多人会混淆这两者。简单来说Playwright 是引擎Playwriter 是更好用的方向盘和座椅。特性维度Playwright (原生)Playwriter (封装库)定位微软官方维护的底层浏览器自动化库。社区或个人基于 Playwright 开发的高级封装库。API 风格提供底层、精细的控制API 更接近浏览器原始操作。提供高层、简化的 API更接近自然语言和常见任务场景。学习曲线相对较陡需要理解浏览器上下文、Frame、Locator 等概念。更平缓上手快对新手友好。代码量相对较多需要更多样板代码来处理等待、异常等。显著减少通过预设的智能等待和组合操作简化代码。灵活性极高可以精细控制每一个步骤和参数。较高但可能对某些极端定制化场景有局限通常可通过“逃生舱口”访问底层 API。适用场景复杂的 E2E 测试框架、需要极致性能和控制力的自动化项目。快速脚本、数据抓取、简单的流程自动化、原型验证、新手学习。依赖关系不依赖其他高级封装库。完全依赖 Playwright是对其功能的增强和包装。注意目前根据我的知识截止日期“Playwriter” 这个名称可能更多是一个概念或社区项目的代称并非一个广为人知的、成熟的 PyPI 包。本文将以这个概念为基础探讨如何设计和使用这样一个库。如果你在 PyPI 上找不到它完全可以参考本文的思路自己动手封装一个。2. Playwriter 的核心设计哲学与架构要理解 Playwriter 怎么用最好先了解它被设计成什么样。它的核心思想是“约定优于配置”和“智能默认值”。2.1 简化 API 的关键设计隐式智能等待这是最大的简化点。在原生 Playwright 中你需要显式调用wait_for_selector,wait_for_load_state等。Playwriter 应该在所有可能发生等待的操作如click,fill,get_text背后自动注入合理的等待逻辑。例如点击前确保元素可点击填充前确保输入框可见且可编辑。链式调用与流畅接口提供类似browser.open(url).find(selector).click()的链式调用让代码读起来像一个连贯的句子。上下文管理器封装自动管理浏览器和页面的生命周期。使用with语句时自动完成启动、关闭和异常情况下的资源清理。统一的定位器策略提供一个更强大的find或locate方法它内部整合了多种定位策略CSS、XPath、文本并尝试智能选择最合适的一个或者提供一个清晰的优先级。减少样板代码自动处理常见的配置如视口大小、User-Agent、忽略 HTTPS 错误等同时允许用户轻松覆盖这些默认值。2.2 架构设想Playwriter 的内部层次一个设计良好的 Playwriter 库可能在内部是这样的结构你的脚本 ↓ Playwriter 高级API (如 Browser, Element) ↓ Playwright 原生API (如 Page, Locator) ↓ 浏览器驱动 (Chromium, Firefox, WebKit)最上层Playwriter提供高度抽象、任务导向的接口。这是你主要交互的层。中间层PlaywrightPlaywriter 的核心依赖负责所有与真实浏览器通信的重任。底层实际的浏览器进程。这种分层意味着 Playwriter 可以捕获你在高级 API 上的调用将其翻译成一系列更底层的 Playwright 操作并在其中插入额外的逻辑如等待、重试、日志记录。2.3 同步与异步的优雅处理Playwright 原生支持同步和异步两种模式。Playwriter 应该对此进行封装提供两种风格的 API或者至少让同步 API 用起来无比简单。同步模式对于大多数脚本和初学者来说同步代码更易于理解和调试。Playwriter 可以默认提供同步接口底层使用playwright.sync_api。# Playwriter 同步模式设想 from playwriter import Browser with Browser() as browser: browser.open(https://github.com) browser.fill(input[nameq], playwright) browser.press(Enter) # 模拟键盘回车 first_result browser.find(a[data-hydro-click*search_result], index0) print(first_result.text)异步模式对于高性能爬虫或需要并发控制多个页面的高级用户Playwriter 也应暴露异步接口可能通过一个单独的模块如playwriter.async_api或一个参数来启用。# Playwriter 异步模式设想 import asyncio from playwriter.async_api import AsyncBrowser async def main(): async with AsyncBrowser() as browser: await browser.open(https://github.com) # ... 其他异步操作 asyncio.run(main())实操心得在设计自己的封装时我建议优先实现同步 API因为它的用户群体最广。异步 API 可以在同步 API 稳定后再作为扩展提供。确保两者在功能上尽可能一致减少用户切换模式时的学习成本。3. Playwriter 核心 API 详解与实操让我们抛开概念看看一个理想的 Playwriter 库应该提供哪些“开箱即用”的功能。我会用假设的 API 来演示你可以将其视为一个设计蓝图或使用指南。3.1 环境安装与初始化假设我们已经通过pip install playwriter安装了这个库再次强调这是一个概念。它的安装必然包含了 Playwright 本身及其浏览器驱动。# 最基本的启动方式使用默认浏览器通常是 Chromium和无头模式 from playwriter import Browser # 方式一使用上下文管理器推荐自动清理资源 with Browser() as browser: page browser.new_page() # 创建一个新页面对象 # ... 你的操作 # 方式二手动管理生命周期 browser Browser() page browser.new_page() # ... 你的操作 browser.close() # 务必手动关闭初始化配置通常我们需要更多的控制。Playwriter 的构造函数应该接受丰富的参数。from playwriter import Browser # 常用配置示例 with Browser( browser_typechromium, # 可选chromium, firefox, webkit headlessFalse, # 显示浏览器窗口便于调试 viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) ..., ignore_https_errorsTrue, # 忽略 HTTPS 证书错误常用于测试环境 slow_mo500, # 将每个操作放慢 500 毫秒方便观察 downloads_path/path/to/downloads # 设置下载目录 ) as browser: # 现在你得到了一个配置好的浏览器实例 pass3.2 页面导航与基本操作这是自动化脚本的起点。with Browser(headlessFalse) as browser: # 打开网页 - 智能等待页面加载完成包括网络空闲 page browser.open(https://www.example.com) # 获取页面标题和URL print(f页面标题: {page.title}) print(f页面URL: {page.url}) # 刷新页面 page.reload() # 后退和前进 page.go_back() page.go_forward() # 截图 - 简化了原生API的参数 page.screenshot(example.png) # 截取可视区域 page.screenshot(example_full.png, full_pageTrue) # 截取整个页面3.3 元素定位与交互的简化这是 Playwriter 的核心价值所在。它应该让定位和操作元素变得极其简单。with Browser(headlessFalse) as browser: page browser.open(https://github.com/login) # 1. 智能定位器一个 find 方法走天下 # 它会自动尝试 CSS 选择器如果失败可能会尝试将其解释为包含该文本的元素 username_input page.find(input[namelogin]) password_input page.find(#password) # 使用 ID 选择器 signin_button page.find(input[typesubmit]) # 2. 简化表单操作 username_input.fill(your_username) password_input.fill(your_password) signin_button.click() # 点击前自动等待元素可点击 # 3. 处理复杂选择器与索引 # 找到第一个类名为 repo 的 h3 元素 first_repo page.find(h3.repo, index0) # 找到所有类名为 repo 的 h3 元素返回列表 all_repos page.find_all(h3.repo) for repo in all_repos: print(repo.text) # 4. 基于文本的定位非常实用 # 点击文本内容为 “Sign up” 的链接或按钮 page.click(textSign up) # 查找包含 “documentation” 文本的元素 doc_link page.find(contains_textdocumentation) # 5. 链式操作与等待组合 # 假设一个按钮点击后才会出现一个下拉菜单 page.find(button.menu-toggle).click().wait_for_visible(ul.dropdown-menu) # 在上面的例子中click() 返回元素自身或页面对象允许链式调用。 # wait_for_visible 是等待某个选择器在页面上可见。定位策略的智能后备一个健壮的find方法内部可能这样工作首先尝试将输入作为 CSS 选择器进行查询。如果未找到尝试将其作为 XPath 表达式如果字符串以//或.//开头。如果还未找到尝试查找包含该文本的元素 (//*[contains(text(), ‘...’)])。可以通过参数strategy‘css’|‘xpath’|‘text’来显式指定策略。3.4 处理弹窗、框架和下拉菜单现代网页充满了这些“拦路虎”。Playwriter 应该让处理它们变得轻松。with Browser(headlessFalse) as browser: page browser.open(https://some-page.com) # 1. 处理 JavaScript 弹窗 (alert, confirm, prompt) # 原生 Playwright 需要监听 dialog 事件Playwriter 可以封装为同步方法 # 假设有一个按钮触发 alert page.on_dialog_accept() # 设置自动接受下一个弹窗 page.find(#trigger-alert).click() # 或者获取弹窗文本 dialog_message page.handle_next_dialog(actionaccept) # 接受并返回消息 print(f弹窗说{dialog_message}) # 2. 处理 iframe # 简化 iframe 的切入和切出 frame page.get_frame(namelogin-frame) # 通过 name 获取 # 或者通过选择器 frame_element page.find(iframe#preview) frame frame_element.content_frame # 获取 iframe 的上下文 with frame: # 使用上下文管理器自动切换回父页面 frame.find(input.username).fill(user) # 操作结束后自动跳出 iframe 上下文 # 3. 处理下拉选择框 (select) # 原生 Playwright 需要创建 Select 对象这里可以一步到位 country_select page.find(select#country) country_select.choose(China) # 通过可见文本选择 country_select.choose(valuecn) # 通过 value 属性选择 country_select.choose(index2) # 通过索引选择 # 4. 文件上传不再是难点 # 原生 Playwright 需要用到 set_input_files且定位的是 input[typefile] page.find(input[typefile]).upload_file(/path/to/your/file.pdf)3.5 键盘、鼠标与等待操作with Browser(headlessFalse) as browser: page browser.open(https://example.com) # 键盘操作 page.find(input.search).fill(keyword).press(Enter) # 填充后按回车 page.press(body, ControlA) # 全选 (CtrlA) page.press(body, ControlC) # 复制 (CtrlC) # 鼠标操作 element page.find(div.draggable) element.hover() # 鼠标悬停 element.drag_to(page.find(div.dropzone)) # 拖放操作 # 高级等待 - Playwriter 应提供更语义化的方法 page.wait_for_url(https://example.com/success) # 等待 URL 变化 page.wait_for_element(div.success-message, statevisible, timeout10000) # 等待元素出现最多10秒 page.wait_for_load_state(networkidle) # 等待网络空闲原生API但很好用 # 显式等待在需要精确控制时使用 import time time.sleep(2) # 不推荐但有时最简单 # 更好的方式是等待某个条件 page.wait_for(lambda: page.find(.result).text 完成, timeout5000)4. 实战用 Playwriter 构建一个网页数据抓取脚本让我们通过一个完整的、贴近实际的例子来看看 Playwriter 如何简化一个常见的任务从电商网站抓取商品列表。任务从某个电商网站搜索“无线耳机”并抓取第一页结果的商品名称、价格和链接。 使用 Playwriter 抓取电商商品数据示例 假设目标网站为 https://demo-shop.example.com from playwriter import Browser import json import csv def scrape_products(search_term无线耳机, max_pages1): 抓取指定关键词的商品数据 all_products [] with Browser( browser_typechromium, headlessTrue, # 无头模式后台运行 viewport{width: 1366, height: 768}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ) as browser: # 1. 打开网站 page browser.open(https://demo-shop.example.com) # 2. 在搜索框输入关键词并搜索 search_box page.find(input.search-field) search_box.fill(search_term) search_box.press(Enter) # 3. 等待结果加载 page.wait_for_element(div.product-list, statevisible) current_page 1 while current_page max_pages: print(f正在抓取第 {current_page} 页...) # 4. 定位所有商品条目 product_items page.find_all(div.product-item) if not product_items: print(未找到商品条目。) break for item in product_items: product {} try: # 使用相对定位在 item 元素内查找子元素 product[name] item.find(h2.product-name).text.strip() product[price] item.find(span.product-price).text.strip() # 获取链接元素的 href 属性 link_element item.find(a.product-link) product[url] link_element.get_attribute(href) if link_element else N/A # 可能还有图片 img_element item.find(img.product-image) product[image] img_element.get_attribute(src) if img_element else N/A all_products.append(product) print(f 已抓取: {product[name]} - {product[price]}) except Exception as e: # 某个商品信息缺失记录日志并继续 print(f 抓取单个商品时出错: {e}) continue # 5. 尝试翻到下一页如果存在且未达到最大页数 if current_page max_pages: next_button page.find(a.next-page, timeout3000) # 给3秒找下一页按钮 if next_button: next_button.click() page.wait_for_load_state(networkidle) # 等待新页面内容加载 current_page 1 else: print(没有下一页了。) break else: break return all_products if __name__ __main__: products scrape_products(无线耳机, max_pages2) # 保存为 JSON with open(products.json, w, encodingutf-8) as f: json.dump(products, f, ensure_asciiFalse, indent2) print(f数据已保存到 products.json共 {len(products)} 条记录。) # 保存为 CSV if products: keys products[0].keys() with open(products.csv, w, newline, encodingutf-8-sig) as f: dict_writer csv.DictWriter(f, fieldnameskeys) dict_writer.writeheader() dict_writer.writerows(products) print(数据已保存到 products.csv)这个脚本展示了 Playwriter 的哪些优势简洁性没有复杂的page.locator(...).wait_for(...)链。定位和等待被融合在find和click等方法中。可读性代码几乎像在描述操作步骤“找到搜索框 - 填充 - 按回车 - 等待列表 - 循环每个商品 - 提取信息”。健壮性find方法内部隐含了等待减少了因元素未加载完成而导致的失败。异常处理也集中在业务逻辑部分。灵活性通过find_all轻松处理列表通过元素句柄的find方法进行相对定位避免了冗长且脆弱的绝对 XPath。实操心得在编写此类抓取脚本时选择器的稳定性至关重要。优先使用id、name或具有唯一性的class。避免使用依赖于页面结构顺序的索引如div:nth-child(3)因为前端微小的改动就可能破坏它。在find失败时一个好的 Playwriter 实现应该能提供清晰的错误信息指出在哪里、用什么选择器失败了。5. 高级特性与性能优化一个成熟的 Playwriter 库不会止步于基础操作。它应该封装一些 Playwright 的高级特性让它们更容易使用。5.1 网络请求拦截与模拟这是 Playwright 的王牌功能之一用于性能测试、屏蔽广告、修改响应或注入数据。from playwriter import Browser import json with Browser() as browser: # 在创建页面前后可以添加请求/响应监听器 page browser.new_page() # 拦截所有图片请求并阻止加载加速页面渲染 page.route(**/*.{png,jpg,jpeg,webp,gif,svg}, lambda route: route.abort()) # 拦截特定 API 请求并返回模拟数据 def mock_api_response(route): if /api/user in route.request.url: mock_data {name: Mock User, id: 123} route.fulfill( status200, content_typeapplication/json, bodyjson.dumps(mock_data) ) else: route.continue_() # 放行其他请求 page.route(**/api/**, mock_api_response) page.goto(https://example.com) # 此时页面加载的图片将被拦截对 /api/user 的请求将获得模拟数据5.2 设备模拟与地理位置轻松模拟移动端访问或特定地理位置。from playwriter import Browser # 模拟 iPhone 12 访问 with Browser( deviceiPhone 12 Pro, # Playwriter 内部映射到 Playwright 的设备列表 headlessFalse ) as browser: page browser.open(https://www.google.com) # 页面将以移动端视图打开 # 模拟特定地理位置和语言 with Browser( localezh-CN, geolocation{longitude: 116.397128, latitude: 39.916527}, # 北京 permissions[geolocation] # 授予地理位置权限 ) as browser: page browser.open(https://maps.example.com) # 网站将获取到中文环境和北京的地理位置5.3 并发与多页面管理对于需要同时处理多个任务的高效爬虫并发是关键。import asyncio from playwriter.async_api import AsyncBrowser async def scrape_single_page(url, browser_context): 在一个浏览器上下文中打开一个页面并执行任务 page await browser_context.new_page() await page.goto(url) title await page.title() await page.close() return {url: url, title: title} async def main(): urls [https://example.com/1, https://example.com/2, https://example.com/3] async with AsyncBrowser() as browser: # 创建一个浏览器上下文可以隔离 cookies、缓存等 context await browser.new_context() tasks [] for url in urls: task asyncio.create_task(scrape_single_page(url, context)) tasks.append(task) results await asyncio.gather(*tasks) for result in results: print(result) # asyncio.run(main())性能提示重用浏览器实例最耗时的操作是启动和关闭浏览器。对于多个任务务必重用同一个Browser实例或BrowserContext。控制并发度不要一次性打开数百个页面这会耗尽内存。使用信号量asyncio.Semaphore或线程池来限制并发数量。使用无头模式生产环境脚本务必使用headlessTrue这能节省大量 GUI 渲染资源。合理设置超时为wait_for_element、goto等操作设置合理的超时时间避免脚本因某个页面加载过慢而无限期卡住。6. 常见问题排查与调试技巧即使使用简化的 Playwriter你依然会遇到问题。以下是常见陷阱和解决思路。6.1 元素定位失败这是最常见的问题。症状ElementNotFoundError或类似的超时错误。排查步骤确认页面已加载在操作前确保使用了page.wait_for_load_state(‘networkidle’)或等待某个特定标志性元素出现。检查选择器打开浏览器的开发者工具F12在 Console 里输入document.querySelector(‘你的选择器’)测试你的 CSS 选择器是否有效。检查元素是否在iframe或shadow DOM内。如果在你需要先切入对应的上下文。元素是否是动态生成的可能需要更长的等待时间或等待特定事件。使用更稳健的定位器优先使用id、>context browser.new_context( extra_http_headers{ Accept-Language: zh-CN,zh;q0.9, Referer: https://www.google.com/ } )使用代理Browser(proxy{‘server’: ‘http://your-proxy:port’})。模拟人类行为添加随机延迟page.wait_for_timeout(random.uniform(1000, 3000))、随机滚动页面、移动鼠标轨迹等。Playwriter 可以封装一些human_like_delay()这样的辅助函数。处理验证码这是一个难题。对于简单图片验证码可以考虑集成第三方 OCR 服务。对于复杂验证码如点选、滑动可能需要人工干预或使用付费打码平台。Playwriter 可以提供一个钩子在检测到验证码时暂停脚本并等待用户手动处理。6.4 资源泄露与浏览器未关闭症状脚本运行后后台浏览器进程没有退出占用内存和 CPU。解决始终使用上下文管理器(with Browser() as browser:)这是最安全的方式。如果必须手动管理确保在try...except...finally块中在finally里调用browser.close()。检查代码逻辑确保在所有可能的退出路径包括异常上都有关闭浏览器的操作。6.5 调试日志一个设计良好的 Playwriter 应该提供详细的日志功能帮助你了解内部正在发生什么。import logging # 设置 Playwright 底层驱动的日志级别 logging.basicConfig(levellogging.INFO) # 或者如果 Playwriter 提供了自己的日志器 from playwriter import set_log_level set_log_level(DEBUG) # 打印出详细的定位、等待、操作信息 with Browser() as browser: # 现在你的操作会输出更多信息 page browser.open(https://example.com)7. 从 Playwriter 思想到自定义封装如果你在 PyPI 上找不到一个叫playwriter的完美库别灰心。本文所描述的其实就是一种对 Playwright 进行面向任务封装的最佳实践。你可以借鉴这些思想为自己或团队创建一套私有的、高度定制的工具函数或类。第一步创建基础包装类# my_playwriter.py from playwright.sync_api import sync_playwright, Page, Locator import logging logger logging.getLogger(__name__) class SmartBrowser: def __init__(self, browser_typechromium, headlessTrue, **launch_kwargs): self.playwright sync_playwright().start() self.browser self.playwright.chromium.launch(headlessheadless, **launch_kwargs) self.context self.browser.new_context() self.page self.context.new_page() logger.info(fBrowser launched (headless{headless})) def open(self, url, wait_untilnetworkidle, timeout30000): 打开页面并智能等待 logger.info(fNavigating to {url}) response self.page.goto(url, wait_untilwait_until, timeouttimeout) return self.page def find(self, selector, timeout10000, statevisible): 查找元素自动等待 logger.debug(fFinding element: {selector}) locator self.page.locator(selector) locator.wait_for(statestate, timeouttimeout) return SmartElement(locator, selector) def find_all(self, selector, timeout5000): 查找所有匹配元素 logger.debug(fFinding all elements: {selector}) self.page.wait_for_selector(selector, stateattached, timeouttimeout) locators self.page.locator(selector).all() return [SmartElement(loc, selector) for loc in locators] def close(self): 关闭浏览器 self.browser.close() self.playwright.stop() logger.info(Browser closed) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() class SmartElement: def __init__(self, locator: Locator, selector: str): self.locator locator self.selector selector def click(self, **click_kwargs): logger.info(fClicking on element: {self.selector}) self.locator.click(**click_kwargs) def fill(self, text, **fill_kwargs): logger.info(fFilling {text} into element: {self.selector}) self.locator.fill(text, **fill_kwargs) property def text(self): return self.locator.text_content() def get_attribute(self, name): return self.locator.get_attribute(name) # 使用示例 if __name__ __main__: logging.basicConfig(levellogging.INFO) with SmartBrowser(headlessFalse) as browser: page browser.open(https://www.python.org) download_link browser.find(a[href/downloads/]) download_link.click() print(fDownload page title: {page.title()})这个简单的SmartBrowser类已经实现了 Playwriter 的核心思想简化初始化、智能等待、更友好的 API。你可以在此基础上根据项目需求不断添加更多功能如处理 iframe、弹窗、文件上传等。最后一点体会工具的价值在于提升效率。无论是使用现成的 Playwriter还是自己封装目的都是让我们从繁琐的底层 API 调用中解放出来更专注于解决实际的业务问题。Playwright 本身已经非常强大而一个好的封装就像给这把利器配上一个称手的刀柄让你用起来更顺手效率倍增。在自动化这条路上不断抽象和优化重复性工作是工程师进阶的必经之路。