news 2026/10/14 17:02:44

手把手教你用Python和FastAPI封装图书馆座位预约系统API

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手把手教你用Python和FastAPI封装图书馆座位预约系统API

简介:面向Python与Golang开发者的济南大学图书馆座位预约系统API开源项目,针对图书馆选座场景中的HTTP接口调用与自动化预约问题,提供一套可运行的爬虫级RESTful客户端实现。包体共9个文件,包括4个Python脚本、2个Go文件以及README、License、Git配置,压缩包仅16KB,代码量精简;其中libapi目录下的Python版本负责核心请求封装、签到逻辑与工具函数,既支持Python 2也支持Python 3,Go文件则可作为备用的客户端实现。项目新版后端改用HMAC认证,通过复用请求头中的HMAC内容实现认证绕过,并将签到流程一并保留。因此读者可以借此研究真实校园系统中的接口鉴权、请求签名、爬虫会话维持与异常排错,也可基于现有封装快速改造出自己的座位预约工具。目前已有1605人学习,适合希望从实际案例入手掌握Python/Go网络编程、RESTful API对接和爬虫调试技巧的开发者。

1. 从“抢不到座位”到“自己写API”:这个项目到底在做什么

如果你在高校图书馆待过,大概率经历过这种场景:早上七点半的预约系统,点进选座页面,明明看到十几个空位,手一抖刷新,全变成了灰色。我把这个月反复上演的“抢座大战”梳理了一遍,发现大多数人输在了手速和页面刷新频率上,而不是输给了同学。于是某开发者做了一个叫“UJN-Lib-Seat-API”的东西——把图书馆座位预约系统背后那套网页和小程序交互的逻辑,封装成一组程序员可以直接调用的HTTP接口。这篇文章要讲的,就是这类API背后的原理、如何从抓包开始落地一套能用的小工具,以及你在做同样事情时可能会踩进去的坑。

这个方向适合谁?第一类是每天和图书馆座位死磕的学生,想写个脚本定时抢座;第二类是学校的开发者或学生组织,想做一个更顺手的预约查询入口;第三类是对接口封装感兴趣、想练手逆向分析的开发者。它不解决“座位不够”的问题,但能让你从“人肉刷新抢座”变成“程序准点提交”,把运气成分降到最低。

下面的内容全部基于“抓包官方预约系统→模拟请求→封装成API服务”这条常规路径。我没有某高校内部系统的文档,也不需要它——因为这类预约系统的交互模型高度相似,只要把一个登录接口和一个座位查询接口跑通,剩下的都是体力活。

2. 座位预约系统的工作原理:先搞懂官方小程序在做什么

2.1 从选座到签到,官方接口的调用链其实很固定

高校图书馆座位预约系统虽然各有各的界面,但背后的调用链逃不出四个环节:登录态获取、馆区与座位查询、提交预约、签到确认。

第一步是登录。不管是小程序还是Web网页,它们内部几乎都是先请求一个“登录”或“会话创建”接口,拿到一个token或者session id。这个凭证在后续所有接口请求里都会带在Header或者Cookie里,用来证明“你是你”。第二步是查询。你需要先拿到图书馆有哪些楼层、哪些区域,再拿到某个区域的座位列表,列表里的每个座位带一个状态字段:可预约、已占用、暂离、维修。第三步是提交预约。选中一个状态可用的座位,带上时间段、学号信息,POST给服务器。第四步是签到,一般要求你到图书馆后扫码或者点“签到”按钮,实际上就是再调一次签到接口。

这个链路里真正麻烦的不是接口数量,而是参数构造。很多系统的查询接口要求传入“日期+时段+区域编码”,而区域编码不是给你看的,是开发者在后台硬编码的数字。你如果不抓包,根本不知道那个区域下拉框里显示的“二楼东区”对应的zone_id到底是1002还是A02。所以任何这种项目的起点,都应该是抓包,而不是猜参数。

2.2 为什么不用浏览器自动化,而是直接模拟HTTP请求

你可能听说过Selenium、Playwright这类浏览器自动化工具。它们确实能模拟点击和输入,看起来更“傻瓜”。但我做这类项目时几乎不会用它们做核心链路,原因有三个。

第一是重量级问题。浏览器自动化需要拉起一个完整的浏览器内核,内存占用动不动几百兆。而图书馆预约系统通常在校内服务器上,接口本身轻得很,你用requests一个进程占几十兆内存,就能同时跑好几十个预约任务。第二是稳定性问题。浏览器自动化依赖页面元素的选择器,只要前端工程师把某个div的class从seat-item改成seat_item_2024,你的脚本就全挂了。而直接调HTTP接口,只要后端参数不变,前端怎么改都不影响你。第三是速度。浏览器自动化从打开页面到完成点击,最快也要几百毫秒。直接构造POST请求,在校园网环境下通常50毫秒以内就能完成“提交预约”这个动作。抢座的时候,这中间的差距就是能不能抢到七点半开馆那一批座位的生死线。

当然,浏览器自动化也不是一无是处。当系统用了非常复杂的滑块验证码,或者接口参数做了动态加密,直接模拟HTTP会很痛苦,这时候用浏览器自动化反而能绕过去。但在做完调研之前,我的第一选择永远是抓包+模拟请求。这个选择也直接决定了你的项目结构:一个纯后端服务,加上一层简单的HTTP路由,就是大多数“预约系统API”项目的全部。

3. 抓包与登录:拿到能用的Token是第一步

3.1 用抓包工具定位登录和查座接口

我在做这类API封装时,第一步永远是开着抓包工具,把登录、选座、预约、签到这四个动作各做一遍,然后对着请求记录整理出一张接口清单。常用的抓包工具是Charles或mitmproxy,手机连上代理后打开小程序,电脑上就能看到流量。如果你用的是Web端,那更简单,浏览器F12打开开发者工具,切到Network面板就行。

先登录一次,找到那个返回token字段的请求。再进选座页面,找到那个返回座位列表的请求。记录下它们的URL、请求方法、Header和Body的key。这一步不需要你自己发明任何东西,完全是记录和整理。我把常见做法整理成一张表:

动作常见接口特征关键返回字段
登录POST,url含login或authtoken / sessionId / cookie
查询区域GET或POST,url含area/zone/buildingareaId, areaName
查询座位GET或POST,url含seat/list/statusseatId, status, timeRange
预约POST,url含reserve/booking/submitorderId, code
签到POST,url含sign/checkin成功状态码

整理完这张表,你就能回答一个关键问题:这个系统值不值得做。如果接口数量在5个以内,参数构造简单,那就值得往下走。如果每个请求都要带一个动态加密的sign参数,而且你花了一下午都定位不到它的生成逻辑,那我的建议是放弃直接模拟,回到浏览器自动化的路线上去。

3.2 用Python实现登录与Session保持:最小可运行代码

拿到接口清单后,就可以开始写代码了。我用requests库时有个习惯:用requests.Session()而不是直接requests.get()。Session会自动保存服务端下发的Cookie,在后续请求里一并带上,省去手工管理Cookie的麻烦。下面是登录环节的最小实现:

import requests # 创建一个Session,后续所有请求都挂在它下面,Cookie会自动管理 session = requests.Session() # 登录接口的URL和参数名是抓包拿到的,这里用占位表示 login_url = "https://lib-seat.school.edu.cn/api/login" login_payload = { "studentId": "20240001", "password": "your-password", # 有些系统要求传验证码或设备类型 # "captcha": "abcd", # "deviceType": "web" } # 模拟浏览器的基本Header,部分系统会校验User-Agent session.headers.update({ "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)", "Content-Type": "application/json" }) resp = session.post(login_url, json=login_payload) data = resp.json() # 学校系统的登录接口返回结构各异,但通常会有一个token字段 if data.get("code") == 0 or data.get("token"): token = data.get("token") # 有些系统要求手动把token加到Header里 session.headers.update({"Authorization": f"Bearer {token}"}) print("登录成功") else: print("登录失败:", data.get("msg"))

这段代码的逻辑很直白:第一步创建Session,第二步拼登录参数,第三步发请求,第四步从响应里取token并更新到Header。参数说明里最需要注意的是studentId和password这两个字段名,不同的系统叫法五花八门,有的叫username,有的叫account,以你抓包看到的实际字段为准。如果响应里没有token,而是直接Set-Cookie,那更好——Session会自动把Cookie存下来,你什么都不用做。

登录成功之后,我会立刻做一件事:写一个get_seats函数,查一次座位列表,确认这个Session是可用的。这一步非常关键,因为很多系统的登录接口返回“成功”,但Session没过多久就失效。如果你查座位的请求返回401或者“未登录”,说明你的登录姿势有问题,最可能的原因是漏掉了某个Header,或者登录接口本身还会下发一个refresh_token的逻辑。

4. 把核心逻辑封装成API服务:FastAPI版本的UJN-Lib-Seat-API

4.1 项目目录与依赖:一个py文件也能跑

当登录和查座跑通之后,下一步就不是继续写零散的脚本了,而是把它们封装成一个有明确接口定义的HTTP服务。这样你可以在手机上用浏览器访问,也可以让定时任务来调它,甚至分享给其他同学。我习惯用FastAPI来做这一层封装,原因很简单:它自带OpenAPI文档,你在浏览器里访问/docs就能看到一个带“Try it out”按钮的调试页面,对不熟代码的人特别友好。

这个项目不需要复杂目录结构,我一般用一个main.py加一个requirements.txt就够。依赖只有两个:fastapi和uvicorn,最多加一个requests。如果你要用定时任务,后面再加apscheduler。先把依赖固定好:

pip install fastapi uvicorn requests

这里不推荐pip install flask吗?不是不行,而是FastAPI的异步支持和请求参数校验更省事。你定义一个/seats接口,用pydantic模型声明参数后,FastAPI会自动帮你把“参数缺了”或“参数类型不对”这种错误挡在函数外面,你只需要关心业务逻辑。写原型阶段这是巨大的时间节省。

4.2 核心路由设计:查座、预约、取消、状态

写入代码时,我把前面验证过的两个基础操作——登录和查座位——封装成两个内部函数,然后暴露成四个路由接口。这样设计的好处是:HTTP层只负责参数解析和结果返回,真正的“脏活”都集中在lib_seat_client这个类里,如果官方系统改参数了,你只需改一处。

from fastapi import FastAPI, HTTPException from pydantic import BaseModel import requests app = FastAPI(title="Library Seat API") class SeatClient: """封装图书馆预约系统的所有HTTP交互""" def __init__(self, base_url: str): self.base_url = base_url self.session = requests.Session() self.logged_in = False def login(self, username: str, password: str) -> bool: # 实际实现参考第3章的代码,这里做简化 resp = self.session.post(f"{self.base_url}/login", json={ "studentId": username, "password": password }) if resp.status_code == 200 and resp.json().get("token"): self.session.headers.update({"Authorization": f"Bearer {resp.json()['token']}"}) self.logged_in = True return True return False def get_seats(self, area_id: str, date: str, time_slot: str) -> list: # 查询座位列表,返回原始JSON resp = self.session.get(f"{self.base_url}/seats", params={ "areaId": area_id, "date": date, "timeSlot": time_slot }) return resp.json() # 为每个请求会话创建一个独立的Client实例,避免串号 client_pool = {} app = FastAPI() class LoginRequest(BaseModel): username: str password: str class ReserveRequest(BaseModel): username: str password: str seat_id: str date: str time_slot: str @app.post("/login") def login(req: LoginRequest): client = SeatClient("https://lib-seat.school.edu.cn/api") ok = client.login(req.username, req.password) if not ok: raise HTTPException(status_code=401, detail="登录失败,请检查账号密码") client_pool[req.username] = client return {"status": "ok"} @app.get("/seats") def seats(username: str, area_id: str, date: str, time_slot: str): client = client_pool.get(username) if not client or not client.logged_in: raise HTTPException(status_code=401, detail="请先登录,再查座位") return client.get_seats(area_id, date, time_slot)

这段代码把“登录”和“查座”做成了两个独立接口。注意我在内存里维护了一个client_pool字典,以用户名作为key,存各自的SeatClient实例。这么做是为了避免多个人同时使用时Session互相污染。实际生产你会用Redis做这个存储,但原型阶段用一个字典足够了。参数说明:login接口入参是username和password,返回的status: ok表示登录成功;seats接口需要四个参数,分别是用户名、区域ID、日期和时间段。区域ID就是你抓包拿到的那种编号,不知道的话先从/areas这类接口查,或者直接看官方小程序选座页面的请求参数。

4.3 启动服务并用curl验证:从代码到可调用接口

写完main.py后,启动服务是一个命令的事:

uvicorn main:app --host 0.0.0.0 --port 8000

--host 0.0.0.0表示监听所有网卡,这样你在校园网的任何一台设备上,用http://你的IP:8000就能访问到。--port 8000是端口号,如果被占用就换一个,比如--port 8010。

启动后先别急着写前端,用curl验证一下接口的连通性。先登录:

curl -X POST http://127.0.0.1:8000/login \ -H "Content-Type: application/json" \ -d '{"username":"20240001","password":"your-password"}'

看到{"status":"ok"}说明登录接口正常。然后查座位:

curl "http://127.0.0.1:8000/seats?username=20240001&area_id=1002&date=2025-05-20&time_slot=1"

如果返回的是带着seatId和status的座位列表,恭喜你,一个能用的预约API已经跑起来了。这一步做完,等于整个项目打完了地基。剩下的预约、取消、签到,都是往SeatClient里加方法的事——从抓包记录里找到那些接口的URL和参数,照着get_seats的写法复制一份,换掉URL改成POST就行。

5. 避坑手册:UJN-Lib-Seat-API落地时的5个翻车现场

5.1 现象:登录成功但预约报错“参数缺失”

你按照第3章的代码登录成功了,查座位也返回了数据,但一提交预约,服务器报错说“参数缺失”。翻遍自己的代码,发现该传的参数都传了。

原因:这类系统常见一个隐藏参数——timetableId或者scheduleId。它不在你抓包时看到的“座位列表”的顶层,而是藏在每个座位对象里面。你只看到座位能选,却忽略了它旁边那个告诉你“这场能约”的排期ID。预约接口要的不是“座位ID+日期+时段”,而是“排期ID+座位ID”,你缺的就是这个排期。

解决:去抓包记录里找“提交预约”那一条请求的Payload。你会发现里面有一个字段是查座列表响应里没有的。查座列表的返回数据里,每个座位节点会有一个schedId之类的唯一值。把那个字段取出来,加到你构造预约参数里去。我的习惯是写代码前先人工模拟一次完整的选座→提交预约,把请求日志保存下来,和代码打印的参数做对比,用diff工具找出没对齐的字段。

5.2 现象:查到的座位是死的,半小时前的空座还在展示

到了下午两点,想约下午四点的座位,接口返回“有空座”,但点进去预约时提示“该座位已被预约”。反复刷新都一样,好像数据根本不同步。

原因:官方查询接口有个缓存机制。你请求座位列表时,它返回的是缓存数据,并不是实时数据库状态。缓存间隔通常是30秒到5分钟。你把请求频率压到10秒一次,自然拿到的全是旧数据。

解决:不要试图绕过缓存,而是接受这个现实。做出提示时,不要描述为“可直接预约的空座”,而是“对方快照里可选的座位”。实际提交预约接口时会进行真正的校验并返回明确的失败码。所以你的客户端逻辑要做成:先查列表,点击预约,如果预约接口返回“已被占用”,再自动拉一次最新列表,把那个座位标记为已占用。这就叫“乐观预约”。

5.3 现象:同一时间高频率请求,IP被官方暂时封禁

抢座高峰期,你每秒发出十几个请求给查座接口,起初一切正常,过了一分钟,所有接口都返回“请求频率过高”或直接超时。

原因:学校系统大多接入了基本的限流逻辑,或者服务器在网关层设置了请求频率阈值。查座接口被设计成轻量高频,但它并没有承诺给脚本用。你把它的请求频率打到人类操作十倍以上,触发封禁是预期内的事。

解决:把请求间隔设置成不低于1秒。如果要抢某个时间点的大量座位,不要在预约前10秒疯狂刷列表,而是提前1分钟把座位列表拉下来,然后在预约开放的瞬间直接提交。提交预约这个接口的限流阈值通常比查询接口宽,因为它的设计预期就是“一批学生同时抢”。如果已经被封禁,通常等5分钟就会自动解禁。千万不要在这种时候继续加大频率,会变成“封禁时间翻倍”的恶性循环。

5.4 现象:session在夜间自动过期,定时任务大面积失败

你写了一个凌晨定时任务,每天7:25自动登录抢7:30开放的座位。前三天跑得好好的,第四天突然全挂了,日志显示“登录失败”或“token失效”。

原因:系统有一个“账户安全策略”,检测到“凌晨登录”这种异常行为,会拒绝下发新token,或者把旧token全部吊销。你前一天跑完任务后没有退出登录,第二天凌晨再用同一个Session去登录,系统认为这台设备“一直在活动”,和你上次离开时间矛盾,就强制失效了。

解决:在每次抢座任务结束之后,主动调用一次“退出登录”接口,把Session关闭。下次使用前重新登录,不要长期复用Session。另外,在定时任务的异常处理里加一个“iffirst login fails, wait 3 seconds and retry once”的逻辑,因为夜间网络波动也会导致登录响应丢失,但重试会成功。

5.5 现象:验证码时有时无,导致自动化脚本不稳定

抓包的时候登录接口没验证码,你把自动化写好了。结果第二天跑,登录返回“需要验证码”。你一脸懵,因为前一天根本没见过这个参数。

原因:很多预约系统的验证码是多级策略。它会在“风险时间段”或“连续失败次数”触发时启用验证码,比如凌晨登录、异地IP、密码错误一次后再登录。平时它不出现,不代表它不存在。

解决:在登录接口的响应里增加一层“是否需要验证码”的判断。如果返回码提示需要验证码,就停止自动化,并推送一个通知给用户,让人工介入。不要尝试在原型阶段接入打码平台,因为学校系统的验证码识别成本高,且抢座场景下人工输入反而更可控。更务实的方式是:把定时任务的时间点设置在系统估算的“低风险时段”,并保证密码正确无误,减少验证码弹出的概率。

6. 进阶玩法:定时抢座、多账号与稳定性验证

6.1 用APScheduler把抢座变成定点触发

如果你的目标是每天准点抢座,那服务里一定要有一个定时任务。APScheduler是Python里最常用的调度库,我把抢座逻辑封装成一个函数,然后让它在每天早上7:29:50秒触发一次:

from apscheduler.schedulers.background import BackgroundScheduler import datetime, time def grab_seat_task(username: str, seat_id: str, area_id: str): client = client_pool.get(username) if not client: print(f"[{datetime.datetime.now()}] 用户未登录,无法抢座") return # 预约开放前10秒开始刷新座位状态 for attempt in range(5): seats = client.get_seats(area_id, "2025-05-20", "1") target = [s for s in seats if s["seatId"] == seat_id] if target and target[0]["status"] == "free": client.reserve(seat_id) print(f"[{datetime.datetime.now()}] 提交预约成功") return time.sleep(2) scheduler = BackgroundScheduler() # 每天7点29分50秒触发一次,用于提前刷新座位状态 scheduler.add_job(grab_seat_task, "cron", day_of_week="mon-fri", hour=7, minute=29, second=50, args=["20240001", "A-102", "1002"]) scheduler.start()

这里参数含义要拆开讲:day_of_week="mon-fri"是指周一至周五触发,图书馆周末开馆时间可能不同,需要另配一条cron规则。hour=7, minute=29, second=50是触发时间,我特意定在7:29:50。如果座位是7:30才开放提交,你要在开放前把座位列表拉下来——这样提交那一刻接口不拥堵。attempt循环里的time.sleep(2)是避免高频请求被限流,一共最多尝试5次,每次间隔2秒,总耗时约10秒。

这个方案尊重了一个大前提:预约开放时间是你从抓包或者官网页面上确认过的“硬事实”。不同的城市和高校,座位开放时间可能是7:30,也可能是10:00,以系统实际行为为准,不要照抄代码里的时间点。

6.2 验证接口返回的可靠性:响应校验与错误码映射

定时任务做出来后,如果你不想每天早上醒来收到一堆失败告警,就要花一点时间做响应校验。我在SeatClient的每个公开方法里,都加了一层“响应格式校验”。核心思路是:先判断HTTP状态码,再判断业务状态码,最后再判断关键字段是否存在。

def _validate_response(self, resp: requests.Response): if resp.status_code != 200: raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:200]}") data = resp.json() # 业务状态码约定为:0成功,非0失败 if data.get("code") is not None and data.get("code") != 0: raise RuntimeError(f"业务错误,code={data.get('code')}, msg={data.get('msg')}") return data

这个方法把所有接口的“失败形态”收敛成了一个统一的RuntimeError,你在上层只需要捕获这个异常,然后按第5章说的几种情况分别处理:code=401时重新登录,code=403时记录“某个座位已被占”,code=429时做退避重试。很多人在写这类API的时候,只校验了HTTP状态码,没校验业务状态码,结果HTTP返回200,但body里明确写了“预约失败”,脚本却当成功处理了。这是最伤人的一种错误,因为它不报错,只是静默地让数据错下去。

最后一章我留着讲验证,还有一个原因:做这类项目,最担心的不是“跑不通”,而是“跑通了但结果是错的”。我吃过一次亏,定时任务连着提醒我三天“预约成功”,到现场才发现那三天的座位全都没约上。原因就是我只判断了HTTP 200,没有去看业务返回里的success字段是true还是false。后来我给自己定了个规矩:所有接口成功判断,必须同时满足“HTTP 200”和“业务状态码为成功值”两个条件,缺一个都算失败。希望这一点也能帮到你少踩我踩过的坑。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 17:02:34

华为S5700交换机固件批量升级:SFTP与自动化脚本实践

1. 为什么批量升级固件这件事值得认真对待手里管着几十台接入层交换机,每次固件升级都是一场硬仗。厂商发来安全通告说某个版本修复了若干缺陷,你看着机房里那一排排设备,心里盘算的是:一台一台手动传固件、改启动项、重启、验证&…

作者头像 李华
网站建设 2026/10/14 17:02:20

RabbitMQ实战:从演示环境到生产级消息可靠传输的桥梁

简介:这是一份演示MFC应用集成RabbitMQ的示例工程,由开发者根据网上零散资料自行梳理完成,目标是在Windows桌面程序中实现消息队列的发送与接收。项目遵循AMQP协议,通过RabbitMQ客户端库(rabbitmq.4.lib/dll&#xff0…

作者头像 李华
网站建设 2026/10/14 17:01:15

手动搭建MySQL MHA高可用架构:从主库宕机到自动切换实战

1. 为什么手动搭建MHA依然值得学1.1 从一次主库宕机说起凌晨两点,监控告警响了,主库连接数直接掉零。当时我们那套业务用的是单点MySQL,没有做任何高可用。运维同学从被窝里爬起来,手动切从库、改配置、重启服务,前后折…

作者头像 李华
网站建设 2026/10/14 17:00:38

低功耗继电器选型指南:电池供电设备的功耗陷阱与五大避坑点

去年做一款电池供电的户外阀门控制器时,我在低功耗继电器选型上栽了个大跟头。设备计划装两节CR123A锂电池,续航目标定在一年,每天只需要开关阀门几次。方案评审的时候,同事随手拿了一颗5V继电器过来,说这点负载绝对够…

作者头像 李华
网站建设 2026/10/14 16:59:30

MES与ERP接口设计说明书:文件交互、排他控制与实绩回传避坑指南

简介:面向SAP R/3与制造执行系统(MES)集成场景的接口设计说明书,以MELEBUS-BMAS模板中追加开发的MES I/F程序为核心,适合负责R/3-MES接口设计与联调的实施顾问和开发人员阅读。资源为1个DOC文档,压缩包约17…

作者头像 李华