简介:本资源是一套基于Python开发的图书馆自动预约座位系统完整源码及配套文档,面向计算机相关专业本科生、研究生及初学者,解决高校图书馆座位紧张、人工抢座效率低等实际问题,适用于毕业设计、课程设计、期末大作业及自动化脚本学习实践。压缩包共18个文件,含2个核心Python脚本(clockIn_lib.py为主逻辑,test.py为测试模块)、1个GitHub Actions工作流配置文件(Clock_in.yml)、4个XML配置文件(.idea目录下IDE设置)、7张关键操作界面截图(assets/目录),以及README.md和requirements.txt等说明与依赖文件,整体大小仅1.73MB,轻量易部署。已有623人下载学习,代码经实测可稳定运行,支持广州大学图书馆自动预约,并集成pushplus+微信公众号消息推送功能,提供从环境配置、Secrets设置到启用Workflow的完整闭环方案,附详细图文配置指引与FAQ排错建议,具备直接复用与二次开发基础。 大学图书馆抢座这事,经历过的人都懂。早上七点开馆,六点半就得在系统里蹲着,手速稍微慢一点,靠窗的、有插座的位置就全没了。我写这个基于Python的图书馆自动预约座位系统,就是为了把这种重复性的抢座操作交给程序去跑,到点自动登录、自动选座、自动提交,整个过程不需要人工干预。项目本身不算复杂,但对新手来说是一个很完整的Python实战案例,涉及网络请求、会话维持、定时任务、配置管理这些经典知识点,拿来练手或者改造成其他预约场景都非常合适。
整个项目源码加使用说明打包在一个zip里,解压之后目录结构很清晰。核心逻辑就三个模块:登录模块负责处理校园网账号认证,预约模块负责构造请求参数并提交选座,调度模块负责在指定时间触发预约动作。另外还配了一个简单的配置文件,馆区编号、座位号、预约时间段都写在里面,改配置就能换座位,不用动代码。
这篇文章我会把这套系统的完整设计思路、核心代码实现、还有我在实际开发和运行中踩过的坑都梳理一遍。适合有一定Python基础、想做一个完整实战项目的读者,也适合正在为图书馆抢座烦恼的同学直接拿去改改用。
1. 系统整体设计思路与功能拆解
1.1 需求分析:自动预约到底要解决什么问题
先理清楚这个系统要解决的核心痛点。图书馆预约座位系统的常规操作流程是:打开预约网页或小程序,输入学号和密码登录,选择校区、馆区、楼层,然后在一堆座位图里找到目标座位,选择时间段,最后点击提交。整个过程看起来不难,但真正抢过座的人都知道,难的是在放票瞬间完成这一系列操作。
人工操作的瓶颈在于反应速度和操作链路太长。从刷新页面到最终提交,中间要经历多次页面跳转和请求往返,哪怕手速再快,总耗时也要十几秒。而热门座位往往是秒没的,所以核心需求就是在放票瞬间,用最快的速度把预约请求发出去。
基于这个分析,系统的设计目标就很明确了:第一,预先把登录态准备好,避免抢座时还要现登录;第二,提前构造好预约请求的全部参数,到点直接发送;第三,精确控制发送时机,误差控制在毫秒级。这三点是系统设计的核心逻辑,后面所有代码都是围绕它们展开的。
1.2 方案选型:为什么用Python+requests而不是浏览器自动化
确定了需求之后,接下来是技术方案选型。现在主流的自动化方案有两类:一类是模拟浏览器操作,比如Selenium、Playwright,另一类是直接模拟HTTP请求,比如requests库。
我最终选择了requests模拟请求的方案,原因有三点。第一是速度优势,requests直接构造和发送HTTP请求,没有浏览器内核加载、渲染、执行JS的开销,在抢座这种对时间极其敏感的场景里,快几十毫秒都是优势。第二是资源占用少,Selenium需要拉起一个完整浏览器进程,内存占用动辄几百MB,而requests脚本跑起来也就几十MB,放在服务器或树莓派上长期运行都很轻松。第三是依赖简单,requests基本是零依赖,装好就能跑。
当然,模拟请求方案也有前提条件,就是必须先了解预约系统的请求流程,搞清楚登录接口返回什么、提交预约时带了哪些参数。这需要在浏览器开发者工具里做一次完整的人工操作,把关键请求记录下来。这个过程其实也是对预约系统的一次逆向分析,我说说具体怎么做。
1.3 模块划分:三个核心模块加一个配置文件
经过上面的设计,整个项目划分成三个核心模块,外加一个配置文件和一个入口脚本。
- login.py:登录模块。负责向认证接口发送学号和密码,获取登录凭证(通常是Cookie或Token),并保存到本地供预约模块使用。
- reserve.py:预约模块。负责构造预约请求的参数,向预约接口发送选座请求,并解析响应结果判断是否预约成功。
- scheduler.py:调度模块。负责在指定时间点触发预约操作,支持一次性任务和周期性任务。
- config.json:配置文件。集中管理学号、密码、馆区编号、座位号、预约时间等参数。
- main.py:程序入口。串联整个流程,按顺序完成登录、参数准备、定时调度等步骤。
这种模块化设计的好处是职责清晰、便于维护。比如换了新的馆区,只需要改配置文件里的馆区编号;预约接口变了,只需要修改reserve.py里的请求参数构造部分,其他模块不受影响。
2. 核心细节解析与实操要点
2.1 登录与会话维持:Cookie和Token的处理方式
登录是整个自动预约流程的第一步,也是最容易出问题的环节。大多数校园预约系统采用两种认证方式:一种是纯表单登录,输入账号密码后服务端返回一个302重定向,设置Cookie;另一种是token认证,登录成功后返回一段加密字符串,后续请求需要带在Header里。
处理这两种方式,requests库都有对应的解决方案。对于Cookie方式,我用requests.Session()来维持会话,Session对象会自动保存服务端返回的Cookie,并在后续请求中自动带上,不需要手动拼接Cookie头。对于token方式,则需要在登录成功后,从响应内容中提取token值,保存到变量或配置文件里,在构造预约请求时添加到Header的Authorization字段中。
实际操作里还有一种情况,就是登录成功之后,预约系统内部还会跳转一次,生成一个新的会话凭证,这个凭证会放在URL参数里或者隐藏表单字段中。遇到这种情况,需要在登录后手动跟随这一步跳转,提取出真正的预约凭证,再保存下来备用。这个凭证一般有效期在半小时到几小时之间,所以我的做法是,在调度模块触发预约前才执行登录,而不是提前很久登录。
2.2 预约请求的构造:参数从哪里来
预约请求的构造是整个系统最关键的部分。很多人第一次写这类脚本时,会在网上找各种抓包工具,其实没必要那么复杂。用Chrome浏览器打开预约系统页面,按F12打开开发者工具,切换到Network面板,勾选Preserve log,然后手动完成一次预约操作。操作过程中产生的所有请求都会记录在面板里,找到那个提交预约的POST请求,点开看它的Payload和Request Headers,就能拿到请求参数格式。
这些参数五花八门,常见的有:usr_id或userid(用户ID)、seat_id(座位编号)、start_time(开始时间)、end_time(结束时间)、date(预约日期)、term_id(学期编号)等。其中有一些是固定的,比如座位编号和馆区编号,可以直接写死在配置文件里。有一些是动态的,比如日期,需要在程序运行时用datetime模块动态生成。还有一些可能是加密的,比如带签名或哈希值的参数,这类参数相对麻烦,需要分析前端JS代码找到生成逻辑。
我在开发时遇到过一个典型情况,预约请求里有一个token参数,但这个token和登录token不是同一个,而是用户点击某个座位后,前端向服务端发请求获取的临时凭证。解决办法是,在预约前先模拟点击座位的请求,从响应中提取这个临时token,再带着它去提交预约。
2.3 定时触发的策略:怎么把时间精度控制在毫秒级
定时触发是另一个核心环节。Python里做定时任务的常见方案有三种:time.sleep循环、APScheduler库、以及直接用系统级定时任务(如crontab或Windows任务计划程序)。
对于抢座场景,我最推荐的是APScheduler的DateTrigger或CronTrigger。APScheduler的调度精度在毫秒级,而且支持一次性任务和循环任务,配置灵活。我们可以在程序启动时,解析配置文件中设定的预约时间,然后创建一个DateTrigger,让预约函数在精确时间点被调用。
具体来说,是这样的思路:程序启动后先完成登录和参数准备,然后计算目标时间与当前时间的时间差,调用APScheduler的add_job方法,把reserve函数注册为一次性任务,trigger设置为DateTrigger(run_date=目标时间)。到达指定时间后,APScheduler会在独立线程中调用reserve函数,完成预约请求发送。
还有一个细节值得注意:预约操作本身需要时间,包括构造请求、建立连接、发送数据等,这些加起来可能需要几十毫秒甚至上百毫秒。所以真正发送预约请求的时间,应该比设定的放票时间提前一点。这个提前量需要根据实际网络环境调试,我在校园网环境下测试下来,提前200毫秒比较合适,放票瞬间请求刚好到达服务器。
3. 实操过程与核心环节实现
3.1 环境准备与依赖安装
开发环境是Python 3.8及以上版本,需要安装的第三方库只有两个:requests和APScheduler。安装命令很简单:
pip install requests apscheduler如果没有pip权限,可以用国内镜像源加速安装:
pip install requests apscheduler -i https://pypi.tuna.tsinghua.edu.cn/simple项目解压后的目录结构是这样的:
library_seat_reservation/ ├── main.py ├── config.json ├── login.py ├── reserve.py ├── scheduler.py └── requirements.txtrequirements.txt内容如下:
requests==2.31.0 APScheduler==3.10.13.2 配置文件设计:把易变参数从代码里抽离
我把所有可能变化的参数都放到了config.json里,这样改配置不需要动代码,更加安全,也方便其他人使用。配置格式如下:
{ "user": { "student_id": "20210001", "password": "your_password" }, "reserve": { "library_id": "LIB_MAIN", "floor": "3F", "seat_id": "A102", "date": "2024-06-15", "start_time": "08:00", "end_time": "22:00" }, "scheduler": { "reserve_time": "2024-06-15 07:00:00", "advance_seconds": 0.2 }, "options": { "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "timeout": 10, "max_retries": 3 } }这里面的reserve_time是需要重点关注的参数。以我所在学校为例,每天早晨7点整开放当天座位的预约,所以我会把reserve_time设置为当天6点59分59秒左右,给程序留出登录和准备的时间。提前量advance_seconds设置为0.2秒,也就是在放票前200毫秒发送请求。
3.3 登录模块实现:解析认证接口并保持会话
登录模块的核心代码是负责向认证系统发送账号密码请求。这里以最常见的表单登录为例,写一个演示版本。
import requests import json class LoginClient: def __init__(self, config): self.config = config self.session = requests.Session() self.session.headers.update({ "User-Agent": config["options"]["user_agent"], "Origin": "https://lib.example.edu.cn", "Referer": "https://lib.example.edu.cn/login" }) self.base_url = "https://lib.example.edu.cn" def login(self): login_url = self.base_url + "/api/auth/login" payload = { "student_id": self.config["user"]["student_id"], "password": self.config["user"]["password"] } resp = self.session.post(login_url, json=payload, timeout=self.config["options"]["timeout"]) resp.raise_for_status() data = resp.json() if data.get("code") == 200: token = data["data"]["token"] self.session.headers.update({"Authorization": "Bearer " + token}) print("[OK] 登录成功,获取token") else: raise RuntimeError("登录失败: " + data.get("msg", "未知错误")) return self.session这个模块里有两个细节值得展开说。
第一,Session对象的复用很重要。我把requests.Session()对象保存在self.session里,登录成功后,这个Session自动携带了服务端下发的Cookie。后续预约请求都用同一个Session发送,服务端就能识别出这是同一个登录用户,不会因为Cookie缺失而提示未登录。
第二,登录接口的返回格式需要提前确认。有的系统返回JSON,有的返回XML,有的直接在Response Header里设置Set-Cookie。如果返回的是JSON,那就从JSON中提取token或用户ID;如果用的是Cookie,就不用做任何提取,Session会自动管理。我建议在写代码前,先用手动请求在Python交互环境里试一次登录,把返回内容完整打印出来,确认格式后再写提取逻辑。
3.4 预约模块实现:构造参数并提交选座请求
预约模块是整套系统的核心,它在收到调度模块触发后,立即构造预约请求并发送。下面是一个简化版实现,演示了如何处理固定参数与动态参数。
import datetime import json class ReserveClient: def __init__(self, config, session): self.config = config self.session = session self.base_url = "https://lib.example.edu.cn" def get_available_seat_token(self, seat_id): # 有些系统在提交预约前,需要先获取该座位的临时token url = self.base_url + "/api/seat/token" params = {"seatId": seat_id} resp = self.session.get(url, params=params, timeout=10) data = resp.json() if data.get("code") == 200: return data["data"]["seatToken"] else: raise RuntimeError("获取座位token失败") def reserve(self): reserve_time = self.config["reserve"] today = datetime.date.today() if reserve_time.get("date"): date_str = reserve_time["date"] else: date_str = today.strftime("%Y-%m-%d") # 动态参数准备 start_str = f"{date_str} {reserve_time['start_time']}:00" end_str = f"{date_str} {reserve_time['end_time']}:00" payload = { "libraryId": reserve_time["library_id"], "floor": reserve_time["floor"], "seatId": reserve_time["seat_id"], "date": date_str, "startTime": start_str, "endTime": end_str, "seatToken": self.get_available_seat_token(reserve_time["seat_id"]) } url = self.base_url + "/api/seat/reserve" resp = self.session.post(url, json=payload, timeout=10) result = resp.json() if result.get("code") == 200: print(f"[OK] 预约成功,座位: {reserve_time['seat_id']}") return True else: print(f"[FAIL] 预约失败: {result.get('msg')}") return False这里有一个很重要的设计思路,就是临时token的获取。在我实际使用的预约系统里,用户点击某个座位后,前端会先向后端发送一个请求来获取该座位的唯一标识或临时token,然后才允许提交预约。直接提交而不带这个token,会返回“参数不合法”的错误。所以我在reserve方法里,先调用get_available_seat_token拿到token,再拼接到正式的预约请求里。
如果实际项目里的预约系统没有这个前置流程,删除这一行就行。所以拿到自己的预约系统后,一定要先通过抓包确认具体请求流程,再对照调整代码,不能直接照搬。
3.5 调度模块实现:使用APScheduler做毫秒级触发
调度模块负责让预约函数在设定的时间点精确执行。我选择APScheduler的DateTrigger来做一次性调度。代码如下:
from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.date import DateTrigger from datetime import datetime import time class ReservationScheduler: def __init__(self, reserve_client, config): self.reserve_client = reserve_client self.config = config def run(self): reserve_time_str = self.config["scheduler"]["reserve_time"] advance = self.config["scheduler"]["advance_seconds"] target_time = datetime.strptime(reserve_time_str, "%Y-%m-%d %H:%M:%S") # 计算提前触发的时间 fire_time = datetime.fromtimestamp(target_time.timestamp() - advance) print(f"[INFO] 预约任务已注册,将在 {fire_time.strftime('%Y-%m-%d %H:%M:%S.%f')} 触发") scheduler = BlockingScheduler() scheduler.add_job( func=self.reserve_client.reserve, trigger=DateTrigger(run_date=fire_time), id="reserve_job", misfire_grace_time=None ) scheduler.start()这里有几个参数需要注意。misfire_grace_time设置为None,意思是如果任务因为某些原因错过了预定时间,不执行补偿调度,直接放弃。这对于抢座场景是合理的,因为放票时间一旦错过,再执行就没有意义了。
另一个细节是advance_seconds提前量的计算。在实际运行时,我会先把预约接口的响应时间测出来:在非高峰期手动调用一次预约接口,用time.time()记录请求发送前和响应返回后的时间差。如果这个时间差是300毫秒,那就把提前量设置为0.3秒左右,确保请求到服务器的实际时刻正好是放票时刻。
3.6 入口脚本:串联整个流程
最后是入口脚本main.py,负责加载配置、创建登录客户端、执行登录、创建预约客户端、启动调度器。
import json from login import LoginClient from reserve import ReserveClient from scheduler import ReservationScheduler def load_config(path="config.json"): with open(path, "r", encoding="utf-8") as f: return json.load(f) def main(): config = load_config() login_client = LoginClient(config) session = login_client.login() print("[INFO] 登录状态就绪") reserve_client = ReserveClient(config, session) scheduler = ReservationScheduler(reserve_client, config) scheduler.run() if __name__ == "__main__": main()整个流程是串行的:加载配置、登录拿会话、构造预约参数、注册定时任务、启动事件循环。当定时时间到达,预约函数被执行,程序打印预约结果后,可以手动中断进程退出。
4. 常见问题与排查技巧实录
4.1 登录总是不成功,明明账号密码都对
这是新手最容易遇到的问题。账号密码在浏览器里能正常登录,但在脚本里就是提示认证失败。这个问题的根源,大多数时候不是密码错了,而是登录请求的格式不对。
预约系统的登录接口一般有三种格式:表单格式、JSON格式、XML格式。如果服务端期望的是表单格式(Content-Type为application/x-www-form-urlencoded),而你用json=发送请求,服务端解析不到用户名字段,自然返回认证失败。解决办法非常简单,把json=换成data=,requests库就会自动把字典编码为表单格式。至于怎么判断该用哪种格式,看抓包记录里登录请求的Content-Type头部就行。
还有一类情况是登录页面本身有加密逻辑,比如密码通过前端的JavaScript做了某种变换(MD5、RSA加密等)后才发送到服务端。这种需要阅读前端JS代码,找到加密函数,在Python里用hashlib或cryptography库复现同样的加密逻辑,再把加密后的结果放到请求里。
排查这类问题有个通用技巧:用Python的requests库发送登录请求后,把服务端返回的完整响应打印出来,对照浏览器里实际登录时的响应内容,逐字比较差异。差异点往往就是问题所在。
4.2 预约接口报错:缺少必要参数或参数格式错误
预约请求构造完成之后,提交时可能会遇到“缺少参数”或“参数格式错误”的报错。这个问题通常是因为对接口参数理解不准确。
举例来说,有的接口期望的时间格式是“2024-06-15 08:00:00”,有的期望是“2024-06-15T08:00:00”,还有的期望是时间戳。如果格式不对,服务端解析失败就会报错。我的排查经验是,不要只看抓包面板里显示的参数值,还要注意请求的Content-Type。如果是JSON格式,要确认嵌套层级和字段名大小写完全一致;如果是表单格式,要确认每个字段的顺序和名称。
这里分享一个调试技巧:先用requests直接构造一个写死的请求,带上从抓包里抄下来的参数,不改任何字段。如果这个请求能成功预约,则说明方向和接口路径都没问题,接下来再逐个把写死参数替换为动态参数,每替换一个就测试一次,这样很快能定位到是哪个参数导致的问题。
4.3 定时任务不触发或触发时间不准
APScheduler触发的精度很高,但如果发现预约时间不准,大概率是提前量设置不合理或系统时间不同步。
先看系统时间。如果运行脚本的电脑或服务器时间本身就和标准时间有偏差,定时触发自然不准。解决方法是先在命令行执行date命令(Linux)或查看Windows任务栏时间,确认系统时间和北京时间一致,必要时开启自动同步时间功能。
再看提前量。如果你的实际请求耗时是500毫秒,提前量却设置为0.2秒,那请求到达服务器时已经晚了300毫秒,热门座位可能已经被抢走。建议做几次测试,用time.time()记录请求发送前后的时间差,取平均值作为提前量。另外,如果程序运行地点的网络延迟不稳定,可以稍微加大提前量,宁可请求早到一点,也不要晚到。
4.4 长时间运行后会话失效,请求提示未登录
有些图书馆预约系统的会话时长比较短,可能是30分钟或1小时,超过这个时间,登录凭证就会失效。我在实际开发中遇到过,配置好定时任务后,提前一小时就启动了程序,结果到了预约时间点,会话早已过期,预约失败。
解决方法有两种。第一种简单粗暴,就是不要提前太长时间启动程序,等预约时间快到了再启动,确保登录后立刻执行预约。第二种更稳妥,就是在detect到请求返回“未登录”或“会话过期”的错误时,自动重新执行登录流程,然后重试预约。我推荐用第二种,因为实际使用中难免会有时间差。
可以在reserve方法里加入一个异常处理分支:
def reserve(self): # ... 构造payload ... resp = self.session.post(url, json=payload, timeout=10) result = resp.json() if result.get("code") in (401, 403): print("[WARN] 会话过期,尝试重新登录") self.session = self.login_client.login() return self.reserve() # 递归重试一次 ...不过要控制递归层级,最多重试两三次,避免陷入死循环。
5. 项目学习价值与合规扩展建议
5.1 从这个小项目能学到什么
这套系统虽然小,但里面的技术点放在任何Python项目中都是通用的。首先是requests库的会话管理,这是爬虫和自动化脚本的必修课,理解了Session如何携带Cookie,后面学Scrapy框架会轻松很多。其次是APScheduler定时任务的用法,这不只能抢座位,做数据备份、定时报表、自动化监控都用得上。
再往深了说,抓包分析接口的过程,锻炼的是逆向思维和排查能力。拿到一个陌生系统,怎么找到登录接口、怎么确认参数格式、怎么处理动态参数,这套方法论学会了,以后不管遇到什么预约类、抢购类的系统,都能快速上手分析。
项目里还有配置分离、异常处理、日志打印这些工程化习惯,虽然看起来很简单,但很多初学者写代码时容易忽略。把它们融入到实践中,代码的可维护性和健壮性会有明显提升。
5.2 合规使用与改造方向
写这类自动化脚本,必须注意使用边界。本文讨论的系统仅限用于学习Python编程、了解HTTP请求与定时任务机制,请勿将其用于破坏他人公平使用公共资源的场景,更不要用于抢课、抢票、刷单等影响公共秩序的行为。建议只在合理范畴内自用,比如帮助自己在图书馆预约一个座位,这本质上和其他人手动预约是一样的操作,只是用程序替代了人工点击。
如果想要拿这个项目做课程设计或毕业设计,可以考虑在现有基础上增加几个功能模块:图形化界面(用tkinter或PyQt)、预约记录数据库(用SQLite)、多座位备选策略(第一选择满了自动选第二选择)、预约结果推送通知(用企业微信机器人或邮件)。这些扩展方向都和学习的主线技术密切相关,做下来能写出一份有深度的项目文档。
5.3 部署到服务器需要注意什么
很多人会把这个脚本部署到云服务器或家里的树莓派上运行,省得每天开电脑。部署时有几个实际问题需要留意。
网络IP问题。预约系统如果做了IP限制,从服务器IP发起的请求可能会被拒绝或触发风控。我的建议是先手动用服务器IP访问一次预约系统,确认能正常打开页面再部署。如果确认有问题,就老老实实在本机定时运行。
服务器时区问题。云服务器默认时区可能是UTC,如果不改时区,定时触发时间会和北京时间差8小时。执行以下命令可以同步时区:
sudo timedatectl set-timezone Asia/Shanghai依赖安装问题。在服务器上使用同一套requirements.txt安装依赖时,建议用虚拟环境,避免和系统自带的包冲突:
python3 -m venv venv source venv/bin/activate pip install -r requirements.txt我个人在实际操作中的体会是,最理想的使用方式是:白天在图书馆用电脑自习时,让脚本在后台等着,到点自动预约;如果当天去不了,就提前找一台实验室或宿舍的机器挂着。毕竟这种系统的初衷是为了方便学习,而不是成为新的焦虑来源。
最后再分享一个小技巧:第一次跑通脚本后,不要急着第二天直接上战场。先在非高峰时段多测试几次预约、取消的完整流程,确认接口稳定性和参数正确性。预约系统是真实业务系统,大规模高频测试可能会给服务器造成不必要的负担,适可而止。这个项目里的思路和技术,完全可以迁移到其他学习场景中,你以后写爬虫、写自动化办公脚本、写定时任务工具时,会自然想起这套系统的实现细节。
本文还有配套的精品资源,点击获取