AI编程(四十五):登录和Cookie — 让程序带着”工牌”进门

作者:

在

上一篇的表单提交已经能”递单子办事”了,但很多网站更狠:单子都不让你递——先登录,才有资格查。账号密码递进去不难,难的是网站认完人发你一张”工牌”(Cookie),之后每次进门都得出示它。今天新概念只有一个:requests.Session——一个自动保管工牌、进门自动出示的请求会话。学完这篇,需要登录的查询也能自动化:登录一次,工位一坐,把该查的都查完。

一、真实场景:每季度一次的”学时查询”折腾

你所在单位有个”继续教育平台”,每季度要查一次自己的培训学时:登录(用户名+密码)→ 点”我的学时” → 看着页面上的数字抄下来 → 填进单位的汇总表。你一个人查一次也就两分钟,但你们科室 15 个人,每到季度末都来问你:”帮我看看我学时够不够?”——账号密码一个个发过来,你一个个登录、一个个查、一个个回复,一上午就没了。

更气人的是:这平台的”我的学时”页面不登录根本打不开——直接访问网址,它把你踢回登录页。上一篇学的 requests.post 在这里只能递登录表单,递完之后呢?程序换下一个网址去查学时,服务器冷冰冰地回一句:”请先登录。”

问题出在哪?上一篇的程序是”一次性”的:每次请求都是陌生人上门,递完单子扭头就忘。 而真实世界的规矩是:你登录时,前台认出你,发你一张工牌;之后你去每个窗口办事,都得把工牌亮一下,工作人员一看:”哦,是三楼的老王,进来吧。”

今天全部的内容就是这一件事:让程序学会登录、领工牌、揣好工牌、带着它反复进门查东西。 新概念只有一句话:

requests.Session() = 给程序配一个工牌夹:登录一次领到的工牌(Cookie),它自动收好,之后每次请求自动出示。

就这一句。剩下的全是老手艺:requests.post(第44篇)递登录表单、BeautifulSoup(第41篇)抠结果、csv(第25篇)存数据、try/except(第10篇)防崩、配置文件(第28篇)存账号密码。

二、新概念只有一个:Cookie 是工牌,Session 是工牌夹

登录到底发生了什么

先花三分钟看懂”登录”的内幕,写代码时就不慌了。你在登录页填用户名密码、点”登录”,浏览器其实干了两件事:

  1. 递表单:把用户名密码 POST 给服务器(就是上一篇的手艺,一点新东西没有);
  2. 收工牌、存工牌:服务器核对无误后,在响应里悄悄塞回一张”工牌”——浏览器把它收好;之后你每点开一个新页面,浏览器都自动把工牌带上去。

这张工牌,行话叫 Cookie——就是服务器发给你的一小段文字(通常是一串看不懂的字符,比如 session_id=a1b2c3...),含义就一句话:”这是 302 房间老王的通行证,有效期 2 小时。”

用 F12 亲眼看一下这场戏:登录后随便打开一个需要登录的页面,F12 → Network → 点那条请求 → Request Headers 里有一行 Cookie: session_id=a1b2c3...——那就是浏览器在出示工牌。你把这行删了再发请求?服务器立刻把你当陌生人,踢回登录页。

为什么 requests 默认”记不住”

坑就坑在这:requests.get() 每次调用都是一个独立的陌生人。你先 requests.post 登录、领了工牌,工牌拿回来躺在 resp 里没人管;下一个 requests.get 再去,两手空空——服务器当然说”请先登录”。程序不是故意忘事,是没有地方放工牌。

解决办法有两个,一个将就,一个正道:

将就版:手工抄工牌。 把 F12 里看到的那行 Cookie 原样抄进请求头:

import requests

headers = {"Cookie": "session_id=a1b2c3xyz"}   # 从 F12 抄的工牌
resp = requests.get("https://example.com/my-hours", headers=headers)
print(resp.text)

能用,但工牌是会过期的(服务器一般设 2 小时、1 天的期限)。过期了你得重新登录、重新抄、改代码里的那行字——抄错了、过期了,程序就”安静地死”(返回登录页而你没察觉)。临时救急可以,天天干活不行。

正道版:requests.Session——工牌夹。 这就是今天的新朋友:

import requests

s = requests.Session()          # 拿出工牌夹,之后都用 s,不用 requests

# 第一步:递登录表单(上一篇的手艺)
resp = s.post(
    "https://example.com/login",
    data={"username": "wang", "password": "123456"}
)
# 服务器发的工牌,s 已经自动收好了——不用你管

# 第二步:查学时——s 会自动出示工牌
resp = s.get("https://example.com/my-hours")
print(resp.text)                # 这回拿到的是登录后的内容了

三个细节,一个都不能少:

  1. s = requests.Session() 建一次就够,之后所有请求都用 s.get / s.post,别再用 requests.get——用回 requests.get 等于把工牌夹扔了、又空手去敲门。
  2. 工牌的保管是全自动的:s.post 登录后服务器发的 Cookie,s 默默收下;之后 s.get 任何网址,都自动带上。你代码里看不到”存工牌”这个动作,就像浏览器里也看不到一样。
  3. resp 还是那个 resp。resp.text、resp.status_code、resp.json()(第38篇)、BeautifulSoup 解析(第41篇)……你会的全部手艺照用。Session 改变的只是”请求之间记不记得住”,别的什么都没变。

类比记忆:浏览器是全自动钱包——登录领工牌、收好、每次出门自动带上,全程无感;requests.get 是每次翻口袋——上一趟能领到工牌,这一趟出门忘带了;requests.Session 是给程序配了个工牌夹——领了就夹好,出门自动摸得到。今天学的就是把工牌夹配上。

顺带一个福利:Session 还能复用底层连接,连续请求一串网址时比一次次 requests.get 更快、更省事。反正以后”登录型”的任务,开局第一行写 s = requests.Session(),就对了。

三、动手:先跟”回声墙”换工牌,再进自家门

照旧先在安全的地方练手。两步走:先用 httpbin 确认”领工牌—出示工牌”这套动作姿势正确,再自己捏一个”要登录的查询网站”实战。

第一步:httpbin.org —— 会发工牌的回声墙

httpbin 有个专门”发工牌”的接口 /cookies/set/名字/值,和一个”看你工牌”的接口 /cookies。用它把 Session 的动作看个真切:

import requests

# 实验1:用工牌夹(Session)
s = requests.Session()
r1 = s.get("https://httpbin.org/cookies/set/name/xiaoming")   # 领工牌
r2 = s.get("https://httpbin.org/cookies")                     # 出示工牌,让它报数
print("带工牌夹看到的:", r2.json())
# {'cookies': {'name': 'xiaoming'}}   ← 工牌还在!

# 实验2:不用工牌夹(裸 requests)
r3 = requests.get("https://httpbin.org/cookies/set/name/xiaoming")  # 领了工牌……
r4 = requests.get("https://httpbin.org/cookies")                    # 但出门没带
print("空手出门看到的:", r4.json())
# {'cookies': {}}   ← 工牌早丢了

跑一遍,看清楚两个 print 的差别——这就是今天新概念的全部内涵:Session 记得住,裸 requests 记不住。实验 2 里 r3 拿到的工牌其实就躺在 r3.cookies 里,只是 r4 不会自动带上它;Session 替你干的就是”收好、下次带上”这点事。

第二步:捏一个”要登录的学时查询网站”

新建 fake_login_site.py(需要 Flask:python3.14 -m pip install flask),和第44篇的假网站一个套路,只是多了道”门”:

# fake_login_site.py —— 需要登录的"继续教育平台"
from flask import Flask, request, make_response, redirect

app = Flask(__name__)

USERS = {"wang": "123456", "zhang": "abc123"}   # 假账号库

HOURS = {                                        # 假学时数据
    "wang": [("2026-09-01", "AI基础课", 8), ("2026-09-15", "数据处理课", 6)],
    "zhang": [("2026-09-03", "安全培训", 4)],
}

def logged_in_user(req):
    """看请求带没带有效工牌,带了返回用户名,没带返回 None"""
    return req.cookies.get("ticket") if req.cookies.get("ticket") in USERS else None

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        u = request.form.get("username", "")
        p = request.form.get("password", "")
        if USERS.get(u) == p:
            resp = make_response(redirect("/my-hours"))
            resp.set_cookie("ticket", u)        # 发工牌:工牌上写着用户名
            return resp
        return "账号或密码错误 重试"
    return """
        用户名: 
密码:
""" @app.route("/my-hours") def my_hours(): u = logged_in_user(request) if not u: return redirect("/login") # 没工牌?踢回登录页 rows = "".join(f"{d}{c}{h}" for d, c, h in HOURS[u]) total = sum(h for _, _, h in HOURS[u]) return f"""

{u} 的培训学时

{rows}
日期课程学时

合计: {total} 学时

""" if __name__ == "__main__": app.run(port=5001)

终端里跑起来:python3.14 fake_login_site.py。浏览器先手动走一遍:打开 http://127.0.0.1:5001/my-hours——被踢回登录页;登录后再看——学时表格出来了。手动确认网站好使,再写程序。

第三步:带工牌夹进门查学时

新建 login_spider.py:

# login_spider.py —— 登录一次,把学时查出来存 CSV
import csv
import requests
from bs4 import BeautifulSoup

s = requests.Session()

# 1. 登录:递表单,工牌由 s 自动收好
resp = s.post(
    "http://127.0.0.1:5001/login",
    data={"username": "wang", "password": "123456"}
)

# 2. 登录体检:确认真的进来了(疫苗在坑1)
resp = s.get("http://127.0.0.1:5001/my-hours")
if "培训学时" not in resp.text:
    print("登录失败或被踢出来了,快检查账号密码!")
    raise SystemExit(1)

# 3. 抠数据(第41篇的手艺)
soup = BeautifulSoup(resp.text, "html.parser")
records = []
for tr in soup.find_all("tr")[1:]:          # 跳过表头
    tds = [td.get_text(strip=True) for td in tr.find_all("td")]
    records.append(tds)
total = soup.find("p").get_text(strip=True)

# 4. 存 CSV(第25篇的手艺)
with open("我的学时.csv", "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.writer(f)
    writer.writerow(["日期", "课程", "学时"])
    writer.writerows(records)

print(f"查到 {len(records)} 条记录,{total},已存入 我的学时.csv")

跑一下:python3.14 login_spider.py——输出”查到 2 条记录,合计: 14 学时,已存入 我的学时.csv”。打开 CSV 确认数据没错。

注意第 2 步的”登录体检”:s.get 拿回 resp 后,先确认页面里有”培训学时”这个只有登录后才有的标记,没有就报错退出。这三行是整篇最值钱的部分——没有它,密码打错时程序不会崩,只会安静地把登录页当结果页抠出一堆空数据(坑1的疫苗)。

跟AI说需求的参考说法:

“这是一个需要登录的查询网站:登录页是 /login(POST 字段 username、password),登录后 /my-hours 才能看到学时表格。帮我用 requests.Session 写个脚本:登录、检查登录成功、把表格存成 CSV,密码从 config.json 读。”

四、改进:把”登录脚本”变成”季度照跑的工具”

三步升级,都跟AI协作完成:

  1. 密码不写死在代码里:账号密码挪进 config.json(第28篇的手艺),程序里 config = json.load(open("config.json")) 读出来用。同事换密码,改 json 就行;更重要的是——代码可以随便给人看、给 AI 看,密码不跟着泄(疫苗在坑3)。跟AI说:”账号密码从 config.json 读,文件不存在时给中文提示并退出。”
  2. 工牌过期自动重办:加一个”被踢回登录页就重新登录、再试一次”的小循环。核心思路是把”体检+查询”包成函数,查不到就重新 s.post 登录再查。跟AI说:”如果请求结果里出现登录页特征,就自动重新登录并重试一次,第二次还失败才报错。”(具体实现留作练习3)
  3. 查完自动汇报:末尾打印”用户 wang:2 条记录、合计 14 学时、已存 我的学时.csv”。汇报是脚本和工具的分界线——工具自己交代干得怎么样,脚本只顾闷头跑。跟AI说:”最后打印汇总:查到几条、合计多少学时、存到哪个文件。”

跑通之后,15 个同事的查询就是把第 1 步升级成”从名单文件读多组账号、循环查、结果合并成一张总表”——全是老手艺(循环第6篇、csv 第25篇),新概念一个没加。这就是系列的设计:每篇只加一个新零件,工具却一直在长大。

五、三个必踩的坑(先打疫苗)

坑1:登录失败了,程序却”一脸平静”。这是登录型程序的头号死法:密码打错、账号被锁、验证码弹出来——s.post 一律返回 status_code 200(”页面正常送达”),程序欢天喜地接着去抠数据,抠出来的是登录页的表单,CSV 一片空白。登录请求返回 200 不代表登录成功,它只代表”服务器收到你的表单了”。疫苗:登录后必须做”体检”——去抓一个”只有登录后才有的标记”(比如”培训学时”四个字、你的用户名),抓不到就大声报错退出。养成习惯:登录型脚本没有体检 = 没完工。

坑2:工牌过期,跑一半被”踢出去”。Cookie 是有寿命的(服务器说了算,常见几小时到几天)。季度末批量查 15 个人,跑到第 10 个时工牌过期,后 5 个请求全部”返回登录页”——如果没体检,又是一场静默灾难。疫苗:每次请求都顺手体检(或至少检查返回内容里有没有登录页特征),发现被踢就自动重新登录再继续,而不是一路错到底。另外手动抄工牌(headers={"Cookie": ...})的将就版,过期就得改代码——所以干活用 Session,抄工牌只用来临时救急。

坑3:账号密码硬编码在代码里,一朝泄密全网皆知。写完脚本顺手把代码贴到群里”请教一下”、上传到网盘、发给 AI 助手时连带密码一起晒——账号密码就跟着代码满世界跑。还有更严重的红线:别人的账号你不能碰。”帮 15 个同事查学时”没问题(人家把账号托付给你),但写循环去”试”别人的密码、批量注册、抢号——那不是自动化,那是违法(《刑法》第 285 条非法侵入计算机信息系统,真判刑)。疫苗三件套:密码放 config.json 或环境变量、config.json 进 .gitignore、带密码的代码哪都不发。程序是把好刀,只切自己案板上的菜。

六、跟AI协作的正确姿势(登录特供版)

  1. 把 F12 抓的”登录那一下”整个贴给AI:”这是登录请求,Form Data 是 username、password,请求网址是 /login,响应会 Set-Cookie。帮我用 requests.Session 写登录代码。”字段名、网址一个都不会错。
  2. 把”登录后页面”贴给AI当体检依据:”这是我登录成功后 /my-hours 的 HTML(贴 resp.text),怎么写一段代码确认’确实登录成功了’?”——AI 会挑出一个只有登录后才有的标记,替你把体检写好。
  3. 让AI把”过期重登”写成可复用的函数:”帮我写一个带自动重新登录的 Session 封装:请求后检查是否被踢回登录页,是则重新登录并重试一次。”这属于”改进步骤”,让AI一次写好,以后所有登录型脚本都能用。
  4. 密码怎么存,问AI:”账号密码不想写死在代码里,也不想明文躺在仓库里,有什么适合个人小脚本的办法?”——AI 会给你 config.json + .gitignore、环境变量等几种方案和各自的适用场景,选个最省心的。

七、今日小练习

练习1:跑通”换工牌”和假网站。先跑 httpbin 的两个实验,确认一个打印 {'cookies': {'name': 'xiaoming'}}、另一个打印 {'cookies': {}};再启动 fake_login_site.py,浏览器手动登录确认学时表格能看到;最后跑 login_spider.py,确认 CSV 里 2 条记录、合计 14 学时。

提示:如果 httpbin 两个实验打印一样,检查第二个是不是也错用了 s.get;如果假网站打不开,看终端里 Flask 是不是还在跑、端口 5001 有没有被占用;如果脚本报”登录失败”,先去浏览器手动登录一次确认密码没打错。

练习2:故意输错密码,看”体检”救你一命。把 login_spider.py 里的密码改成 "wrong" 跑一次——应该看到”登录失败或被踢出来了”的报错,干干净净退出。然后注释掉体检那三行再跑一次——注意观察:程序不报错,只是 CSV 里一行数据都没有,抠出来的是登录页的表单。对比一下:哪种失败更让你省心?

提示:能让你看见的失败是好失败,安静的失败能让你对着空 CSV 猜一下午。登录型脚本的体检,就是给程序装的烟雾报警器。测完记得把密码改回来、体检放出来。

练习3(动脑+选做):给工具装上”工牌过期自动重办”。按第四节第 2 步跟AI协作实现:把”体检+查询”包成函数,发现被踢回登录页就自动重新登录、重试一次,第二次仍失败才报错退出。验收标准:把 fake_login_site.py 里 resp.set_cookie("ticket", u) 那行改成 resp.set_cookie("ticket", u, max_age=1)(工牌 1 秒过期),跑你的脚本——它应该自己重新登录、照样把学时查出来。选做挑战:把脚本扩展成”从 账号名单.json 读多组账号、逐个查、汇总成一张总表”,体会”登录零件”复用起来有多顺手。

提示:核心就两招——”体检失败 → 重新 s.post 登录 → 重试原请求”,最多重试一次(不然网络真坏了会死循环)。写完先用正确的 set_cookie 跑一遍确认没改坏,再改 max_age=1 测自动重登。

下篇预告

下一篇(第46篇):下载文件 — 让程序把”资料”搬回家。前面拿到的都是文字和表格,但网上更多好东西是文件:PDF 通知、Excel 模板、图片素材。下一篇学”边下载边写盘”的搬运手艺:大文件、断点、批量下载,全用你已经会的 requests 再加一个”往文件里写”的动作。今天先把工牌夹配上——下一篇下载”需要登录才能拿”的内部资料时,Session 直接就能派上用场。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注