
做爬虫的人迟早要翻过“登录墙”这道坎。我早期用 requests 直接抓取需要登录的页面时经常收到一堆重定向提示或登录接口的响应数据没拿到反而要先跟网站的认证机制纠缠半天。后来在几个实际项目中反复折腾我才把模拟登录的实现方式收敛成三条清晰的路线一是基于 requests.Session 维持会话二是直接复用浏览器 Cookie三是从 Selenium 模拟浏览器操作。这篇文章就把这三条路背后的原理、完整代码、适用场景和踩坑经验一次性讲透正在做 Python 爬虫模拟登录的朋友可以直接拿去参考。1. 登录背后的运行逻辑我们到底在模拟什么1.1 HTTP 无状态和会话机制的渊源HTTP 协议本身是无状态的也就是说服务器默认不会记住你上一次请求是谁。登录系统的本质就是在“无状态”的协议之上人为制造出“有状态”的会话。浏览器第一次带着用户名密码请求登录接口时服务器在内存或数据库里创建一条会话记录生成一串唯一的标识通常叫 session_id然后通过响应头里的 Set-Cookie 把这个标识写回浏览器。浏览器后续请求自动携带这个 Cookie服务器找到对应会话记录才知道“还是你”。这个机制看似基础但我见过太多卡在登录环节的人代码没写错接口也调对了就是不理解自己为什么拿不到数据。本质上就是因为会话没有建立服务器眼里你只是一个陌生访客。搞懂这一点后面所有的模拟登录方案都能串起来。1.2 登录认证流程的共性结构无论目标网站多复杂登录流程基本可以抽象成四个步骤获取登录页、提取动态参数、提交认证信息、维持后续会话。其中第二步是新手最容易忽略的地方。很多网站为了防御跨站请求伪造会在登录表单里藏一个随机 token提交时要求带上有些网站甚至会把 token 放在 HTTP 响应头的自定义字段里。举个实际例子。我之前对接某垂直行业的信息平台时登录表单里潜伏着一个隐藏字段叫 csrf_token正常情况下不填也能打开登录页但真正提交登录请求时少了它服务器就返回“参数错误”。我一开始以为是密码问题反复核对账号密码都没用最后抓包比对才发现在登录请求体里多了一个神秘的字段。所以除非你百分百确定字段结构否则建议登录前先 GET 一次登录页用解析库提取页面源码里的隐藏字段再原样拼进 POST 数据里。1.3 会话令牌与认证机制的差异Cookie 只是承载会话标识的载体并不是唯一方案。现在越来越多系统改用 JWTJSON Web Token或自定义令牌机制。JWT 通常是把用户信息、过期时间用密钥签名后放进请求头 Authorization 里而不是靠服务端保存会话记录。这对爬虫的影响很大如果用 requests.Session 模拟的是“服务器端保存会话”的模式那遇到 JWT 认证的接口时重点就是拿到 token 字符串并在后续请求头里主动携带。判断目标网站用哪种认证方式方法也很简单登录成功后打开浏览器的开发者工具切到 Network 面板找一个需要登录才能访问的接口看它的请求头里是带 Cookie 还是带 Authorization。这个细节直接决定你应该走哪条模拟路线。2. 方式一requests.Session 提交表单最经典的会话维持方案2.1 Session 对象为什么能自动“续上”会话requests 库里的 Session 对象是一个可以跨请求保持状态的东西。它会在内部维护一个 CookieJar服务器每次通过 Set-Cookie 写回的 Cookie 都会被自动保存后续任何由这个 Session 发起的请求都会自动携带。这相当于是我们在代码里手动复刻了浏览器“打开页面→保存 Cookie→下次请求带上”的完整行为。相比每次 requests.get 单独发起请求Session 最大的优势就是不用手动拼接 Cookie 字符串也保证了连接复用带来的性能提升。2.2 一个可以直接套用的登录模板下面这段代码是我用 requests.Session 完成模拟登录的通用骨架目标站点用一个常见的表单登录示例替代import requests from bs4 import BeautifulSoup session requests.Session() # 伪装成真实浏览器很多反爬策略靠 User-Agent 做第一道过滤 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,en;q0.8, }) login_url https://example.com/login # 第一步请求登录页提取动态参数 resp session.get(login_url, timeout10) soup BeautifulSoup(resp.text, html.parser) token_input soup.select_one(input[namecsrf_token]) csrf_token token_input[value] if token_input else # 第二步构造登录数据并提交 login_data { username: your_username, password: your_password, csrf_token: csrf_token, } resp session.post(login_url, datalogin_data, allow_redirectsTrue) # 第三步通过 URL 变化判断登录是否成功 if resp.url ! login_url: print(登录成功当前地址:, resp.url) else: print(登录失败检查返回内容中的错误提示) # 第四步访问需要登录才能查看的页面 profile_resp session.get(https://example.com/profile, timeout10) print(受保护页面状态码:, profile_resp.status_code)这里的 allow_redirects 参数值得单独说明。很多网站登录成功后会自动 302 跳转到首页或用户中心如果设置成不跟随重定向你看到的响应其实是跳转前的那条中间响应容易让你误判登录失败。默认情况下 requests 是跟随重定向的但如果你自定义了某些请求配置务必确认这一项没有被动过。2.3 调试登录接口时的关键动作遇到登录失败我先做的永远是抓包对比。打开浏览器开发者工具手动在页面上登录一次从 Network 面板里找到那条 POST 请求逐字段对比自己代码里的 data 参数看有没有漏字段、字段名写错、或者提交的 Content-Type 不对。另外一个容易忽略的点是请求头里的 Referer 和 Origin。部分网站的登录接口会校验来源页如果 Referer 不是登录页本身服务器会直接拒绝。建议在 Session 创建后就把 Referer 固定为登录页地址这样后续跨页面的请求也能保持上下文一致。这类方案适合大多数表单登录的网站尤其是那些没有复杂加密参数、也没有滑块验证码的系统。它最大的优点是轻量和快速一个脚本轻轻松松就能撑起整个爬虫的数据采集流程。3. 方式二直接复用 Cookie跳过登录环节的直通车3.1 手动取 Cookie 的场景与原理如果说 requests.Session 是模拟浏览器完成登录的整个过程那么直接复用 Cookie 就是跳过这个过程直接从“已经登录”的状态开始。思路很简单你用浏览器正常登录一次从开发者工具里复制出登录后的 Cookie 字符串然后写死在爬虫代码里。这种做法在原理上完全合法——因为我确实是在模拟一个已经完成登录的真实用户。特别适合那些登录流程特别复杂——需要扫码、需要短信验证、或者有高强度加密参数——但一旦登录成功后普通数据接口又没什么反爬措施的网站。坦白讲这种办法最“破”但也最有效。我做过一个数据监控类的需求目标平台强制要求手机验证码登录requests 模拟登录的难度极高而每次手动用手机收验证码又太麻烦。最后方案就是用浏览器人工登录一次把 Cookie 提取出来写到配置文件中爬虫定时任务每次启动时直接加载这批 Cookie。整体效率提升了一个量级。3.2 从浏览器提取 Cookie 的正确方法手动提取 Cookie 用浏览器开发者工具就能完成。打开 Network 面板刷新页面随便点击一个请求在请求详情里的 Headers 选项卡中找到 Cookie 字段把它完整复制出来即可。更推荐的方式是直接在 Console 里输入 document.cookie不过要注意如果某个 Cookie 设置了 HttpOnly 属性document.cookie 是取不到的而 Network 面板里看到的请求头 Cookie 是完整包含 HttpOnly 字段的。所以需要完整 Cookie 时优先看 Network 面板。提取后的代码使用方式很简单import requests cookie_str sessionidabc123def456; csrftokenxxxx; uid123456; headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Cookie: cookie_str, } resp requests.get(https://example.com/profile, headersheaders, timeout10) print(resp.status_code, resp.text)也可以把 Cookie 字符串解析成键值对再用 requests 的 cookies 参数注入import requests from http.cookies import SimpleCookie cookie_str sessionidabc123def456; csrftokenxxxx; uid123456 cookie SimpleCookie() cookie.load(cookie_str) session requests.Session() session.cookies.update({k: m.value for k, m in cookie.items()}) resp session.get(https://example.com/profile, timeout10) print(resp.status_code)后者好处是后续仍然可以用 Session 对象保持连接且如果有某个接口需要单独设置 Cookie 时可以灵活覆盖。3.3 维护 Cookie 池的进阶操作手动复制 Cookie 最大的问题就是有效期。Session 类 Cookie 通常几小时到几天就过期一旦过期你的爬虫就会返回登录跳转页面。为了不让定时任务半夜悄悄挂掉我给这个方案做了一个简单的“Cookie 保鲜”机制在爬虫脚本里增加一个前置探测请求访问一个只有登录用户才能看的接口——比如“个人资料”或“我的订单”页面如果返回的状态码是 302 或响应内容里出现登录引导字样就立即告警通知人工介入重新登录并更新 Cookie 配置。更进一步的做法是维护一个 Cookie 池。比如你有十个账号每个账号在浏览器里登录后把 Cookie 以 JSON 格式存进数据库爬虫每次随机挑选一个 Cookie 某个 Cookie 失效就把它标记为不可用并触发补充。对于需要高频采集、且单账号有访问频率限制的场景这种方式能显著延长整个系统的可用时间。Cookie 复用方案的核心优势是开发效率极高完全绕开了复杂的登录逆向工作。缺点也摆在明面上Cookie 会过期、多个请求可能被服务器检测到异常、不能处理需要交互的操作。它适合用来做“短平快”的项目不适合做长时间无人工干预的稳定系统。4. 方式三Selenium 模拟浏览器用“笨办法”解决复杂登录4.1 为什么 requests 解决不了时要请出浏览器有些网站的登录流程根本不走常规表单。比如登录前要求先完成滑块拼图验证、拖动验证、点选文字验证或者前端 JavaScript 对密码做了一层层加密你在 Network 面板里看到的 password 参数已经被处理得面目全非。这时用 requests 模拟的性价比非常低——你需要逆向加密算法、模拟滑块轨迹特征、处理动态 token 的刷新每一步都可能踩坑。Selenium 的思路完全不同它直接驱动一个真实浏览器用户该干嘛它就干嘛。你看到的登录页面长得什么样它就操作什么元素。滑块验证、点击验证码这些操作本质上是在“真实环境中完成真实操作”服务器收到的请求和正常人类用户几乎没有区别。我把这种方式称为“最笨但最稳”的方案。笨在性能和资源消耗上——启动一个浏览器实例的内存开销远大于 requests 发请求稳在跨过了绝大部分前端逻辑的逆向工作只做最直接的“模拟操作”。4.2 Selenium 登录的完整示例下面是一个用 Selenium 完成模拟登录的示例代码依然以常规表单登录为例但你可以把其中元素定位和操作部分替换成实际的滑块或验证码操作from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC options webdriver.ChromeOptions() options.add_argument(--window-size1280,800) # 无头模式适合服务器环境但部分网站的滑块检测对无头浏览器更严格 # options.add_argument(--headlessnew) options.add_experimental_option(excludeSwitches, [enable-automation]) driver webdriver.Chrome(optionsoptions) try: driver.get(https://example.com/login) # 显式等待确保元素渲染完成 username_input WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.NAME, username)) ) password_input driver.find_element(By.NAME, password) username_input.send_keys(your_username) password_input.send_keys(your_password) # 提交表单 submit_button driver.find_element(By.CSS_SELECTOR, button[typesubmit]) submit_button.click() # 等待登录完成条件是 URL 发生变化或出现用户信息元素 WebDriverWait(driver, 10).until( EC.url_changes(https://example.com/login) ) print(登录成功当前地址:, driver.current_url) # 如果需要继续用 requests 抓取数据可以导出 Cookie cookies driver.get_cookies() for cookie in cookies: print(cookie[name], , cookie[value]) finally: driver.quit()代码里有两个点值得讲透。第一个是显式等待不要用 time.sleep 这种死等方式页面加载速度受网络波动影响太大一旦渲染稍慢就可能定位不到元素导致脚本崩溃。用 WebDriverWait 配合 expected_conditions能精确等待到“目标元素可交互”的那一刻。第二个是 excludeSwitches 参数它去掉 Chrome 的自动化提示标签有助于规避一部分基于浏览器指纹的自动化检测。4.3 滑块验证码和半自动化思路遇到滑块验证时Selenium 的拖动动作可以用 ActionChains 实现from selenium.webdriver import ActionChains slider driver.find_element(By.CSS_SELECTOR, .slider-block) ActionChains(driver).click_and_hold(slider).move_by_offset(xoffset260, yoffset0).release().perform()但是滑块能不能一次通过很大程度取决于目标网站的检测严格度。某些网站会检测拖动轨迹的加速度、停顿点、鼠标悬停位置简单的直线位移轨迹很容易被识别为机器操作。一个实用的规避思路是人为制造非线性轨迹把一次长距离拖动拆成多段短距离移动中间加入随机停顿。如果检测实在太严格我推荐采用半自动化模式Selenium 先自动填好用户名和密码到滑块验证处暂停人工在浏览器窗口里手动完成验证然后脚本继续执行后面的登录完成和 Cookie 导出。这种半人工方案每次大概需要人工介入几秒钟但能保证极高的登录成功率适合那些登录频率低但必须成功的关键场景。Selenium 方案最大的价值是通用性不管目标网站的登录逻辑多复杂只要浏览器能登录成功Selenium 理论上就能做。它的主要问题是速度慢、内存占用高、需要安装对应版本的浏览器驱动ChromeDriver 等以及在一些云服务器上需要额外配置显示环境。但对于“用 requests 根本解决不了”的场景它是绕不开的选择。5. 三种方式的横向对比与选型决策5.1 核心维度对比把三种方式放在一张表里比较选型思路就很清楚了对比维度requests.Session 提交表单直接复用 CookieSelenium 模拟浏览器开发成本较低需处理动态参数最低取 Cookie 即可较高需维护元素定位和等待逻辑运行速度快毫秒级响应快毫秒级响应慢秒级起步内存开销大长时间稳定性中等依赖 token 有效期较低Cookie 易过期需人工刷新较高但进程崩溃风险需要守护机制应对复杂加密弱需要逆向前端逻辑中直接跳过登录过程强真实执行前端逻辑应对验证码弱强登录人工完成中滑块可模拟复杂验证码需半自动被反爬识别风险中请求特征明显低但 Cookie 复用异常可能触发风控低行为更接近真人5.2 按场景选择更靠谱如果你面对的网站是常规表单登录没有复杂的加密和验证requests.Session 是首选。它轻量、可控、易维护适合构建长期稳定运行的爬虫系统。唯一的难点是需要处理登录页里的动态字段但这是可以靠解析页面提取解决的。如果你需要登录的目标网站有短信验证码、扫码登录、或者前端加密环节而你又不想在逆向上面花太多时间Cookie 复用方案是投入产出比最高的。我常把这套思路用在数据采集频率不高、但登录必须要人工介入的项目里。如果你的目标是复杂交互型网站比如需要过滑块验证、需要在页面上执行一系列操作才能看到数据、或者登录后还要处理动态加载的内容那就老老实实上 Selenium。它的“笨重”换来的是对前端复杂逻辑的最大兼容性。5.3 成本与风险的再权衡成本不只在代码开发阶段。requests.Session 方案后续维护成本在应对网站改版——只要登录表单的字段或接口地址变了你的代码就要跟着更新。Cookie 复用方案的成本在人工维护——Cookie 一过期就要重新登录。Selenium 方案的成本在资源和稳定性——你需要一个跑得动浏览器的环境还需要一个监控机制来保证浏览器进程没有因为异常崩溃而无人接管。风险评估同样重要。无论哪种方案都要控制请求频率避免对目标站点造成访问压力。我的习惯是给所有请求加一个随机延时比如在两次请求之间 sleep 2 到 5 秒同时严格遵循 robots.txt 的约定。爬虫技术本身是中性的怎么用才是关键。6. 真实项目中的组合拳一次从登录到采集的完整实践6.1 业务背景和方案切换过程去年我接到一个数据采集需求需要抓取某行业资讯平台的文章列表和详情内容但所有文章必须登录后才可查看。项目一开始我图省事直接用手动复制 Cookie 的方式上线了当时评估的是“反正数据一天只采集一次Cookie 应该能撑几天”。结果不到三天Cookie 就过期了定时任务开始拉回一串登录页的 HTML。我临时让客户重新登录并发了新 Cookie但心里清楚这种方案不可持续。这次经历促使我重构了整个采集流程。6.2 最终采用的双层结构最终的架构是 Selenium 和 requests.Session 的混合模式Selenium 负责登录并导出 Cookierequests 负责后续的数据采集。启动流程是这样设计的爬虫进程启动时先加载配置文件里保存的 Cookie用 requests 发起一个探测请求检查 Cookie 是否有效如果失效就调用 Selenium 模块执行登录登录成功后把新 Cookie 存回配置文件然后继续用 requests 完成本次采集。import json import requests def load_cookies_from_file(path): with open(path, r) as f: return json.load(f) def save_cookies_to_file(path, cookies): with open(path, w) as f: json.dump(cookies, f, indent2) def check_cookie_valid(session, probe_url): resp session.get(probe_url, allow_redirectsFalse, timeout10) # 如果跳转到了登录页说明 Cookie 失效 return resp.status_code 200 and login not in resp.headers.get(Location, ) def build_session_from_cookies(cookies): session requests.Session() for c in cookies: session.cookies.set(c[name], c[value]) return session # 主流程先加载后探测必要时触发浏览器登录 cookie_file cookies.json session build_session_from_cookies(load_cookies_from_file(cookie_file)) probe_url https://example.com/profile if not check_cookie_valid(session, probe_url): print(Cookie 已失效准备启动浏览器登录...) # 这里调用 Selenium 登录函数返回 cookie 列表 # new_cookies selenium_login() # save_cookies_to_file(cookie_file, new_cookies) # session build_session_from_cookies(new_cookies) # 开始采集 data_page session.get(https://example.com/articles, timeout10) print(data_page.text)这套结构跑了两周只出现过一次 Cookie 失效的情况那次系统自动切换到了 Selenium 登录全程无人干预。相比纯 Selenium 方案requests 负责的高频采集明显更快页面批量请求的耗时从原来的十几秒降到了两三秒。6.3 几个值得延续的细节习惯整个项目落地后我复盘出几条通用经验。首先是“探测请求”的设计——不要等采集请求失败才发现 Cookie 过期先用一个轻量级探测接口做健康检查可以省掉很多无意义的错误日志。其次是日志要记录清楚Cookie 失效、登录失败、页面结构变化都要有对应标记后续排查问题时能大大缩短定位时间。最后是接口地址和字段名尽量参数化别写死在代码各个角落改版时改配置文件比改业务逻辑轻松得多。如果你正在做一个需要模拟登录的爬虫项目我建议无论最初选哪种方式都预留一个 Cookie 导出和加载的接口。因为很可能你今天用 requests 能轻松登录明天网站就上了一道滑块验证码到时候你可以无缝切换到 Selenium 登录而不需要推翻整个采集模块重写。多留一条备用通道是爬虫项目长寿的关键。