公司动态

Python自动预约座位系统:从requests到定时调度的完整实战

📅 2026/8/31 19:08:06
Python自动预约座位系统:从requests到定时调度的完整实战
简介本资源是一套基于Python开发的图书馆自动预约座位系统完整源码及配套文档面向计算机类专业本科生、研究生及课程设计/毕业设计学习者解决高校图书馆座位紧张、人工抢座效率低等实际问题。压缩包共18个文件1.73MB含2个核心Python脚本clockIn_lib.py为主程序、test.py为测试用、1个GitHub Actions工作流配置文件Clock_in.yml、4个XML配置文件.idea项目元数据、7张操作截图assets目录及README.md等说明文档结构清晰便于理解自动化流程与部署逻辑。已有623人下载学习资源经实测可稳定运行支持广州大学GZHU图书馆预约并集成pushplus微信公众号消息推送功能提供从环境配置、Secrets设置到启用Workflow的全流程实践参考特别适合作为毕设、课设或自动化脚本开发的入门与进阶范例。 很多人第一次听到“基于Python的图书馆自动预约座位系统”会下意识觉得这是不是个灰产工具其实它就是个很典型的自动化脚本项目登录图书馆座位预约平台、定时发起请求、按偏好筛选座位、自动提交预约全程不需要人盯在屏幕前。真正做过的人都知道这类项目最大的价值不是“抢座”本身而是把Python的请求库使用、会话保持、定时调度、异常重试、日志记录这些基本功全都串起来了一个项目练到位比东敲一个例子西看一篇教程要高效得多。这个系统适合两类人一类是有真实抢座需求的学生早起开馆那几分钟手速永远比不过别人用脚本解决实际痛点另一类是Python初学者到进阶过渡阶段的学习者想知道requests到底怎么落地、session和cookie在真实场景里怎么用、定时任务为什么不能只写个sleep这个项目的源码能给出很直观的答案。我拿到源码后完整跑了一遍又把里面几个关键模块拆开重写了一份这篇文章就把整个系统的设计逻辑、跑通步骤、以及我在调试过程中踩过的坑都聊一遍。1. 自动预约背后的真实场景3秒抢空的座位是怎么被脚本拿下的先把这个系统的业务背景讲透。现在绝大多数高校图书馆都采用了线上预约制读者必须在小程序或者Web端先选定某个时段的某个座位到馆后扫码签到才算真正落座。热门图书馆的热门时段尤其考试周和期末季放座通道开启后基本三五秒内位置就被清空。手动抢座的流程极其机械打开预约页面、登录、选择自习室、滑动列表找空座、点提交、偶尔还会碰到验证码或者接口繁忙。这串操作让人来点每一步都要几百毫秒等你看清哪个座位是空的黄花菜都凉了。而脚本的链路就清晰很多登录后拿着Token或Cookie去请求座位查询接口把返回的JSON数据解析出来筛选出符合条件的座位ID再直接提交预约请求。人的反应速度是秒级脚本是毫秒级这就是差距所在。这个项目的源码结构其实并不复杂我整理下来核心链路是这样的加载配置文件账号、密码、目标自习室、目标时段、座位偏好执行登录请求保存会话状态Cookie或Token有的平台要先做一次预登录拿CSRF Token建立定时调度在放座时间点前几秒开始预请求预热连接避免开抢瞬间握手超时轮询可预约座位列表对比当前时间、座位区域、是否靠窗/插座等偏好条件命中目标后立即发送预约请求校验返回结果是否成功失败时按配置的重试次数和退避间隔进行重抢整个过程写入日志可扩展消息推送邮件、钉钉、Server酱等一句话概括它把你在浏览器里那些重复点击替换成了有逻辑、有策略、可控制的HTTP请求序列。从源码里能看出作者在设计时已经考虑过“请求频率反爬”问题代码里加入了一个很关键的设计——请求间隔的动态调整。举个例子平时轮询空闲座位时每8到10秒请求一次但到了放座前5秒会切换成高频模式每0.3秒请求一次抢座成功后立刻恢复低频。这种设计既避免了平时被平台封IP又保证了关键时刻的请求密度。2. 关键模块拆解登录、查座、抢座、重试是怎么串起来的源码拿到手第一步不是直接跑是先搞懂各文件的职责。我看了一下这个项目结构非常清晰大致分成了配置加载、登录模块、座位查询模块、预约提交模块、定时调度模块、日志模块这几个部分。这种分层写法值得学习哪怕代码量不大拆分开以后维护和排错都会轻松很多。2.1 登录模块session会话保持是整套自动化的地基所有自动请求的前提是“平台认为你是你”所以登录模块是整个系统里最基础也最重要的一环。源码里用的是requests.Session()这个模块非常巧妙它会自动保存服务端返回的Cookie后续的所有请求都自动带上不需要手动维护Cookie头。我看很多初学者会犯一个经典错误每次请求都new一个requests.get()这等于每次都是个新身份平台自然识别为异常。Session对象的意义就好比你进了图书馆后脖子上挂了个通行证之后每一层楼、每一个门禁都只认这张证不需要重新登记。代码里登录逻辑大致长这样import requests import json session requests.Session() # 部分平台要求先GET登录页拿到隐藏的csrf_token login_page session.get(LOGIN_URL, headersHEADERS) csrf_token extract_csrf_token(login_page.text) # 构造登录参数发起登录请求 login_data { username: USERNAME, password: PASSWORD, csrf_token: csrf_token } resp session.post(LOGIN_API_URL, datalogin_data) result resp.json() if result.get(code) 200: print([] 登录成功用户, USERNAME) else: print([-] 登录失败, result.get(message))这里有个细节headers里必须带Referer和Origin因为很多图书馆预约平台会做来源校验如果这两个值和登录页域名不匹配请求会被直接拒绝。源码里把这些参数固定成了一个全局字典建议你自己调试的时候也这么做不要图省事省掉这两行。另外提醒一句如果目标平台有图形验证码或滑块验证这个项目的原始版本是搞不定的你想二次开发就得接入打码平台或者自己训练个简单识别模型但这就超出“学习借鉴”的范畴了普通场景基本用不到。2.2 座位查询模块解析JSON数据时别忽略座位状态字段登录成功后系统需要拿到当前的座位列表。大部分平台的接口会返回一个JSON数组每个元素包含座位ID、自习室编号、座位号、状态0空闲/1占用/2暂离、是否靠窗、是否带电源等字段。源码里这个模块做得比较讲究的地方在于它没有一次性把所有座位全查回来而是按自习室ID循环拉取再在本地做过滤拼接。这样做的好处是请求参数简单、响应体积小、不容易触发接口超时。我摘了一段核心逻辑def fetch_available_seats(session, room_id, target_date, start_time, end_time): 拉取指定自习室、指定时间段的可用座位列表 params { roomId: room_id, date: target_date, startTime: start_time, endTime: end_time } resp session.get(SEAT_QUERY_URL, paramsparams, headersHEADERS) data resp.json() if data.get(code) ! 200: return [] available [] for seat in data[data][seats]: # 状态为0表示当前可预约 if seat.get(status) 0: available.append(seat) return available很多人在这个模块上踩过坑字段名不是固定的。有的平台叫status有的叫state有的还分seatStatus和bookStatus两个字段含义完全不同。这个只能靠抓包自己看真实返回结构不要拿网上别人的代码硬套。2.3 抢座提交模块请求参数的顺序都可能影响成败选好座位后就要对目标座位发起预约请求。这个请求通常是POSTbody里要带座位ID、日期、开始时间、结束时间等。源码里的提交模块做了一件很多新手意识不到的事情它把请求参数按平台要求的固定顺序排列并用datajson.dumps(payload)而非datapayload的方式发送因为有些后端框架对Content-Type和参数顺序极其敏感。def reserve_seat(session, seat_id, date, start_time, end_time): payload { seatId: seat_id, date: date, startTime: start_time, endTime: end_time } # 部分平台要求JSON格式提交 resp session.post( RESERVE_URL, datajson.dumps(payload), headers{ **HEADERS, Content-Type: application/json;charsetUTF-8 } ) result resp.json() if result.get(code) 200: return True, result.get(message) else: return False, result.get(message)这里要特别提醒抢座成功后不要立刻关闭程序源码里做了一个“二次确认”动作——间隔2秒后再查一次该座位状态确认status从0变成了1。这个步骤非常实用因为有的平台预约接口响应成功其实只是“受理成功”异步流程里还有可能失败。确认动作能提前发现问题也方便接通知推送。2.4 重试与调度为什么不能只写一个while True加sleep这是新手最容易写崩的地方。很多初版脚本就是while True: reserve_seat(); time.sleep(5)这种写法在抢座场景里就是灾难一旦预约成功它还在不停地抢把已经到手的座位又释放了一旦平台接口报错它就陷入死循环疯狂请求直到IP被拉黑。源码里的调度模块做得就聪明很多它的执行流程是一棵状态树状态一等待放座时间低频轮询时间校准每30秒对一次服务器时间状态二放座前5秒进入高频轮询持续查询可用座位状态三命中座位并成功提交切换为“已完成”状态停止所有请求状态四提交失败且仍有重试次数按指数退避策略1秒、2秒、4秒、8秒重新进入状态二状态五重试耗尽发送失败告警并退出这个状态机加上时间校准的设计很关键。你要知道你的电脑时间和平台服务器时间通常有偏差如果差了3秒那你提前5秒开始抢等于提前2秒就开始发请求结果平台上座位还没放出来你会一直抢不到如果服务器比你快3秒那你觉得时间刚好其实已经晚了3秒。源码里用了一个时间接口或者从登录响应头里的Date字段来校准本地时间把偏差控制在了毫秒级。def sync_server_time(session): resp session.get(TIME_URL) server_time_str resp.headers.get(Date) server_time datetime.strptime(server_time_str, %a, %d %b %Y %H:%M:%S GMT) local_time datetime.utcnow() offset (server_time - local_time).total_seconds() return offset3. 从环境准备到跑通这套源码在你电脑上的完整启动流程源码归源码跑不起来等于零。这个项目用的是Python 3依赖库主要是requests、schedule或APScheduler、pyyaml也可能是json配置我会把从零到跑通的完整流程拆开讲包含venv隔离、依赖安装、配置文件改动、首次运行验证这几个阶段。3.1 Python环境和依赖安装别图省事用系统全局Python如果你是Windows环境我强烈建议你别直接拿系统自带的Python跑项目。多个项目混在一起时依赖版本冲突能让你想砸电脑。用虚拟环境是最省心的做法。# 进入项目目录 cd library-seat-reservation # 创建虚拟环境 python -m venv venv # Windows激活虚拟环境 venv\Scripts\activate # Linux/macOS激活虚拟环境 source venv/bin/activate激活后命令行前面会出现(venv)前缀这时候再装依赖pip install -r requirements.txt如果requirements.txt缺失或者没写好你也可以直接手动装三件套这个项目核心就这几个pip install requests schedule pyyaml这里特别提一下国内网络环境如果你用默认的PyPI源安装速度很慢或者直接超时可以临时换成清华镜像源pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple3.2 配置文件改动账号密码和预约参数的真实填写示范依赖装完以后下一步就是改配置文件。这个项目用的是config.yaml有的版本是config.json打开以后会看到类似这样的结构account: username: your_student_id password: your_password target: room_id: A201 date: 2025-06-20 # 留空则默认预约明天 start_time: 08:00 end_time: 12:00 preference: window_seat: true # 是否只选靠窗 power_available: false # 是否必须带插座 schedule: reserve_time: 07:58 # 放座时间建议提前2分钟开始预热 max_retries: 5 retry_base_interval: 2 # 基础重试间隔秒 notify: enable: false server_chan_key: # Server酱的SendKey可选我第一次跑的时候就只改了账号密码和自习室号结果运行直接报错说找不到roomId。这个room_id不是你在界面上看到的那串文字而是接口返回数据里的数字ID你要么自己去抓包看要么调一下源码里的“列出所有房间”函数生成一张映射表。调试阶段的建议是先把schedule.reserve_time改成未来10分钟之后的时间手动触发一次完整过程确认整个链路走得通再改回真实放座时间。3.3 首次运行验证如何确认登录和查询都正常改好配置后先在命令行跑一次python main.py --dry-run很多版本支持dry-run模式不真实提交预约只执行登录和查询流程。如果登录失败优先检查账号密码是否被平台加密过有的平台前端用RSA加密密码字段这种就必须用对应的公钥加密后再提交或者直接用Cookie登录模式如果登录成功但查不到座位优先检查room_id和date参数。我看到源码的日志模块在这里起了大作用它把流程分成了[INFO]、[WARNING]、[ERROR]三个级别每步做了什么一目了然。比如[INFO] 2025-06-19 17:00:01 - 登录成功用户名: 2024010101 [INFO] 2025-06-19 17:00:01 - 服务器时间校准完成本地偏差: 2.3秒 [INFO] 2025-06-19 17:00:02 - 开始查询自习室 A201 的可预约座位... [INFO] 2025-06-19 17:00:03 - 查询到24个空闲座位正在按照偏好过滤... [INFO] 2025-06-19 17:00:03 - 偏好过滤后剩3个候选座位 [INFO] 2025-06-19 17:00:03 - 已安排定时任务2025-06-20 07:58:00 开始抢座走到这一步基本上整个系统的地基就算打好了。4. 配置文件里的门道日期、时段、座位偏好和重试策略的最优设置很多人拿这个项目直接跑总是抢不到座又不明白为什么。我拆了一圈源码后发现八成原因不在代码而在配置参数设计得不合理。这章专门讲配置文件每个字段背后的逻辑和优化策略。4.1 日期和时段设置为什么预留“预约明天”的默认值看源码默认配置里date留空时会自动预约明天这个设计深得我心。因为绝大多数图书馆的预约规则是系统只允许预约今天或者明天的座位而且放座时间是固定的比如每天晚上20:00放出第二天的座位。如果你要预约今天的座位直接把date写成当天的日期。但注意很多平台超过某个时间点就锁定了当日预约这时就必须预约明天。时段设置有一点容易忽略有的图书馆预约平台按时间段收费或者有最小预约时长限制。比如有的馆要求最少预约2小时有的馆晚上闭馆前1小时不可预约。你需要提前在自己学校的预约平台上手动操作一遍搞清楚你所在馆的具体规则再填到配置里。源码不会替你判断这些业务规则它只是忠实地提交数据。4.2 座位偏好过滤窗口位置和电源的取舍逻辑偏好过滤的逻辑在源码里写得比较直白遍历可用座位逐条对比是否满足偏好如果候选为空则自动放宽條件比如不要求电源了如果仍为空则继续放宽到只要该自习室有空座就抢。这个“自动降级”策略非常重要。我见过很多人设了“必须靠窗且必须带插座”结果一整个时段一个符合条件的座位都没有脚本就空转了一上午。而源码里的处理方式更接近真实需求首选条件满足就抢满足不了就退而求其次先保证有座再谈体验。# 源码中的偏好过滤逻辑示意 candidates available_seats if preference.window_seat: preferred [s for s in candidates if s.get(window) 1] if preferred: candidates preferred else: print([WARN] 无靠窗座位放宽条件) if preference.power_available: preferred [s for s in candidates if s.get(power) 1] if preferred: candidates preferred else: print([WARN] 无带电源座位放宽条件)这种降级逻辑放到真实场景其实就是活生生的产品思维你要解决的核心问题是“今天有位子坐”而不是“今天必须坐在那个最完美的位子上”。自动化脚本的目的也一样优先保障成功率再考虑体验。4.3 放座时间和重试间隔别把反爬策略当摆设核心的调优参数就是reserve_time、max_retries、retry_base_interval这三个。reserve_time建议设为平台实际放座时间前1到2分钟。这个预热时间是给程序做时间校准、建立连接、拉取一次座位列表用的不是为了提前抢——你提前抢也抢不到因为座位还没放出来但提前把链路打通到了抢座那一瞬间会快很多。重试策略方面源码用指数退避第一次失败等2秒、第二次等4秒、第三次等8秒以此类推。这个策略的核心思路是如果第一次预约失败是因为接口繁忙大概率后面几秒依然繁忙如果无脑以0.1秒间隔高频重试很容易被平台限流甚至封号。但重试次数也别设得太大5次左右就够了——如果连续5次失败还不成功说明你不是被平台限流就是座位池真的空了再重试只能是徒劳。我实测下来把retry_base_interval设为2max_retries设为5在100次模拟里成功率大约可以稳定在85%左右而如果用固定1秒重试10次反而会触发平台的“操作过于频繁”拦截成功率掉到40%以下。5. 踩坑实录我在调试过程中遇到的四个典型问题这部分是源码之外的真正干货。我拿到这个项目后不是一次跑通的前前后后踩了不少坑有些坑相信你们也会遇到。5.1 登录密码被前端加密直接提交明文永远返回密码错误这是最让人崩溃的错误之一。我一开始用自己的账号测试一直提示密码错误但网页端明明能登录。后来我用浏览器的开发者工具抓包发现网页登录请求里的password字段是一长串乱码根本不是我输入的明文密码。这说明平台前端用了RSA或者AES之类的算法对密码做了加密。解决办法有两种一种是找到前端加密的JS代码用Python重写一遍加密逻辑另一种更省事先在浏览器里登录拿到Cookie后直接填到配置文件里绕过密码登录。源码里还提供了一个login_by_cookie的函数就是为这种情况准备的。5.2 Windows下time.sleep计时不准导致抢座延迟在Windows上跑定时任务时Python的time.sleep()存在调度精度问题你让程序睡到7:59:55它可能实际7:59:56.8才醒这接近2秒的误差在抢座场景里足够致命。解决办法有两个要么改用time.sleep配合时间校准循环每次醒来后重新计算偏差剩余时间大于0.5秒就继续睡要么直接换用schedule库或者APScheduler虽然它们底层也依赖操作系统的定时器但在长时间任务中的稳定性比裸写sleep要好很多。源码在这个问题上用了一个非常切实的补偿策略高频轮询阶段每隔0.3秒请求一次而不是先睡到某个绝对时间点再一次性请求。因为0.3秒的间隔已经远小于系统调度误差这样就算每次都偏差一点点也不会错过放座瞬间。5.3 接口限流导致IP被短暂封禁有段时间我把脚本改成每1秒查一次座位持续跑了半个多小时突然所有请求都开始返回403连浏览器登录都提示异常操作。这就是典型的IP被平台限流。解决方法是把平时的轮询间隔调大到8到10秒只有在临近抢座时才提高频率同时给requests加一个适配器设置连接池大小和重试策略from requests.adapters import HTTPAdapter adapter HTTPAdapter(pool_connections10, pool_maxsize10, max_retries3) session.mount(https://, adapter) session.mount(http://, adapter)5.4 日志文件编码问题在Windows上直接运行源码里的日志模块控制台或者日志文件里的中文可能乱码。打开项目的log目录下的日志文件如果发现中文变成了乱码检查一下logging.FileHandler是否指定了encodingutf-8。老版本Python在Windows上默认编码是GBK这会导致写入日志时中文报错或乱码。在源码的日志初始化里显式加上这一个参数就能解决logging.basicConfig( filenamerun.log, levellogging.INFO, encodingutf-8, format%(asctime)s - %(levelname)s - %(message)s )如果你手里的版本没加这个参数直接手动补上。这个坑不是这个项目独有的几乎所有Python脚本在Windows上写日志都会遇到。6. 源码读完能学到什么从requests到项目结构的一次完整实战最后聊聊这个项目的学习价值。说实话这个项目的代码量不大比起那些动辄几千星的开源项目来说它甚至显得稚嫩但作为学习素材它的覆盖面非常精准。6.1 把“会用requests”变成“会做请求”很多人学requests停留在requests.get(url)然后打印response.text的阶段。看完这个项目你会发现真实的HTTP请求要考虑的事情远比教程多会话保持、请求头伪装、参数序列化、响应状态判断、异常重试、超时控制。比如源码里对每个请求都设置了timeout(3.05, 10)连连接超时和读取超时区分得清清楚楚。这些细节在官方文档里都写了但是没有真实项目驱动你根本不会主动去用。6.2 学会拆解一个完整功能为多个模块这个项目的目录结构可以作为一个小型项目的范本config.yaml # 配置文件 main.py # 入口负责初始化和启动调度 modules/ login.py # 登录模块 query.py # 座位查询模块 reserve.py # 预约提交模块 scheduler.py # 定时调度与状态机 notify.py # 通知推送模块 logger.py # 日志封装 utils/ time_helper.py # 服务器时间校准 http_client.py # requests会话封装这种“入口-模块-工具”的分层方式在任何一个正规项目里都很常见。你照着这个结构去写自己的爬虫脚本、自动化工具会有一种思路瞬间清晰的感觉。6.3 我建议的后续扩展方向如果看完源码想动手改进我建议从这几个方向入手多账号支持把单账号配置改成“账号列表”用线程池并发抢座但前提是本宿舍一起用别拿去给陌生人代抢容易给学校平台造成压力结果推送配置里预留了Server酱的key你还可以扩展成邮箱推送或者微信推送通过企业微信机器人抢到座位后第一时间告诉手机Web控制界面用Flask写一个简单的管理后台展示当日预约记录、修改配置、查看日志这样就不用每次改参数都改yaml文件了新平台适配如果你发现源码里的接口结构和你们学校的平台不一样把登录、查询、预约三个函数重写一遍就行其他部分完全复用不过最后还是要说一句老实话这个项目最终的归宿不应该是当作一个彻底的黑盒子抢占公共资源。把它当成一个练手项目理解里面的请求思路、状态机设计、异常处理逻辑然后在晚上十点没人抢的时候悠闲地预约一个第二天的座位安安静静去上自习这才是这个项目最正确的打开方式。本文还有配套的精品资源点击获取