扫码登录这东西,单独用一次还好,可一旦落到自动化脚本里,就成了噩梦。每次跑用例都要掏手机、解锁、对准二维码,运气好三秒通过,运气不好等半天二维码过期,整套流程卡死在登录这一步,后面全是白等。我早期做Web自动化测试时,被这个环节折磨得不轻,后来转向“复用浏览器”的思路,才算把扫码登录这关真正绕过去。
所谓复用浏览器,说白了就是让自动化脚本去接手一个已经登录过的浏览器实例,或者直接恢复上一次的登录缓存,这样就不需要每次从零开始扫码。这个思路解决的核心问题只有一个——登录状态的重用。今天我把这套方法从思路、工具选型到实操步骤完整拆开讲,包含我在实际项目中踩过的坑和总结的排查技巧,希望能帮你彻底告别扫码登录的自动化噩梦。
1. 复用浏览器的核心思路与适用场景
1.1 为什么扫码登录会成为自动化痛点
扫码登录本身是安全设计上的进步:不暴露密码、一次一码、支持二次验证。但在自动化场景里,它恰恰成了最不友好的环节。自动化脚本本质上是一个没有手机的程序,你没法让脚本去“看”二维码,更没法让它去模拟手指点击确认。
常见的绕行方案有哪些?有的人走接口直接伪造登录态,但很多网站的接口有签名、加密参数和风控校验,伪造成本极高;有的人用OCR识别二维码内容再配合逆向协议,这个方案对安全防护较强的站点基本行不通;还有的人设置超长cookie有效期,但一旦cookie过期或换设备,又得重新扫码。这些方案要么维护成本高,要么不稳定,尤其在批量跑用例、多账号切换、夜间定时任务等场景下,扫码这一步就是整个链路里最脆弱的单点。
复用浏览器的思路其实非常朴素:既然扫码的是浏览器里的人,那我就让这个浏览器保持不变,或者把登录后的状态完整保存下来,下次直接恢复。这样扫码只需要一次,之后无论脚本跑多少轮,都不需要再碰手机。
1.2 复用浏览器到底复用了什么
很多人一听到“复用浏览器”,第一反应是“我是不是要把整个浏览器窗口一直开着不关”。这其实是两个层级的概念,需要区分清楚。
第一个层级是“复用浏览器进程”。就是说你手动打开一个浏览器窗口,登录好网站,然后让自动化工具去接管这个已经打开的实例。这个方案适合一次性的调试任务,比如说你临时要做一个数据采集脚本,不想重新走一遍登录逻辑,那就手动登录完,直接让脚本连上这个浏览器干活。
第二个层级是“复用登录态数据”。这才是真正适合长期自动化任务的方案。它的思路是:先把登录成功后浏览器里的cookie、localStorage、sessionStorage等状态数据持久化到本地文件,然后每次启动自动化时,让新的浏览器实例加载这些数据,模拟出一个“已经登录过”的浏览器。用户感知上,就是脚本一启动就是登录状态,扫码这步被完全跳过。
这两个层级在实际项目中往往会组合使用。先用第一个层级手动登录一次,然后把登录态导出来,之后所有自动化任务都用第二个层级去恢复状态。
1.3 哪些场景适合用复用浏览器方案
不是所有自动化场景都需要复用浏览器,但它在我经手的以下场景里确实非常有效:
- 数据采集类脚本:需要登录后才能抓取数据,且目标网站没有现成开放API,每次启动脚本都扫码不现实。
- UI自动化回归测试:测试用例本身不关心登录流程,只关心登录后的业务功能,复用浏览器能节省大量执行时间。
- 多账号批量操作:比如同时运营多个店铺账号,每个账号都要保持登录态,用复用浏览器做账号隔离和状态管理非常顺手。
- 定时任务和无人值守任务:凌晨或者周末跑的任务,不可能每次都有人守在旁边扫码,复用浏览器是唯一可行的路子。
反过来,如果你正在测试的恰恰是登录功能本身,或者你的脚本运行环境是完全隔离的一次性容器,那复用浏览器就不太合适。这种场景下应该老老实实走登录流程,或者用测试环境提供的专用测试账号。
2. 工具选型:Playwright与Selenium的对比
2.1 Playwright的持久化上下文
在目前主流的自动化工具里,Playwright对“复用登录态”的支持可以说是最直接的。它提供了两个关键能力。
第一个是storageState,你可以把当前浏览器上下文的登录状态保存成一个JSON文件,里面包含了cookie、localStorage、sessionStorage的所有数据。下次启动时,直接在创建浏览器上下文时加载这个文件,就能恢复登录状态。
第二个是browserContext的持久化,你可以指定一个userDataDir作为用户数据目录,相当于把整个浏览器的用户数据(包括登录态、扩展、页面数据)都保存在本地磁盘上。浏览器关闭后,下次用同一个userDataDir启动,就跟你上次关掉浏览器之前一模一样,很多需要保持登录的站点都能直接恢复。
这两个能力和我们前面说的两个层级完全对应:storageState对应“复用登录态数据”,userDataDir对应“复用浏览器进程状态”。我个人在多数场景下更推荐storageState,因为它是纯数据,干净、轻量、便于多环境复制;而userDataDir虽然更接近真实用户环境,但体积大,且偶尔会因为浏览器崩溃导致数据损坏。
2.2 Selenium的调试端口复用
Selenium是更传统的自动化工具,它本身没有直接提供保存登录态的方法,但可以通过“调试端口”的方式复用浏览器。
具体思路是:以带远程调试端口的模式启动Chrome浏览器,手动在这个浏览器里完成扫码登录,之后Selenium通过WebDriver协议连接这个已有的浏览器实例。这样Selenium用的就是你已经登录好的浏览器,不需要再走登录流程。
这种方式的好处是直观、侵入性低,适合调试类任务。坏处是它依赖一个“已经打开的浏览器实例”,如果浏览器进程异常退出,脚本也就断了。而且多并发场景下,每个实例都要单独管理端口,稍显笨重。
Selenium还有一种更常规的登录态复用方式,就是手动导出cookie,然后在脚本启动时用add_cookie加回去。这个方法能做到和Playwright的storageState类似的效果,但操作上更麻烦一点,而且对localStorage的处理没有原生支持。
2.3 我的选型建议
我在实际项目中,会按任务类型做选型:
- 如果是临时调试、一次性的数据采集,优先用Selenium的调试端口复用,因为启动快、不用写额外的登录态保存代码,手动扫码登录后就够了。
- 如果是长期跑的自动化任务,需要无人值守、重复执行,优先用Playwright的
storageState,因为登录态数据落盘后可以反复加载,稳定性高。 - 如果项目里已经深度使用Selenium,不想引入新工具,那就用Selenium的cookie导出恢复方案,虽然麻烦一点,但也能达到目的。
说实话,如果你和我的场景类似——大部分时间在写爬虫和自动化脚本,那么Playwright应该是更顺手的工具。它性能好,API设计现代,对登录态的处理几乎是开箱即用的。Selenium则更适合团队中已经存在大量基于它的历史代码的情况。
3. 实操:Playwright复用浏览器跳过扫码登录
3.1 第一步:手动登录并保存存储状态
这个步骤的核心是:先手动把登录流程走一遍,让登录态写进浏览器上下文,然后把上下文保存下来。
我通常用一段临时脚本,用Playwright启动一个带界面的Chromium浏览器,然后打开目标网站,停在登录页面,等你自己扫码登录。登录成功后,脚本会把当前上下文的存储状态写到文件中。
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context() page = context.new_page() page.goto("https://example.com/login") # 等你自己扫码登录,手动按回车确认 input("扫码登录完成后,按 Enter 键继续...") # 保存存储状态到本地文件 context.storage_state(path="state.json") browser.close()这里有几个细节要注意。context.storage_state(path="state.json")只保存cookie、localStorage和sessionStorage,不保存其它浏览器数据,比如IndexedDB或者Service Worker,如果你的目标网站依赖这些数据,只靠storageState可能不够,这时候就要用launch_persistent_context配合user_data_dir。另外,保存时确保没有开启无痕模式,否则localStorage和cookie可能无法正常落盘。
扫码之前最好先确认一下目标网站的登录态是否完全写入,有些网站登录成功后还会异步刷新一次token,如果你按回车太快,可能保存的是登录前的状态。稳妥的做法是,扫码后在页面上停留几秒,或者随便点击一个需要登录才能访问的页面,确认状态已经稳定。
3.2 第二步:加载存储状态启动上下文
保存好state.json之后,后续的自动化脚本就可以直接加载它,启动即登录态。
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context(storage_state="state.json") page = context.new_page() page.goto("https://example.com/dashboard") # 如果正常跳转到个人中心,说明登录态恢复成功 print(page.title()) browser.close()关键点在于browser.new_context(storage_state="state.json")这里。一行代码,就把之前保存的所有登录态数据灌进了新的上下文。启动后页面就会自动带上登录凭证,直接访问需要鉴权的页面。
这里有一个容易被忽略的问题:state.json文件里保存的cookie是有域名的,只能在对应的域名下生效。如果你在脚本里访问的是https://example.com,但登录操作发生在https://www.example.com,那cookie可能不会完整匹配。遇到这种情况,检查一下登录前后的URL,确认它们属于同一个cookie domain,或者保存状态时统一用主域名访问。
另外,如果你的网站登录态是依赖IP或者设备指纹的,比如某些风控严格的银行类站点,仅仅恢复cookie可能仍被判定为异常登录。这种场景下,用launch_persistent_context恢复整个用户数据目录会更保险,因为它连浏览器的指纹信息也一并恢复了。
3.3 第三步:处理登录态过期与自动续期
任何登录态都有有效期,复用浏览器方案不是一劳永逸的。几天不跑、接口改动、服务端session失效,都会导致state.json里的状态变成废纸。
我的做法是给登录态加一个“健康检查”和“自动重新登录”的包装逻辑。简单说,就是每次启动脚本时,先尝试访问一个只有登录才能访问的页面,如果页面没有跳转去登录页,说明登录态有效,直接执行任务;如果被重定向到登录页,就先用非headless模式打开浏览器,手动扫码一次,重新生成state.json,然后继续执行任务。
def is_logged_in(page): page.goto("https://example.com/profile") return page.url == "https://example.com/profile" def refresh_state(): with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context() page = context.new_page() page.goto("https://example.com/login") input("请扫码登录,完成后按 Enter...") context.storage_state(path="state.json") browser.close()我个人会把state.json当作缓存来处理,不会在项目里硬编码它的路径。后续如果有多套环境,比如测试环境和生产环境,就用不同的文件路径来区分。
另外一个非常实用的技巧是,在扫码后检查一下服务端设置的cookie有效期。有的网站cookie有效期只有一小时,有的却是三十天,知道有效期之后,你就可以预估什么时候需要重新扫码,提前在调度系统里安排一次手动登录任务,避免半夜任务因为登录态过期而全部失败。
4. 实操:Selenium复用已打开浏览器
4.1 使用debug端口启动浏览器
如果你还在用Selenium,或者因为某些历史原因必须继续用Selenium,也别急着推翻重来。Selenium复用浏览器的核心,是通过Chrome的远程调试端口。
第一步,先以调试模式启动Chrome。在命令行里执行:
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile注意--user-data-dir必须指定一个独立的目录,不能用默认的用户目录,否则Chrome会报错。这个目录相当于整个浏览器的“记忆”,会保存你的登录态、扩展、设置。
启动之后,在浏览器里手动访问目标网站,完成扫码登录。此时浏览器的所有状态都保存在/tmp/chrome-profile里。
第二步,用Selenium连接这个已打开的Chrome实例。这时不能直接用webdriver.Chrome(),而是要通过ChromeOptions设置调试地址:
from selenium import webdriver options = webdriver.ChromeOptions() options.debugger_address = "127.0.0.1:9222" driver = webdriver.Chrome(options=options) driver.get("https://example.com/dashboard")只要连接成功,driver控制的就是你已经登录的那个浏览器标签页,直接就能访问需要鉴权的页面,完全跳过扫码。
4.2 通过WebDriver连接已有实例
上面连接的过程很简单,但有一个容易踩的坑是Selenium版本和Chrome版本的兼容性。如果chromedriver版本和Chrome版本不匹配,连接调试端口时可能会报SessionNotCreatedException。我的建议是,无论用Selenium 3还是Selenium 4,都确保chromedriver和浏览器主版本号完全一致。
还有一个细节是,用调试端口方式连接时,Selenium拿到的其实是一个已经存在的浏览器实例,它不会帮你自动创建新的tab,而是复用现有的页面。如果你希望在网站上打开多个标签页,可以直接通过driver.switch_to.new_window()创建新窗口,这些窗口依然继承同一个实例的登录态。
如果你需要长期使用这个方案,我还建议把启动Chrome调试模式和连接Selenium的步骤封装成两个独立的小模块。一个负责管理浏览器的生命周期,一个负责执行具体业务脚本。这样即使浏览器意外崩溃,你可以快速重启调试模式的Chrome,而不需要重启整个自动化任务。
4.3 注意事项与隐患
Selenium调试端口复用方案虽然方便,但有几个天然的隐患需要提前知道。
首先是并发限制。同一个调试端口的浏览器实例,同一时间只能被一个Selenium客户端连接。如果团队里多人共用一台机器的调试端口,或者多个任务试图同时控制同一个浏览器,会直接导致session冲突,表现就是命令超时或自动断开。解决方案是每套任务用独立的端口和独立的--user-data-dir。
其次是防火墙和权限问题。远程调试端口如果暴露在公网,任何人都可以连上这个浏览器提交命令,这是相当大的安全隐患。所以我强烈建议只在本机或内网使用,并且不要在外网暴露9222端口。
最后是浏览器进程的稳定性。手动打开的Chrome窗口,要保证在整个自动化任务运行期间不被关掉。如果有人在物理机上不小心点了关闭按钮,所有依赖这个实例的脚本都会瞬间失败。比较稳妥的做法是,在服务器或专用的无人值守机器上跑这个方案,并且设置屏保锁定,防止人工误操作。
5. 常见问题与排查技巧实录
5.1 登录态丢失、过期怎么办
这个问题几乎每个人都会遇到。很多人保存了state.json,第二天跑脚本发现还是跳转到登录页了,第一反应是“复用浏览器没用”。但大多数时候不是复用方案的问题,而是目标网站的登录态本身就有效期限。
排查思路是:先确认一下state.json文件里cookies的数量和内容。如果cookies还在,但里面某些关键字段(比如sessionid、csrf_token、auth_token)已经过期,那说明是服务端把会话废掉了,这不是保存格式的锅。
常用的解决办法有三个:一是缩短登录态的使用周期,比如每天早上任务开始前,用非headless模式重新扫码一次;二是使用两个账号轮换,一个失效自动切到另一个,给运维留出人工处理的时间;三是分析过期原因,看看是否有接口在登录后异步刷新token,如果有,可以考虑在任务开始前先访问一次主页,让站点把新token种进cookie,再执行核心操作。
5.2 复用失败、端口冲突或权限异常
如果是Selenium调试端口方案,最常见的错误是端口被占用或拒绝连接。先用netstat -ano | grep 9222看端口是否处于监听状态,确认Chrome确实以调试模式启动了。
如果端口正常,但Selenium依然连接不上,检查浏览器是否开启了代理或者有插件拦截网络请求,这些都会干扰WebDriver与Chrome之间的管道通信。还有一点值得注意,Selenium 4.x和3.x在debugger_address的写法上也有细微区别,Selenium 4推荐直接在options.debugger_address里设置,Selenium 3可能会识别不到这个属性,需要额外传参。
如果上述都排查了还是报错,直接重启Chrome和脚本。有时候就是浏览器进程卡死了,这种玄学问题优先用重启解决。
5.3 多账号与多环境隔离
如果你同时在管理多个账号,千万别把所有账号的登录态都塞在同一个state.json里。那样的话,账号A的登录会把账号B的登录顶掉,而且不同账号的cookie混在一起,会出现权限错乱的诡异问题。
我的做法是强行按“账号”维度拆隔离。
- 使用
state_账号A.json、state_账号B.json这样的命名规范。 - 每个任务启动前,用独立的
browser.new_context(storage_state=对应的账号文件)。 - 如果用的是Selenium调试端口方案,每个账号分配独立的
--user-data-dir和独立的端口。
这样多账号批量操作时,互相之间不会串数据,调试排错也容易。
5.4 安全与合规提醒
这里单独说一句,复用登录态虽然便利,但也意味着你实际上是在“代理”一个真实用户的身份。如果你操作的是自己的账号,那问题不大;但如果操作的是他人的账号,或者账号权限较高,请务必遵守目标网站的用户协议和相关法律法规。不要用复用浏览器的能力去做任何踩红线的操作,比如绕过访问控制、批量获取他人隐私数据、干扰正常业务流程等。
我在项目中一直坚持的原则是:复用登录态只用于自己有权访问的数据,并且所有自动化操作都在合理频率范围内,不要对目标站点造成压力。合理使用这套技术,它是提效工具;滥用它,则是给自己挖坑。
6. 我的实操体会与进一步扩展
6.1 踩过最多的坑,反而是“保存状态太早”
有一次我写一套爬虫,为了省事,扫完码之后立刻保存state.json,结果第二天脚本起来,访问首页正常,访问列表页却一直403。排查了半天,发现登录后站点还会在后台发起一次鉴权请求,把真正有效的access_token更新到localStorage里,而我保存的状态是扫码后过早就保存的,token还是旧值,自然被服务端拒了。
从那以后,我养成了一个习惯:扫码登录成功之后,强制访问一次需要登录的页面,等待1-2秒,再执行storage_state导出。这个动作相当于把登录后的“会话成熟期”给等过去,确保保存的是稳定可用的状态。
6.2 每一步都可以继续自动化
复用浏览器跳过扫码登录只是一个起点。当你掌握登录态持久化之后,还可以继续把整条链路自动化:登录态检测、登录态刷新、多账号调度、失败重试、数据上报,完全可以组装成一套无人值守的自动化工作流。
我目前在项目里就是把这个方案嵌入到定时任务系统中,每天通过API接口触发采集任务,任务启动时检查登录态有效期,如果即将过期,自动调用一个需要人工介入一次的刷新流程,刷新完成后继续跑任务。
最后再分享一个小技巧:保存的state.json文件里,很多cookie带有expires字段,你可以写一个小脚本,定期扫描所有cookie的过期时间,在登录态失效前提前预警。这样你就不用等到脚本报错时才去重扫,而是能提前规划人工扫码窗口,让整个自动化体系看起来就像“永续在线”一样顺畅。