ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

告别Selenium:纯Python+Requests实现微信视频号扫码登录拿Cookie

告别Selenium:纯Python+Requests实现微信视频号扫码登录拿Cookie 前两个月接了个自动化小项目目标很直接把微信视频号管理后台的登录Cookie稳定拿下来供后续的批量数据任务使用。第一版我偷懒直接上了Selenium——浏览器自动打开、人工扫码、再抓取Cookie本地Windows上跑得欢。结果一挪到云服务器问题全冒出来了内存动不动400MB往上跑两三天必崩Selenium和Chrome版本一不对付又得重新调。气得我干脆重写用纯Python Requests把整套扫码登录流程模拟了一遍效果反而出奇地好登录从十几秒压缩到三秒以内不说内存占用可以忽略不计。这篇文章把方案原理、完整代码和调试时踩的坑一次讲清楚给还被Selenium折腾的兄弟们一条新路子。1. 触发我重写的那个夜晚Selenium在服务器上的崩溃先说清楚场景微信视频号助手channels.weixin.qq.com是视频号创作者的后台里面有作品数据、粉丝分析、评论管理但官方并没有给个人开发者提供一套公开的、可以直接调用的数据API。想批量拉自己账号的数据最现实的办法就是先登录拿到Cookie然后带着Cookie去请求后台的接口。这个需求听起来简单但一开始我被“自动化登录”这四个字带偏了第一反应就是上Selenium。毕竟社区里一搜全是这类教程浏览器自动化好像才是正统解法。结果在生产环境跑了一周我整个人都被它磨得没脾气。1.1 一套流程串起来要走十几秒内存却吃掉400MBSelenium本质上会拉起一个真实的浏览器内核不管你有没有头它都跑了一整套Chromium。视频号助手这个后台页面本身又不轻登录进去之后要加载各种图表、实时数据面板和前端框架一个无头浏览器实例轻松吃掉300到500MB内存。在我那个只有2G内存的云服务器上单账号跑勉强能撑住一旦加上数据抓取、定时调度、日志回传这些模块系统负载直接拉满。最崩溃的是连续跑两三天后Chrome进程偶尔会僵在那里不退出内存一点一点泄漏最后整个服务被系统OOM杀掉。除了内存版本匹配也是个无底洞。Selenium要跟Chrome版本严格对应Chrome一升级chromedriver版本不匹配代码直接跑不起来。服务器上又不敢随便“自动升级”每次发版都要手工锁版本维护成本远超我第一次写登录脚本的时间。对比下来Requests方案的优势非常直观对比项Selenium方案Requests方案内存占用300~500MB几乎可忽略启动时间8~15秒0.5秒以内外部依赖Chrome chromedriver Selenium库requests qrcode维护成本三套版本互相匹配只需关注接口是否改版适合场景复杂页面交互登录取Cookie、调接口如果你的目标只是“拿到登录态然后去调接口”用Selenium有点杀鸡用牛刀的意思而且这头牛还特别难伺候。1.2 扫码登录其实是个异步轮询浏览器是被大材小用的很多人没意识到扫码登录这件事根本不需要浏览器渲染页面它在网络层面其实是一个标准的异步轮询模型脚本请求服务器获取一个临时会话标识通常叫uuid把带uuid的链接编码成二维码展示给用户用户拿起手机扫码、确认脚本每隔一两秒问一次服务器这个人扫码了没服务器回答已经确认并返回一个跳转地址脚本访问这个跳转地址服务端通过Set-Cookie下发登录态Cookie整个过程没有复杂的DOM操作没有JS渲染没有CSS布局纯粹就是“请求-响应-轮询-再请求”。浏览器在这里唯一的“功劳”是帮你画了个二维码但这个用Python的qrcode库一样能干而且画得又快又好看。理清了这层逻辑你会发现Selenium已经没有任何存在的必要。Requests能搞定全部网络请求qrcode能搞定二维码展示剩下的就是按顺序把请求串起来。2. 手机确认登录的那几秒里HTTP世界发生了什么写代码之前必须把原理吃透否则接口一变你就抓瞎。这章我用大白话把扫码登录背后发生的事情讲清楚理解了这些你再看代码就是顺水推舟的事。2.1 二维码里藏的不是密码是一个待确认的uuid二维码的内容看起来是一串乱码URL但实际上它不包含你的账号密码只有一个临时生成的uuid参数。这个uuid是整个登录会话在服务端的唯一标识相当于一张“未签名的工单”。手机微信扫码后会识别出这串URL发现这是微信开放平台的登录确认请求于是把当前登录的微信身份跟这个uuid绑定起来。这一步只是“绑定”还没真正授权。你会在手机上看到“确认登录”的页面点击之后服务端才把这张工单标记为“已确认”。所以整个流程的安全性建立在两个关键点上一是uuid存活时间极短通常几分钟就过期二是必须由手机端主动确认服务器才会放行。用餐厅排队等位来类比最直观uuid是你手里的叫号牌手机扫码相当于告诉服务员“我是这个号”点确认才是“我确认要排这个号”。服务员服务器只看叫号牌叫到号了状态变更才会让你进包厢下发Cookie。2.2 状态轮询前端定时器和后端接口的默契用户扫码之后网页怎么知道“该跳转了”靠的是轮询。前端JavaScript里通常会有一个setInterval定时器每隔1到2秒向后端的状态查询接口发一次请求传的参数还是那个uuid。后端拿到uuid后会返回一个状态码大致就三种情况未扫码继续等已扫码但没确认提示用户在手机上操作已确认返回下一步跳转地址这个机制在Requests里实现起来极其简单把setInterval换成while循环加sleep就行。前端定时器还在为页面卡顿焦虑的时候你已经用一个普通循环把同样的事干完了。需要提醒的是轮询的本质是“主动问”不是你跟服务器之间维持了一条长连接。所以间隔太短没有任何收益只会增加服务端压力还容易被限流。间隔太长则用户等得着急。实测下来1.2到1.8秒是个比较舒服的区间。2.3 登录态Cookie是怎么从跳转链路里长出来的当用户在手机上点了确认轮询接口返回的数据里会带一个redirect_url这个地址很关键。它通常包含一次性票据相当于服务端给你开了一张“临时通行证”。你用Requests去访问这个地址时服务端会在响应的Set-Cookie头里下发真正的登录态Cookie。这里就显露出requests.Session的价值了。Session对象会自动记录响应里的Set-Cookie并按域名、路径、过期时间帮你管理Cookie。你要做的只是拿着同一个Session继续访问平台首页后续请求就自动带上登录凭证了。所以整个登录链路里最核心的就三步拿uuid、轮询确认、访问跳转地址收Cookie。每一环都不需要浏览器参与纯HTTP请求就能完整跑通。3. 代码落地一个覆盖扫码到保存Cookie的完整登录模块原理讲完进入正题。我会给出一份可以直接跑的代码然后在后面逐段拆解关键逻辑。这不是“只能跑通演示”的玩具代码里面包含了重试机制、超时控制、登录态复用等生产环境需要的细节照着改一改就能接入你自己的项目。3.1 只用两个依赖环境准备一句话安装命令非常简单pip install requests qrcode[pil]这里解释一下为什么用qrcode[pil]qrcode库负责生成二维码保存为PNG图片时需要依赖pillow这个图像库。后面那个[pil]后缀是qrcode的extras写法会帮你把pillow一起装上。如果你不装pillow跑qrcode.make(...).save(...)这行代码时会直接报错报错信息还不容易联想到是缺库。这是第一个小坑后面避坑章节还会展开。Python版本建议3.8以上用到的语法和库都很基础新老版本兼容性没什么大问题。3.2 完整代码video_login.py video_login.py 微信视频号助手登录 Cookie 获取纯 Python Requests 实现。 依赖requests, qrcode[pil] 用法 python video_login.py # 优先复用本地 Cookie失效则重新扫码 python video_login.py --login # 强制重新扫码登录 import json import random import re import sys import time from pathlib import Path import qrcode import requests # ---------------------- 配置区以实际抓包为准 ---------------------- LOGIN_PAGE https://channels.weixin.qq.com/platform/login # 二维码内容通常指向这个确认页 CONFIRM_PAGE https://open.weixin.qq.com/connect/confirm # 登录轮询接口改版后请从 Network 面板重新抓取 LP_CONFIRM_API https://lp.open.weixin.qq.com/connect/confirm COOKIE_FILE wx_channels_cookies.json QR_FILE wx_channels_qrcode.png QR_TIMEOUT 300 # 二维码有效期单位秒 # 轮询状态码以页面 JS 实际定义为准 SCAN_WAITING 1 SCAN_CONFIRMED 2 SCAN_SUCCESS 3 # -------------------------------------------------------------------- class ChannelsLogin: def __init__(self): self.session requests.Session() self.session.headers.update({ 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 ), Accept-Language: zh-CN,zh;q0.9, }) self.cookie_file Path(COOKIE_FILE) def _get(self, url, *, refererNone, paramsNone, retries3, **kwargs): GET 请求封装统一带 Referer并处理 429 限流 headers {Referer: referer} if referer else {} for attempt in range(retries): try: resp self.session.get( url, headersheaders, paramsparams, timeout15, **kwargs ) if resp.status_code 429: wait 2 ** attempt random.uniform(0.3, 1.0) print(f[429] 触发限流{wait:.1f}s 后重试) time.sleep(wait) continue resp.raise_for_status() return resp except requests.RequestException as exc: if attempt retries - 1: raise RuntimeError(f请求失败: {url} - {exc}) from exc time.sleep(2 ** attempt random.uniform(0.2, 0.8)) raise RuntimeError(f重试 {retries} 次仍然失败: {url}) def get_qr_content(self): 第一步从登录页解析二维码内容 resp self._get(LOGIN_PAGE) html resp.text # 不同版本登录页注入的数据结构不同常见字段codeUrl / qrUrl / uuid patterns [ rcodeUrl\s*:\s*([^]), rqrUrl\s*:\s*([^]), ruuid\s*:\s*([^]), ] for pattern in patterns: match re.search(pattern, html) if match: value match.group(1) if value.startswith(http): return value return f{CONFIRM_PAGE}?uuid{value} # 兜底直接从 HTML 里捞形如 .../connect/confirm?uuidxxx 的链接 url_match re.search( rhttps://[a-z0-9.-]*weixin[a-z0-9.-]*\.qq\.com/connect/confirm\?uuid[a-zA-Z0-9_-], html, ) if url_match: return url_match.group(0) raise RuntimeError( 没有从登录页解析到二维码内容页面结构可能改版请抓包更新 ) staticmethod def parse_uuid(qr_content): match re.search(ruuid([a-zA-Z0-9_-]), qr_content) if not match: raise ValueError(f二维码内容里没有 uuid: {qr_content}) return match.group(1) def poll_login_state(self, uuid, timeoutQR_TIMEOUT): 第二步轮询等待扫码确认返回跳转地址 print(f二维码已生成: {QR_FILE}请用手机微信扫码{timeout}s 内有效) deadline time.time() timeout while time.time() deadline: resp self._get( LP_CONFIRM_API, params{uuid: uuid, t: int(time.time() * 1000)}, refererCONFIRM_PAGE, ) data resp.json() status data.get(status) if status SCAN_CONFIRMED: print([提示] 已扫码请在手机上点击确认) elif status SCAN_SUCCESS: print([成功] 已确认登录) for key in (redirect_url, redirect, url): if data.get(key): return data[key] return LOGIN_PAGE # 1.2~1.8s 随机间隔避免固定频率被限流 time.sleep(random.uniform(1.2, 1.8)) raise TimeoutError(等待扫码超时请重新运行脚本) def finalize_login(self, redirect_url): 第三步访问跳转地址收集 Set-Cookie返回完整 Cookie 字典 self._get(redirect_url, refererLOGIN_PAGE) # 再访问一次平台首页确认 Cookie 在真实请求链路里生效 self._get(LOGIN_PAGE, refererredirect_url) return self.session.cookies.get_dict() def save_cookies(self, cookies): self.cookie_file.write_text( json.dumps(cookies, ensure_asciiFalse, indent2), encodingutf-8, ) print(fCookie 已保存到 {self.cookie_file}) def load_cookies(self): if not self.cookie_file.exists(): return None cookies json.loads(self.cookie_file.read_text(encodingutf-8)) if cookies: self.session.cookies.update(cookies) return cookies or None def is_logged_in(self): 检查本地 Cookie 是否还有效 resp self._get( LOGIN_PAGE, params{t: int(time.time() * 1000)}, retries1, ) url resp.url.lower() if login in url: return False if platform/home in url or platform/index in url: return True print(f[未知] 当前 URL: {resp.url}请人工判断登录态) return False def login(self): qr_content self.get_qr_content() uuid self.parse_uuid(qr_content) qrcode.make(qr_content).save(QR_FILE) redirect_url self.poll_login_state(uuid) cookies self.finalize_login(redirect_url) self.save_cookies(cookies) return cookies if __name__ __main__: cli ChannelsLogin() if --login not in sys.argv and cli.load_cookies(): if cli.is_logged_in(): print(本地 Cookie 仍有效直接复用) print( json.dumps(cli.session.cookies.get_dict(), ensure_asciiFalse, indent2) ) sys.exit(0) print(本地 Cookie 已失效重新扫码登录) cli.login() print(登录完成Cookie 内容) print(json.dumps(cli.session.cookies.get_dict(), ensure_asciiFalse, indent2))代码里配置区的接口路径是我当时从浏览器Network面板抓的微信这类型接口改版比较频繁如果你跑的时候发现解析不到二维码或轮询没反应优先按第4章的方法重新抓包把新接口路径填到配置区就行。3.3 关键逻辑逐段拆解这份代码里最值得说的地方是几个容易被忽略的小细节。第一构造函数里用了requests.Session()这是一个隐形的Cookie罐子。Session会在后续请求中自动携带之前收到的所有Cookie也会自动处理响应里的Set-Cookie。如果不用Session而用裸的requests.get你就要手工管理Cookie的存取登录链路一复杂必出问题。第二统一维护User-Agent和Accept-Language。很多服务端接口会校验请求头UA不是浏览器标识、Accept-Language缺失轻则返回异常页面重则直接拒绝请求。建议直接从自己浏览器的Network面板复制一份完整的UA字符串而不是用requests默认的python-requests/x.x。第三_get方法里的429处理逻辑。这里做了两层一是遇到429状态码时等待并重试二是遇到网络异常时按指数退避重试。429就是服务端告诉你“请求太频繁了”此时必须停下来等一等而不是继续猛冲。重试间隔用2 ** attempt递增再叠加一个随机数避免多个重试请求在同一时刻撞上去。第四get_qr_content用正则从登录页HTML里提取二维码内容而不是硬编码某个接口。这样做的原因是登录页里通常会以JS变量的形式注入初始状态但变量名在不同版本里可能是codeUrl、qrUrl或者uuid。多写几个正则去匹配总有一个能中比写死一种格式健壮得多。第五轮询间隔不是固定值而是random.uniform(1.2, 1.8)。固定间隔会让请求呈现出明显的时钟规律风控系统很容易识别这种机器行为。随机抖动一下请求模式更接近真人操作也降低了整体频率。第六finalize_login里为什么要访问两次第一次访问redirect_url是为了拿Set-Cookie第二次访问LOGIN_PAGE是为了验证这些Cookie在真实页面请求链路上是否生效。有些时候只访问一次redirect_urlCookie虽然拿到了但没在实际业务域下“激活”再访问一次首页才能把完整登录态固化下来。4. 实测踩过的坑从429限流到Cookie失效五个绕不开的问题代码只是第一步真正耗时间的是调试。下面这五个坑我全部踩过每个都值得单独拿出来说因为它们在真实项目里几乎必然会出现。4.1 轮询太激进被429按在地上摩擦第一个版本我把轮询间隔设成了0.5秒想着能快点拿到结果结果跑了不到十次轮询接口就开始返回429状态码。响应体里就是那句经典的429 Too Many Requests服务端限流了。这里要理解限流不是bug它是服务端的自我保护机制。视频号这种平台背后是海量真实用户任何单个客户端都不应该以极短间隔连续请求同一个接口。0.5秒的轮询频率在服务端看来基本就是机器特征。解决办法就是我代码里写的间隔至少1秒往上我最终调到1.2到1.8秒的随机区间连续跑多个账号也没再触发限流。还有个建议如果登录流程失败需要重试不要立刻重跑整个脚本等上十几秒再开始让服务端的状态稍微缓一缓。4.2 保存了一堆Cookie请求还是跳回登录页这是我调试过程中最困惑的一个问题。流程跑完了Cookie也存成JSON文件了但下次脚本启动时带着Cookie去请求首页还是被302重定向到登录页。排查下来有几个原因一是保存时机不对。有些人会在轮询确认后立刻保存Cookie但此时跳转地址还没访问服务端最关键的那几个Set-Cookie还没下发保存的自然是不完整的。必须等finalize_login执行完完整走完重定向链路再用cookies.get_dict()导出。二是Cookie的作用域问题。requests.Session.cookies.get_dict()默认返回该Session管理的所有Cookie但不同Cookie可能对应不同domain。如果你在代码里手工过滤、只挑了几个“看起来有用”的字段很可能会漏掉真正关键的凭证。我建议直接保存完整的字典不做过多的手工筛选。三是最隐蔽的一点浏览器里有些Cookie是前端JavaScript写入的不走Set-Cookie头。requests是纯HTTP客户端没有JS执行环境拿不到这种Cookie。要避免这个问题就得保证登录链路完全走服务端的Set-Cookie不要依赖任何页面JS伪造Cookie的流程。访问完整的redirect_url就是为了把所有该由服务端下发的Cookie都激活。4.3 qrcode图片在服务器上打不开二维码白生成了本地Windows上跑得好好的qrcode生成PNG后自动打开扫码完事。一到Linux服务器代码照样跑PNG也生成了但我人不在服务器旁边总不能每次登录都远程下载图片吧。这里有两个解法。第一个是用qrcode库的终端ASCII输出功能直接在终端打印出二维码的字符画。新版qrcode库支持print_ascii方法qr qrcode.QRCode(border1) qr.add_data(qr_content) qr.make(fitTrue) qr.print_ascii(invertTrue)这样你SSH登录服务器后终端里直接就能看到二维码用手机对着屏幕扫即可。注意invertTrue在部分终端上显示效果更好如果扫不出来就试试把invert去掉。第二个是生成PNG后把文件放到你临时搭建的一个静态目录下手机浏览器访问图片地址再扫。这个方式更适合有公网环境的服务器但要注意扫完码记得删掉图片文件避免二维码被别人扫走导致账号风险。另外提醒一句如果报错ModuleNotFoundError: No module named PIL就是安装时没带[pil]后缀补装一下pillow即可。4.4 Cookie里的登录态说没就没程序却毫无察觉登录态有效期不是永久的短则几小时长则几周取决于平台策略。最糟糕的情况不是Cookie失效而是你的程序不知道它失效了还在拿着过期的Cookie拼命请求业务接口返回的全是登录跳转页面解析半天解析出一堆破烂数据。所以我把登录态检测做成了独立的方法is_logged_in请求平台首页观察最终URL。如果URL里出现login说明被踢回登录页了如果停留在platform/home或platform/index之类的地址说明Cookie还有效。在主程序里每次启动任务前先走一遍这个检测失效了再自动拉起登录流程。这属于典型的“吞掉异常不如提前预防”成本低效果立竿见影。4.5 想并发跑多个账号结果一起被风控登录逻辑跑通后我以为可以放心并发多个账号了就写了个多线程脚本同时轮询多张二维码。结果没过多久所有账号的轮询请求全部开始报429有的甚至出现长时间超时。原因不复杂多个登录会话共用同一个出口IP每个会话都在高频轮询叠加起来对服务端就是一波集中的请求洪峰。风控系统看到同一个IP下有多个uuid同时在疯狂轮询直接把该IP的请求降权了。解决思路是错峰。不同账号的登录流程不要在同一时刻启动人为错开几秒到十几秒每个账号的轮询间隔也随机化避免整齐划一的请求曲线。如果账号数量多建议给每个账号分配独立的Session不要共用一个Session实例。5. 拿Cookie做后续开发前先把这层“自愈机制”搭好登录模块的价值不只在于跑通一次而在于它能不能稳定地反复使用。我建议你拿到代码后先把持久化和自愈机制调通再接业务逻辑。否则上线之后天天半夜爬起来扫码那体验比Selenium还酸爽。5.1 JSON持久化和自动加载代码里的save_cookies和load_cookies已经把持久化逻辑写好了。Cookie以JSON格式存到本地文件下次启动时load_cookies会读取文件并更新Session里的Cookie记录。这里有两个细节可以优化。一是文件路径用相对路径时要考虑工作目录问题建议改成绝对路径或者跟脚本所在目录绑定。二是Cookie文件里全是敏感凭证一定要加入.gitignore列表防止误提交到代码仓库。我习惯把Cookie文件权限设置为当前用户可读写不给其他用户任何权限。5.2 登录态失效后的自愈循环有了is_logged_in方法就可以写一个简单的自愈循环def run_with_relogin(job_func): cli ChannelsLogin() if not cli.load_cookies() or not cli.is_logged_in(): cli.login() while True: try: job_func(cli.session) except LoginExpiredError: print(登录态失效重新扫码登录) cli.login() time.sleep(60)原理很简单每次执行业务任务前或者捕获到登录失效异常时先检查再处理。如果本地Cookie失效就重新拉起扫码登录流程。注意扫码登录依然需要人工参与所以真正的“无人值守”在扫码场景里是不存在的。你只能尽量把Cookie有效期拉满把重新扫码的频率降到最低。5.3 请求节奏控制一个简单的限速器登录之后的业务请求同样需要注意节奏。视频号后台的接口虽然没有特别严苛的限制但短时间大量请求一样会触发限流。我的做法是在Session的请求层加一个轻量的延时控制器class RateLimiter: def __init__(self, min_interval1.0, max_interval2.0): self.min_interval min_interval self.max_interval max_interval self._last_request_time 0 def wait(self): now time.time() elapsed now - self._last_request_time interval random.uniform(self.min_interval, self.max_interval) if elapsed interval: time.sleep(interval - elapsed) self._last_request_time time.time()每次请求前调用rate_limiter.wait()就能保证两次请求之间至少隔了一段时间并且间隔是随机的。这个思路和登录轮询里的随机抖动是同一个道理核心就是“别让请求序列表现出太强的机器规律”。最后说一句关于Cookie安全的题外话视频号登录Cookie就是你访问管理后台的完整凭证拿到它基本等于拿到了这个账号后台的访问权。不管你是把自己的脚本分享给朋友还是在技术社区提问务必先把Cookie内容脱敏再发出去。我见过有人直接把Cookie贴到工单里结果第二天账号被异地登录非常危险。另外还有个小技巧如果二维码过期了程序提示超时不需要杀掉进程重启直接在代码里捕获TimeoutError后重新调用一次login()即可。二维码有效期通常是5分钟超过这个时间必须重新生成。把这一步做成循环用户体验会好很多。扫码登录这件事远没有想象中那么玄乎理解了它的HTTP本质之后用Requests实现反而是最舒服的解法。
返回列表