背景:为什么我要跟Cookie死磕?
上个月接了个内部需求,要每天定时从公司用的一个项目管理平台(类似Jira)拉取任务数据,生成报表。这个平台没有提供官方API,只能通过网页端操作。手动操作太耗时,我就想写个Python脚本自动化。
一开始想得太简单:找到登录接口,POST用户名密码,拿到Cookie,然后用这个Cookie去访问数据页面。听起来很顺对吧?结果一上手就发现,这个网站的登录流程比我想的复杂得多------有CSRF token,登录后还有一次302重定向,而且Cookie的生效路径和域名都有讲究。我前后折腾了三天,把requests的Session对象、CookieJar以及HTTP状态码查了个遍,才终于让脚本稳定跑起来。
如果你也遇到需要处理复杂登录鉴权的网站,特别是那些用传统表单登录、有各种安全校验的,我踩过的这些坑或许能帮你省下不少时间。
问题分析:为什么直接发POST请求不行?
我最开始的代码长这样(已脱敏):
python
import requests
login_url = 'https://example.com/login'
data_url = 'https://example.com/api/data'
payload = {
'username': 'my_user',
'password': 'my_pass'
}
# 尝试1:直接登录
resp = requests.post(login_url, data=payload)
print(resp.status_code) # 输出200,好像成功了?
# 尝试用这个resp的cookies去访问数据
cookies = resp.cookies
data_resp = requests.get(data_url, cookies=cookies)
print(data_resp.status_code) # 输出403!被拒绝了。
第一个200状态码给了我虚假的希望。后来仔细看data_resp.text,发现返回的是登录页面的HTML------意思就是根本没登录成功,服务器把我踢回去了。
排查过程:
- 检查登录响应 :我打印了
resp.history,发现是一个空列表,说明没有重定向?不对,感觉有问题。 - 手动模拟对比:我用浏览器开发者工具,仔细对比了我的脚本请求和浏览器正常登录发出的请求。
- 发现关键差异 :
- 缺失的Token :浏览器登录时,POST数据里除了账号密码,还有一个叫
csrfmiddlewaretoken的字段,值是一长串字符。这个token是从登录页的HTML表单里提前获取的。 - Cookie的接收 :浏览器在
GET登录页面时,服务器就已经下发了一个sessionid的Cookie。后续的POST登录请求,会带上这个Cookie 一起发送。而我的脚本是直接POST,完全没理会登录页。 - 重定向处理 :浏览器登录成功后,会收到一个
302状态码,然后自动跳转到首页。我的脚本没有自动跟随重定向(requests默认是跟随的),但即便跟随了,因为前面的问题,跳转后的请求也缺乏正确的会话状态。
- 缺失的Token :浏览器登录时,POST数据里除了账号密码,还有一个叫
所以,核心问题不是"发送登录请求",而是模拟一次完整的浏览器会话:先获取页面和初始Cookie,提取隐藏的校验参数,再发起登录,并妥善处理登录后的跳转,最后维持这个会话状态去访问其他页面。
核心实现
1. 使用Session对象维持会话状态
这是我学到的第一个,也是最重要的教训:对于需要登录的连续请求,一定要用requests.Session()。
Session对象会帮你自动管理Cookies。它会在同一个会话内,自动将服务器返回的Cookie保存下来,并在后续的请求中自动带上,完美模拟浏览器的行为。不用你再手动去resp.cookies然后塞到下一个请求里。
python
import requests
# 创建会话,这是所有操作的基础
session = requests.Session()
# 可以设置一个通用的请求头,更像浏览器
session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
这里有个坑 :有些网站会检查User-Agent,用默认的python-requests可能会被拒绝。所以最好一开始就设置一个常见的浏览器UA。
2. 获取登录页并解析CSRF Token
很多Web框架(如Django、Flask)为了防御CSRF攻击,会在表单里埋一个随机token。你必须先访问登录页(GET请求),拿到这个token,才能用于接下来的POST请求。
同时,这个GET请求会让服务器下发初始的会话Cookie(比如sessionid),Session对象会自动为我们保存。
python
login_page_url = 'https://example.com/login'
# 第一次GET请求,获取页面和初始Cookie
login_page_response = session.get(login_page_url)
print(f"获取登录页状态码: {login_page_response.status_code}")
# 这里假设token在HTML中一个name='csrfmiddlewaretoken'的input标签里
# 我们使用lxml来解析(需要安装:pip install lxml)
from lxml import html
tree = html.fromstring(login_page_response.text)
# 尝试通过input标签的name属性获取csrf token
# 注意:不同网站token的字段名和获取方式可能不同,这里是常见情况
csrf_token = tree.xpath('//input[@name="csrfmiddlewaretoken"]/@value')
if csrf_token:
csrf_token = csrf_token[0]
print(f"成功提取CSRF Token: {csrf_token[:20]}...") # 打印前20位
else:
# 如果xpath没找到,可能需要检查网页源码,看看token藏在哪里
# 也可能网站用了其他名称,如`authenticity_token`, `_token`等
print("警告:未找到CSRF Token,可能需要调整解析方式!")
# 有时token可能在meta标签里://meta[@name="csrf-token"]/@content
注意这个细节 :token的名称和获取方式因网站而异。必须用浏览器的"检查元素"功能,仔细查看登录表单的源码 ,找到那个隐藏的<input>字段。这是成功的第一步。
3. 构造登录请求并正确处理重定向
现在,我们有了初始Cookie(在session里),也有了CSRF Token,可以构造POST请求了。
python
login_action_url = 'https://example.com/login' # 通常和登录页是同一个URL
payload = {
'username': 'your_username_here',
'password': 'your_password_here',
'csrfmiddlewaretoken': csrf_token, # 关键:把上一步提取的token加进去
# 可能还有其他隐藏字段,如`next`(登录后跳转地址)
}
print("正在发起登录POST请求...")
# 用session.post,它会自动带上GET登录页时获得的Cookie
login_response = session.post(login_action_url, data=payload)
# 重要:检查登录是否真的成功
print(f"登录POST请求状态码: {login_response.status_code}")
print(f"登录后重定向历史: {[r.status_code for r in login_response.history]}")
print(f"最终URL: {login_response.url}")
关键点分析:
login_response.history:如果登录成功,服务器通常会返回302或303状态码进行重定向。requests会自动跟随,并将这些中间响应记录在history列表里。最后一个响应才是重定向终点(如首页)的内容。所以,看到history里有302,且最终URL是登录后的页面(而不是登录页本身),是一个好迹象。login_response.url:检查最终停留的页面URL,如果还是登录页的URL,那大概率是登录失败了(账号错误、token无效等)。
4. 验证登录状态并访问目标数据
如何确认session已经处于登录状态?一个简单的方法是尝试访问一个需要登录才能看的页面。
python
# 访问一个登录后才能访问的页面,比如个人主页或API接口
profile_url = 'https://example.com/dashboard'
profile_response = session.get(profile_url)
print(f"访问个人页状态码: {profile_response.status_code}")
if profile_response.status_code == 200:
# 可以进一步检查返回内容里是否有登录用户的特征信息
if "欢迎回来" in profile_response.text or "Dashboard" in profile_response.text:
print("✅ 登录状态验证成功!")
else:
print("⚠️ 状态码200,但内容可能不对,需要检查。")
else:
print(f"❌ 访问失败,状态码: {profile_response.status_code},登录可能未成功。")
验证成功后,你就可以用这个已经携带了有效登录Cookie的session对象,去任意访问需要鉴权的接口了。
python
# 最终访问我们最初想要的数据接口
target_data_url = 'https://example.com/api/v1/tasks'
data_response = session.get(target_data_url)
if data_response.status_code == 200:
data = data_response.json() # 假设接口返回JSON
print(f"成功获取数据,共 {len(data)} 条记录。")
# ... 处理你的数据
else:
print(f"获取数据失败: {data_response.status_code}")
print(data_response.text)
完整代码示例
下面是一个整合了所有步骤、稍作抽象、可直接修改运行的完整脚本。请将your_username_here、your_password_here以及URL替换成你的目标网站信息。
python
import requests
from lxml import html
import time
class WebsiteLoginFetcher:
"""一个用于处理带Cookie登录的示例类"""
def __init__(self, base_url):
self.base_url = base_url.rstrip('/')
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/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
})
def get_csrf_token(self, login_page_url):
"""从登录页面HTML中提取CSRF Token"""
print(f"[1] 正在获取登录页面并提取Token: {login_page_url}")
try:
resp = self.session.get(login_page_url, timeout=10)
resp.raise_for_status() # 如果状态码不是200,抛出异常
except requests.exceptions.RequestException as e:
print(f" 获取登录页失败: {e}")
return None
# 使用lxml解析HTML
tree = html.fromstring(resp.text)
# 尝试几种常见的CSRF Token存放位置
token = None
# 方式1: 在input隐藏字段中
token_elements = tree.xpath('//input[@name="csrfmiddlewaretoken"]/@value')
if not token_elements:
# 方式2: 其他常见名称
for name in ['csrf_token', '_token', 'authenticity_token', 'csrf']:
token_elements = tree.xpath(f'//input[@name="{name}"]/@value')
if token_elements:
break
if not token_elements:
# 方式3: 在meta标签中
token_elements = tree.xpath('//meta[@name="csrf-token"]/@content')
if token_elements:
token = token_elements[0]
print(f" 成功提取Token: {token[:30]}...")
else:
print(" 警告:未能在页面中找到CSRF Token。可能需要手动分析页面结构。")
# 打印部分页面源码以便调试
# print(resp.text[:2000])
return token
def login(self, login_url, username, password, extra_form_data=None):
"""执行登录操作"""
print(f"[2] 正在执行登录...")
# 1. 获取Token
csrf_token = self.get_csrf_token(login_url)
if not csrf_token:
print(" 登录中止:无法获取CSRF Token。")
return False
# 2. 构造登录数据
login_data = {
'username': username,
'password': password,
'csrfmiddlewaretoken': csrf_token, # 关键字段
}
if extra_form_data:
login_data.update(extra_form_data)
# 3. 发起登录POST请求
try:
# 注意:有些网站登录后不希望自动重定向,可以设置`allow_redirects=False`先看看
resp = self.session.post(login_url, data=login_data, timeout=15, allow_redirects=True)
except requests.exceptions.RequestException as e:
print(f" 登录请求失败: {e}")
return False
print(f" 登录POST状态码: {resp.status_code}")
print(f" 重定向历史: {[h.status_code for h in resp.history]}")
print(f" 最终URL: {resp.url}")
# 4. 简单判断登录是否成功
# 条件1: 有重定向历史(通常302)
# 条件2: 最终URL不是登录页本身
if resp.history and login_url not in resp.url:
print(" ✅ 登录流程看起来成功(发生了重定向)。")
return True
else:
print(" ❌ 登录可能失败(未发生预期重定向或仍停留在登录页)。")
# 可以打印响应内容的前500字符帮助调试
# print("响应内容预览:", resp.text[:500])
return False
def fetch_protected_data(self, data_url):
"""使用已登录的session访问受保护的数据"""
print(f"[3] 正在访问受保护数据: {data_url}")
try:
resp = self.session.get(data_url, timeout=10)
print(f" 数据请求状态码: {resp.status_code}")
if resp.status_code == 200:
return resp.text # 或者 resp.json()
else:
print(f" 请求失败,响应内容: {resp.text[:200]}")
return None
except requests.exceptions.RequestException as e:
print(f" 数据请求异常: {e}")
return None
# ========== 使用示例 ==========
if __name__ == '__main__':
# !!!请替换为你的实际信息 !!!
BASE_URL = 'https://example.com' # 网站根地址
LOGIN_PAGE_URL = f'{BASE_URL}/login' # 登录页面地址
USERNAME = 'your_username_here'
PASSWORD = 'your_password_here'
TARGET_DATA_URL = f'{BASE_URL}/api/some_data' # 你想爬的数据地址
fetcher = WebsiteLoginFetcher(BASE_URL)
# 执行登录
is_logged_in = fetcher.login(LOGIN_PAGE_URL, USERNAME, PASSWORD)
if is_logged_in:
print("\n登录成功!开始获取数据...")
# 可以加个短暂延迟,避免请求过快
time.sleep(1)
data = fetcher.fetch_protected_data(TARGET_DATA_URL)
if data:
print("数据获取成功!")
# 这里处理你的数据,比如解析JSON或HTML
# print(data[:500]) # 打印前500字符看看
else:
print("数据获取失败。")
else:
print("\n登录失败,请检查账号、密码、网络或网站结构是否变化。")
踩坑记录
-
坑1:忽略初始GET请求和CSRF Token
- 现象:直接POST账号密码,返回200但实际未登录。
- 原因:没获取登录页下发的初始Cookie和表单里的隐藏Token。
- 解决 :老老实实先
GET登录页,用lxml或BeautifulSoup解析出Token,并用Session对象维持Cookie。
-
坑2:重定向(302)处理不当
- 现象 :登录请求返回200,但
history为空,url还是登录页。 - 原因 :网站可能用了JavaScript跳转,或者登录失败时也返回200并展示错误信息在登录页。
requests的自动重定向只针对3xx状态码。 - 解决 :不要只看最终状态码。检查
response.history是否有302,并对比response.url是否跳转到了登录后的页面(如首页、仪表盘)。更可靠的方法是登录后立即访问一个需要登录的页面来验证。
- 现象 :登录请求返回200,但
-
坑3:Cookie作用域(Domain/Path)问题
- 现象 :登录成功,用
session访问/dashboard正常,但访问/api/v1/data却返回403。 - 原因 :服务器设置的Cookie可能指定了
Path(如Path=/web)或Domain。requests.Session的CookieJar会严格按照这些属性来匹配和发送Cookie。如果API的路径不在Cookie的Path范围内,请求就不会带上这个Cookie。 - 解决 :用
print(session.cookies)查看所有Cookie的详细信息。如果真是作用域问题,可能需要调整请求的URL,或者与后端沟通(如果是自己的项目)。对于爬虫,有时需要"欺骗"一下,但需注意合规性。
- 现象 :登录成功,用
-
坑4:Session未正确复用
- 现象 :我把登录的代码写在一个函数里,函数内部创建了
session并登录成功。但在函数外部,我又用了一个新的requests.get去访问数据,结果失败。 - 原因 :
requests.get()是独立的函数调用,不会使用函数内部创建的那个session对象的状态。 - 解决 :将
session对象作为类的属性(如上面的示例)或全局变量,确保在整个流程中使用的都是同一个session实例。
- 现象 :我把登录的代码写在一个函数里,函数内部创建了
小结
搞定Python requests的Cookie登录,核心就两点:用Session对象维持状态 ,完整模拟浏览器的"GET取Token -> POST带Token登录 -> 跟随跳转"流程 。过程中最花时间的往往不是写代码,而是用开发者工具仔细分析网站的请求细节。下次再遇到,你就知道该从哪里下手了。如果想更深入,可以研究一下如何处理更复杂的认证(如OAuth 2.0)、验证码自动识别,或者使用selenium应对纯JavaScript渲染的登录场景。