Playwright持久化上下文实现免登录爬虫:原理、实战与优化
1. 项目概述:为什么我们需要“免登录”爬虫?
做爬虫的朋友,尤其是处理那些需要登录才能访问数据的网站时,最头疼的环节是什么?十有八九会回答:登录状态的维持。传统的爬虫流程,比如用requests库,你得先模拟登录,拿到cookies或session,然后在后续的请求里小心翼翼地带上。这过程麻烦不说,还特别脆弱——网站稍微改一下登录验证逻辑,或者cookies过期了,你的爬虫就立刻罢工,得重新调试登录流程。
更让人心烦的是,很多现代网站采用了复杂的反爬机制,比如动态加载、JavaScript 加密、人机验证(如滑块、点选)等。requests这种基于 HTTP 请求的库,面对这些“花招”常常力不从心。这时候,像Playwright这样的浏览器自动化工具就成了“神器”。它能驱动一个真实的浏览器(如 Chromium, Firefox, WebKit)去访问网页,完美执行所有 JavaScript,就像真人操作一样,轻松绕过这些前端反爬。
但 Playwright 的常规用法,比如browser.new_context()每次都会创建一个全新的、无状态的浏览器上下文,这意味着每次运行脚本都要重新登录。对于需要长时间运行、定时抓取或者处理大量需要登录态页面的爬虫来说,这显然不现实。
于是,launch_persistent_context这个功能的价值就凸显出来了。它允许我们启动一个持久化的浏览器上下文。简单来说,就是给这个浏览器会话分配一个本地目录来存储用户数据(包括 cookies、本地存储、IndexedDB 等)。第一次运行时,你手动登录一次;之后每次运行脚本,它都会从这个目录加载之前的会话状态,自动保持登录,实现真正的“免登录”爬虫。
这不仅仅是省去了模拟登录的代码,更重要的是稳定性和真实性。你用的是浏览器真实的登录态,几乎和你在电脑上手动登录后保持的状态一模一样,极大地降低了被网站识别为爬虫的风险。接下来,我就结合一个实战案例,带你从零开始,手把手实现一个基于launch_persistent_context的免登录爬虫。
2. 核心思路与方案选型:为什么是 Playwright + Persistent Context?
在决定技术方案前,我们先明确一下需求和各个方案的优劣。我们的核心目标是:稳定、高效地爬取需要登录才能访问的数据,并尽可能模拟真人行为以降低被封风险。
2.1 常见方案对比
Requests + Session/Cookies:
- 优点:速度极快,资源消耗低,是传统爬虫的基石。
- 缺点:
- 登录模拟复杂:需要逆向分析登录接口的加密参数(如 token, sign),对于有图形验证码或复杂前端加密的网站难度极大。
- 状态维持脆弱:Cookies 会过期,需要处理刷新逻辑。网站更新接口,爬虫容易失效。
- 无法处理动态内容:对于大量依赖 JavaScript 渲染的页面束手无策。
Selenium:
- 优点:老牌浏览器自动化工具,社区成熟,支持多种语言和浏览器。
- 缺点:
- 速度相对较慢:启动和操作浏览器的开销比 Playwright 和 Puppeteer 大。
- API 设计较旧:等待元素等操作需要写显式等待(WebDriverWait),不如 Playwright 的自动等待优雅。
- 无原生持久化上下文:实现类似功能需要手动处理用户数据目录,配置更繁琐。
Playwright:
- 优点:
- 为现代 Web 设计:由微软团队开发,天生支持单页应用(SPA)、网络拦截、移动端模拟等。
- 强大的自动等待:大部分操作(如
click,fill)内置了等待元素可用的逻辑,代码更简洁健壮。 - 跨浏览器且一致:一套 API 支持 Chromium, Firefox, WebKit,测试和爬虫都很方便。
- 原生支持
launch_persistent_context:这正是我们需要的核心功能,API 简洁直观。 - 速度快:相比 Selenium,通信效率更高。
- 缺点:较新,某些极端场景下的社区资源可能不如 Selenium 丰富(但已足够强大)。
- 优点:
结论:对于需要登录且可能有复杂前端交互的爬虫任务,Playwright凭借其现代的特性、简洁的 API 以及对持久化上下文的原生支持,是目前综合体验最好的选择。launch_persistent_context完美解决了登录态持久化的问题,让我们能专注于数据抓取逻辑本身。
2.2launch_persistent_context工作原理浅析
理解其原理,能帮助我们更好地使用和排查问题。当你调用playwright.chromium.launch_persistent_context(user_data_dir, ...)时:
首次启动(
user_data_dir为空或不存在):- Playwright 会在指定路径
user_data_dir创建一个新的浏览器用户数据目录。 - 启动一个全新的 Chromium 实例,并将其用户数据指向这个目录。
- 此时浏览器上下文是全新的,没有 cookies 和历史记录。你需要在这个上下文里完成登录操作。
- 当你关闭浏览器或上下文时,所有的会话数据(包括登录后的 cookies)会自动保存到
user_data_dir中。
- Playwright 会在指定路径
后续启动(
user_data_dir已存在且有数据):- Playwright 会启动 Chromium,并直接加载
user_data_dir目录下保存的所有用户数据。 - 浏览器打开后,访问目标网站,你会发现已经处于登录状态,因为 cookies 已经被加载。
- 这实现了“免登录”的效果。
- Playwright 会启动 Chromium,并直接加载
重要提示:
user_data_dir这个目录是独占的。你不能同时用两个 Playwright 实例去启动同一个用户数据目录,否则会报错。在爬虫设计中,要做好进程锁或确保单实例运行。
3. 环境搭建与核心依赖安装
工欲善其事,必先利其器。我们先来把 Playwright 的环境搭好。
3.1 创建项目与安装 Playwright
建议使用虚拟环境来管理依赖,避免污染全局 Python 环境。
# 1. 创建项目目录并进入 mkdir persistent-context-spider && cd persistent-context-spider # 2. 创建虚拟环境(以 venv 为例) python -m venv venv # 3. 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 4. 安装 Playwright for Python pip install playwright # 5. 安装 Playwright 所需的浏览器内核(Chromium, Firefox, WebKit) # 通常我们爬虫用 Chromium 就够了,它最兼容。 playwright install chromium注意事项:
playwright install这一步会下载浏览器,可能需要一些时间,取决于你的网络。它默认会下载到 Playwright 的缓存目录,与我们的user_data_dir是分开的。- 如果下载慢,可以考虑设置环境变量
PLAYWRIGHT_DOWNLOAD_HOST为国内镜像源,但请注意,非官方镜像可能存在安全风险,生产环境慎用。
3.2 目录结构规划
一个清晰的项目结构有助于后期维护。我们的项目目录可以这样规划:
persistent-context-spider/ ├── venv/ # Python 虚拟环境(.gitignore) ├── user_data/ # 存放持久化浏览器数据的目录(重要!不上传git) │ └── my_github_session/ # 示例:为不同网站创建不同的子目录 ├── src/ │ ├── __init__.py │ ├── crawler.py # 核心爬虫类 │ └── config.py # 配置文件(如URL、选择器) ├── logs/ # 日志目录 ├── data/ # 爬取的数据存放目录 ├── main.py # 主程序入口 ├── requirements.txt # 依赖列表 └── README.md关键点:user_data/目录必须加入.gitignore,因为它里面包含你的个人登录信息(cookies),上传到公开仓库是严重的安全隐患。
4. 核心代码实战:构建免登录爬虫类
现在,我们来编写核心的爬虫类。我们将以一个需要登录的网站(例如,一个模拟的仪表盘或 GitHub 个人页面)为例,但请注意,实际爬取时应严格遵守网站的robots.txt和服务条款。
4.1 基础爬虫框架搭建
首先,在src/crawler.py中创建一个基础的爬虫类。
import asyncio from pathlib import Path from typing import Optional, Dict, Any import logging from playwright.async_api import async_playwright, BrowserContext, Page class PersistentContextCrawler: """基于持久化上下文的免登录爬虫基类""" def __init__(self, user_data_dir: str, headless: bool = True): """ 初始化爬虫 :param user_data_dir: 用户数据目录路径,用于持久化cookies :param headless: 是否以无头模式运行(True为后台运行,False会打开可见浏览器) """ self.user_data_dir = Path(user_data_dir) self.headless = headless self.context: Optional[BrowserContext] = None self.browser = None self.playwright = None # 确保用户数据目录存在 self.user_data_dir.mkdir(parents=True, exist_ok=True) # 设置日志 self.logger = logging.getLogger(self.__class__.__name__) async def __aenter__(self): """异步上下文管理器入口,用于启动浏览器和上下文""" await self.start() return self async def __aexit__(self, exc_type, exc_val, exc_tb): """异步上下文管理器出口,用于关闭资源""" await self.close() async def start(self): """启动Playwright和持久化上下文""" self.playwright = await async_playwright().start() # 使用 launch_persistent_context 核心API self.context = await self.playwright.chromium.launch_persistent_context( user_data_dir=str(self.user_data_dir), headless=self.headless, # 以下是一些常用且推荐的参数,能更好地模拟普通浏览器 viewport={"width": 1920, "height": 1080}, user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", ignore_https_errors=True, # 忽略HTTPS证书错误,某些内部测试站可能需要 bypass_csp=True, # 绕过内容安全策略,确保脚本能正常运行 # 降低检测风险,可以添加额外的启动参数 args=[ '--disable-blink-features=AutomationControlled', # 隐藏自动化控制标志 '--disable-dev-shm-usage', # 解决Docker等环境下的共享内存问题 '--no-sandbox', # 非绝对必要不建议在非受控环境使用,此处仅为示例 ] ) self.logger.info(f"持久化上下文已启动,用户数据目录: {self.user_data_dir}") async def close(self): """关闭浏览器上下文和Playwright""" if self.context: await self.context.close() self.logger.info("浏览器上下文已关闭") if self.playwright: await self.playwright.stop() self.logger.info("Playwright已停止") async def get_page(self) -> Page: """从持久化上下文中获取一个新的页面(标签页)""" if not self.context: raise RuntimeError("上下文未启动,请先调用 start() 或使用 async with") page = await self.context.new_page() # 可以在这里为页面设置一些默认超时或事件监听 page.set_default_timeout(60000) # 设置默认超时为60秒 return page代码解读与避坑指南:
- 异步编程:Playwright Python 强烈推荐使用
async/await异步 API,性能远高于同步 API。我们的类也设计为异步的。 __aenter__和__aexit__:这是异步上下文管理器。使用async with PersistentContextCrawler(...) as crawler:可以确保无论中间是否发生异常,最后都会自动调用close()方法释放资源,避免浏览器进程残留。launch_persistent_context参数:user_data_dir: 核心参数,必须是字符串路径。headless: 调试时设为False可以看到浏览器操作过程,生产环境设为True节省资源。viewport和user_agent: 设置一个常见的分辨率和 UA,让爬虫更像真人浏览器。args:--disable-blink-features=AutomationControlled是关键,它可以帮助隐藏一些能被网站检测到的自动化特征(但请注意,没有银弹,高级反爬仍可能检测到)。--no-sandbox: 在 Docker 或某些 Linux 无头环境中可能需要,但会降低安全性。如果你的脚本在本地桌面环境运行正常,就不要加这个参数。
- 资源管理:一定要在最后关闭
context和playwright,否则后台会残留浏览器进程,消耗内存。
4.2 实现登录与状态检查方法
虽然我们的目标是“免登录”,但首次运行还是需要登录的。我们添加一个通用的登录方法,并提供一个检查当前是否已登录的辅助方法。
# 在 PersistentContextCrawler 类中继续添加方法 async def is_logged_in(self, check_url: str, logged_in_selector: str) -> bool: """ 检查当前上下文是否已处于登录状态 :param check_url: 用于检查的页面URL(通常是登录后的个人中心、仪表盘等) :param logged_in_selector: 登录成功后在该页面上必定存在的某个元素的选择器 :return: True 表示已登录,False 表示未登录 """ page = await self.get_page() try: self.logger.info(f"正在检查登录状态,访问: {check_url}") # 设置较短的超时,因为如果未登录可能会跳转到登录页或返回错误 page.set_default_timeout(10000) await page.goto(check_url, wait_until="networkidle") # wait_until="networkidle" 等待网络基本空闲 # 等待特定的登录成功标志元素出现 await page.wait_for_selector(logged_in_selector, state="visible", timeout=5000) self.logger.info("登录状态检查:已登录") return True except Exception as e: self.logger.warning(f"登录状态检查:未登录或检查失败。错误: {e}") return False finally: await page.close() async def login_if_needed(self, login_url: str, check_url: str, logged_in_selector: str, login_callback): """ 如果未登录,则执行登录流程 :param login_url: 登录页面的URL :param check_url: 登录状态检查URL(同 is_logged_in) :param logged_in_selector: 登录成功选择器(同 is_logged_in) :param login_callback: 一个异步回调函数,接收 (page: Page) 参数,在该函数内编写具体的登录步骤 """ if await self.is_logged_in(check_url, logged_in_selector): self.logger.info("当前会话已登录,跳过登录流程。") return self.logger.info("未检测到登录状态,开始执行登录流程...") page = await self.get_page() try: await page.goto(login_url, wait_until="networkidle") # 调用用户自定义的登录逻辑 await login_callback(page) # 登录后,等待一小段时间让cookies等状态保存 await asyncio.sleep(2) # 再次检查是否登录成功 if await self.is_logged_in(check_url, logged_in_selector): self.logger.info("登录流程执行完毕,状态已保存。") else: self.logger.error("登录回调函数执行后,仍未检测到登录状态。请检查登录逻辑。") raise RuntimeError("登录失败") finally: await page.close()实操心得:
wait_until参数:page.goto()的wait_until选项非常重要。"load"是页面load事件触发,"domcontentloaded"是 DOM 加载完成,"networkidle"是网络空闲(约500ms无新请求)。对于单页应用或动态加载的页面,"networkidle"更可靠,但等待时间可能更长。需要根据目标网站情况调整。- 登录回调函数 (
login_callback):这是一个设计模式。我们将变化的登录逻辑(每个网站都不一样)抽象成一个回调函数,让使用爬虫类的人去实现。这样爬虫基类就保持通用性。回调函数里应该包含输入用户名、密码、点击登录按钮、处理验证码等所有步骤。 - 状态检查:
is_logged_in方法至关重要。它决定了是否需要触发登录流程。选择器必须唯一且稳定,最好是登录后个人主页上的一个特有元素,比如用户头像、昵称元素等。
4.3 编写一个具体的网站登录回调示例
假设我们要爬取一个虚构的网站https://example.com,它有一个简单的登录表单。
# 新建一个文件 src/example_spider.py import asyncio from src.crawler import PersistentContextCrawler async def example_login_callback(page): """example.com 网站的登录逻辑""" # 1. 等待登录表单加载 await page.wait_for_selector('input[name="username"]', state="visible") # 2. 填写用户名和密码 (在实际代码中,密码应从安全配置中读取,切勿硬编码!) username = "your_username" password = "your_password" # 警告:切勿提交包含真实密码的代码到版本库! await page.fill('input[name="username"]', username) await page.fill('input[name="password"]', password) # 3. 处理可能的验证码(这里以简单的控制台输入为例) # 假设验证码图片的selector是 '#captcha-img' if await page.is_visible('#captcha-img'): captcha_element = await page.query_selector('#captcha-img') # 这里可以调用OCR服务识别,或者手动输入。本例中我们截图并提示手动输入。 await captcha_element.screenshot(path='captcha.png') captcha_code = input("请查看当前目录下的 captcha.png 文件,输入验证码: ") await page.fill('input[name="captcha"]', captcha_code) # 4. 点击登录按钮 login_button_selector = 'button[type="submit"]' # 或 'text="登录"' await page.click(login_button_selector) # 5. 等待登录完成(例如,等待页面跳转或某个登录后元素出现) # 这里可以等待跳转到 dashboard,或者等待一个登录成功的提示 try: await page.wait_for_url('**/dashboard/**', timeout=15000) # 使用通配符匹配URL # 或者等待一个登录后才有的元素 # await page.wait_for_selector('.user-avatar', timeout=15000) except Exception as e: # 如果没跳转,可能登录失败,检查错误信息 error_msg = await page.text_content('.error-message') if await page.is_visible('.error-message') else "未知错误" raise RuntimeError(f"登录可能失败: {error_msg}") from e class ExampleSpider(PersistentContextCrawler): """针对 example.com 的爬虫""" def __init__(self, user_data_dir: str = "./user_data/example_com"): super().__init__(user_data_dir, headless=False) # 调试时先设为非无头模式 self.login_url = "https://example.com/login" self.check_url = "https://example.com/dashboard" self.logged_in_selector = ".dashboard-header" # 仪表盘页面的标题元素 async def run(self): """主要的爬取流程""" # 1. 确保登录 await self.login_if_needed( self.login_url, self.check_url, self.logged_in_selector, example_login_callback ) # 2. 登录成功后,开始爬取数据 self.logger.info("开始爬取数据...") page = await self.get_page() await page.goto(self.check_url, wait_until="networkidle") # 3. 示例:提取仪表盘上的某些数据 # 假设数据在一个 class 为 .data-item 的列表里 data_items = await page.query_selector_all('.data-item') for item in data_items: title = await item.text_content() # 这里可以进行更复杂的数据解析... print(f"抓取到项目: {title}") await page.close() self.logger.info("数据爬取完成。") # 主程序入口 async def main(): async with ExampleSpider() as spider: await spider.run() if __name__ == "__main__": asyncio.run(main())关键点与技巧:
- 密码安全:绝对不要将密码、API密钥等敏感信息硬编码在代码中!应该使用环境变量、配置文件(
.env文件,用python-dotenv读取)或密钥管理服务。 - 选择器策略:优先使用
name、id等稳定属性。其次是>async def crawl_many_pages(self, urls: list): """使用同一个持久化上下文并发爬取多个页面""" semaphore = asyncio.Semaphore(5) # 控制最大并发数为5,避免对目标网站造成过大压力 async def crawl_one_page(url): async with semaphore: page = await self.context.new_page() try: await page.goto(url, wait_until="domcontentloaded") # ... 你的数据提取逻辑 ... data = await page.text_content('body') return data finally: await page.close() tasks = [crawl_one_page(url) for url in urls] results = await asyncio.gather(*tasks, return_exceptions=True) # 处理 results,注意其中可能有异常注意事项:并发数 (
Semaphore的值) 不宜设置过高。一方面是对目标网站友好,遵守robots.txt中可能规定的Crawl-delay;另一方面,单个浏览器上下文的资源(内存、CPU)也是有限的,页面开太多会卡顿甚至崩溃。5.2 请求拦截与性能优化
Playwright 可以拦截和修改网络请求,这个功能在爬虫中非常有用:
- 屏蔽无关资源:阻止图片、样式表、字体、媒体文件等加载,极大提升页面加载速度。
- 模拟 API 响应:直接 mock 某些 API 的返回数据,用于测试或绕过复杂的前端逻辑。
- 捕获 API 请求:直接监听 XHR/Fetch 请求,拿到结构化的 JSON 数据,这往往比解析 HTML 更高效。
async def setup_request_interception(self, page: Page): """设置请求拦截,只加载文档和脚本,屏蔽图片等""" await page.route("**/*", lambda route: route.abort() if route.request.resource_type in ["image", "stylesheet", "font", "media"] else route.continue_() ) # 使用前,在 page.goto 之前调用 await self.setup_request_interception(page)5.3 应对反爬虫策略
即使使用持久化上下文,网站仍可能通过其他手段检测爬虫。
- 指纹检测:Playwright 通过
args: ['--disable-blink-features=AutomationControlled']已经移除了大部分自动化特征。你还可以使用playwright-stealth这类第三方库来应用更多反检测技巧。 - 行为模式:避免过于规律的操作。在点击、输入之间加入随机延迟(
await asyncio.sleep(random.uniform(1, 3))),模拟人类思考时间。使用page.mouse.move()模拟更真实的鼠标移动轨迹。 - IP 限制:这是最棘手的。持久化上下文解决的是登录态,解决不了 IP 被封的问题。对于大规模爬取,你需要使用代理 IP 池。Playwright 支持为每个浏览器上下文或页面设置代理:
context = await playwright.chromium.launch_persistent_context( user_data_dir=..., proxy={ "server": "http://your-proxy-server:port", # 如果需要认证 "username": "user", "password": "pass" } )重要原则:始终尊重
robots.txt文件,控制请求频率,避免在对方服务器高峰时段爬取。爬虫的本质是自动化访问,应尽量做到对目标网站友好。6. 完整项目示例与部署建议
让我们整合以上所有内容,形成一个完整的、可运行的脚本
main.py。import asyncio import logging from pathlib import Path from src.example_spider import ExampleSpider # 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('logs/crawler.log'), logging.StreamHandler() ] ) async def main(): # 定义用户数据目录,建议按网站区分 user_data_dir = Path("./user_data/example_com") # 使用异步上下文管理器,确保资源正确释放 async with ExampleSpider(user_data_dir=str(user_data_dir)) as spider: try: await spider.run() except Exception as e: spider.logger.error(f"爬虫运行过程中发生错误: {e}", exc_info=True) # 可以根据错误类型决定是否要清理损坏的会话(例如删除 user_data_dir) if __name__ == "__main__": asyncio.run(main())部署到服务器(如 Linux)的注意事项:
- 无头模式:将
headless=True。 - 依赖安装:服务器上也需要执行
playwright install chromium。如果服务器没有图形界面,可能需要安装一些系统依赖,Playwright 的安装脚本通常会提示。对于 Ubuntu/Debian,可能需要:sudo apt-get install libnss3 libatk-bridge2.0-0 libdrm2 libxkbcommon0 libgbm1 libasound2。 - 进程管理:使用
systemd,supervisor或pm2来管理爬虫进程,实现开机自启、崩溃重启、日志轮转。 - 定时任务:使用
cron或systemd timer来定时执行你的 Python 脚本。 - 会话维护:持久化上下文意味着
user_data_dir会一直增长。需要定期监控其大小。切勿在多台机器或多个容器中共享同一个user_data_dir路径,这会导致数据损坏。如果使用 Docker,可以将user_data_dir挂载为 volume 以持久化会话。
7. 常见问题与排查手册
在实际操作中,你肯定会遇到各种各样的问题。这里总结了一些典型问题和解决方法。
问题现象 可能原因 排查步骤与解决方案 启动时报错: Failed to launch: Process failed to launch!1. 浏览器内核未安装。
2. 系统缺少依赖库。
3.--no-sandbox参数在桌面环境引起问题。1. 运行 playwright install chromium。
2. 根据 Playwright 官方文档安装系统依赖。
3. 尝试移除args中的--no-sandbox。launch_persistent_context报错:User data directory is already in use同一个 user_data_dir被另一个 Playwright 实例或浏览器进程占用。1. 确保之前的爬虫脚本已完全关闭(检查进程)。
2. 重启电脑释放锁。
3. 为不同的爬虫任务使用不同的user_data_dir子目录。登录状态不保存,每次都要重新登录 1. user_data_dir路径权限问题,无法写入。
2. 网站使用了非持久化的 Session Storage 或特殊的登录机制。
3. 登录后没有正确等待或关闭页面导致状态未保存。1. 检查目录读写权限。
2. 手动登录一次后,检查user_data_dir下是否生成了文件(如Cookies)。
3. 在登录回调函数最后,增加await asyncio.sleep(3)并确保页面正常跳转完成。页面加载超时 ( TimeoutError)1. 网络慢或不稳定。
2. 页面资源过多或有无穷的请求。
3.wait_until条件不满足。1. 增加 page.set_default_timeout()。
2. 使用请求拦截屏蔽非必要资源。
3. 将wait_until从"networkidle"改为"domcontentloaded"或"load"。
4. 使用page.wait_for_selector等待关键元素代替等待页面完全加载。被网站检测为爬虫 1. 浏览器指纹被识别。
2. 操作行为过于规律。
3. 请求频率过高。1. 确保使用了 --disable-blink-features=AutomationControlled参数。
2. 考虑使用playwright-stealth。
3. 在操作间添加随机延迟。
4. 降低并发请求频率,使用代理 IP 池。page.click()或page.fill()不生效1. 元素尚未加载或不可交互。
2. 元素被遮挡(如弹窗)。
3. 选择器定位到了多个元素。1. 在操作前使用 page.wait_for_selector(selector, state='visible' 或 'attached')。
2. 使用page.click(selector, force=True)强制点击(慎用)。
3. 检查页面是否有iframe,需要在iframe内操作。
4. 使用更精确的选择器,如page.query_selector('div.button >> text=Submit')。最后的个人体会:
launch_persistent_context确实是 Playwright 爬虫生态中的一把利器,它将繁琐的会话管理交给了浏览器本身,让我们能更专注于业务逻辑。但在享受便利的同时,也要清醒认识到,它并非“隐身”斗篷。良好的爬虫实践,核心永远在于对目标网站的尊重、对规则的遵守,以及代码的健壮性和可维护性。把这个工具用好,它能帮你自动化很多重复的网页操作;用不好,则可能给自己和目标网站都带来麻烦。希望这篇长文能帮你打下扎实的基础,在实际项目中游刃有余。
