简介:这份资源是一套基于Python的医院挂号自动抢号脚本代码包,面向想要学习浏览器自动化与验证码识别的Python开发者,也适合有实际挂号需求的人参考。脚本以华西医院网页为例,使用Selenium模拟浏览器打开登录页,自动发送并处理手机验证码,完成登录后设置倒计时,在指定时间准时进入医生主页,随后选择电子健康卡并借助ddddocr识别图形验证码自动填入,最后通过循环检测确认按钮完成预约。资源共6个文件,压缩包仅8KB,包括py主程序、config.ini配置、requirements.txt依赖清单、demo.html示例页面、inscode配置文件和md/txt说明文档,结构简洁,便于快速阅读。代码提供了完整思路和关键片段,涵盖元素定位、显式等待、验证码识别、异常重试等自动化难点;同时针对页面加载延迟和按钮未出现等情况加入了轮询与重试机制,帮助提升挂号成功率。目前已有343人学习下载,读者可根据自身需求修改参数,或将该思路迁移到其他定时预约、抢购等场景。 很多朋友私信我,上来就是一句:能不能给我写一个Python医院抢号脚本,带代码的那种。搜一下“Python医院抢号脚本[代码]”这个标题,各种文章、帖子确实不少,但真正把原理讲清楚、能让人举一反三的,几乎没有。这篇文章我打算换个思路:不直接给你一份“复制就能用”的抢号代码,那种代码要么几天就失效,要么本身有合规风险,而是把这类型脚本背后的核心模块、关键技术点、工程化设计拆开来讲,最后给出一套不依赖任何真实医院接口、可本地跑通的调度框架Demo。适合对Python网络请求、自动化调度有一定基础,想理解这类工具本质的开发者。
1. 先说清楚:这类脚本到底在忙什么
1.1 核心模块拆解:从身份凭证到预约提交的完整链路
一段看似不起眼的“抢号脚本”,落到工程层面其实是四个独立模块的协作产物。
第一个是身份模块。医院预约系统几乎都是登录后操作,脚本必须持有你的登录凭证。常见做法是用requests.Session()维持Cookie,或者在登录后保存Token,后续每个请求都带上。别小看这一步,很多人脚本跑不通,不是请求逻辑写错了,而是Session没有正确保持,服务端根本不认这个“人”。
第二个是查询模块。脚本要反复访问号源查询接口,拿到某个科室、某个医生的剩余号量。这里涉及接口地址、请求参数(日期、科室ID、医生ID)、响应解析。医院系统的前端页面每一次改版,接口返回的JSON结构都可能变,所以这个模块是维护成本最高的地方。
第三个是动作模块。查到位之后,真正提交预约的动作通常是一个POST请求,参数里包含号源ID、就诊人ID、时间段等。动作模块要处理的是一致性问题:你查到的号源可能在提交前的一瞬间被别人抢走了,这时服务端会返回“号源已满”或“冲突”,脚本需要捕获并处理这种竞态。
第四个是通知模块。抢到没抢到,得让用户知道。简单的是写日志、发邮件,常见的是接入企业微信机器人、钉钉机器人或Server酱,把结果推到手机上。
把这四个模块串起来,才是一个完整的“抢号脚本”该有的骨架。很多人上网求代码,拿到的往往只有查询和提交两段,身份和通知模块都是残缺的,自然跑不起来。
1.2 为什么它不是“写个循环”那么简单
有一种常见的误解:抢号不就是while True: requests.get(查号接口),查到就提交嘛。真这么简单,就不需要专门写工程脚本了。
真实场景里至少有三个问题。第一,查询频率不能无限高。你每秒钟打一次接口,服务器会压力很大,风控系统也会盯上你,轻则当前IP被限流,重则账号被标记。第二,不是查到了就一定能提交成功。从查询到提交是一个“检查再更新”的竞态窗口,别人可能正好在你查询之后、提交之前抢走了号源。这也是为什么很多脚本设计成查询到号源后立刻用同一个请求上下文去提交,而不是等用户确认。第三,参数是动态的。你以为接口地址不变就万事大吉,实际上一段时间后服务端可能给请求参数加了签名、时间戳、加密字段,原有脚本全部失效。
所以,“抢号脚本”本质上是一个带状态、带频率控制、带容错重试的Web自动化程序。理解到这一层,你才能看懂后面所有的技术选型。以下所有内容都默认你已经理解了这一层,我直接进入关键技术点。
2. 关键技术点逐个拆:身份保持、任务调度与请求容错
2.1 Session与Cookie:怎么让服务器认出“你是你”
HTTP协议本身是无状态的,服务器不记得你上一个请求干了什么。Session机制的出现就是为了解决这个问题:你登录成功后,服务器返回一个Cookie,后续请求只要带上这个Cookie,服务器就知道“你是同一个已登录用户”。
在Python里,requests.Session()是处理这件事最顺手的工具。它做了两件好事:自动保存和附带Cookie,以及复用底层TCP连接,减少握手开销。很多新手栽的坑是每次请求都用requests.get()裸调,登录接口返回的Cookie没有保存,下一次请求服务端直接返回401或登录页。
一个稳妥的做法是:
import requests session = requests.Session() login_url = "https://your-api.example.com/api/login" login_payload = {"account": "your_account", "password": "your_password"} resp = session.post(login_url, json=login_payload, timeout=10) # 登录成功后,session里自动存了Cookie和必要的身份信息 # 后续所有请求都用这个session对象如果服务端返回的是Token而不是Cookie,那就在每次请求头里带上Authorization: Bearer <token>。这种设计在移动端App接口里更常见,本质上一样,你只需要维护一个全局的请求头工厂就行。
有人会问:JWT Token不是无状态的吗,还需要Session吗?在实际工程里,医院系统往往是“Token + 服务端会话”混合使用,你没必要纠结理论模型,实践原则只有一个:把身份信息和请求上下文封装成一个可复用的对象,别在每个函数里重复登录。
2.2 时间基准与定时调度:抢的其实是“时间窗口”
这类脚本的核心竞争点就是时间窗口。医院放号时间往往是固定时间点,脚本需要在放号瞬间发出请求。那你首先得保证脚本所在机器的时钟和服务端时钟一致。本地电脑时间慢了两秒,放号瞬间你还在等本地时钟走到点,实际上服务端已经放号两秒了,热门号源早就没了。
解决思路很简单:在放号前做一次时间同步。可以用requests去请求一个标准时间接口,解析返回的时间戳来校正本地偏移量,调度器按校正后的时间触发任务。在代码层面,写一个轻量的时间校正函数:
import time import requests def get_server_offset(server_time_url: str) -> float: """返回服务端时间与本地时间的差值(秒),本地慢则为正""" resp = requests.get(server_time_url, timeout=5) server_ts = resp.json()["timestamp"] # 假设接口返回标准时间戳 return server_ts - time.time()这个offset可以在放号前几秒获取一次,然后在调度等待逻辑里用上。注意别频繁请求时间接口,那会给自己带来不必要的限流风险。
定时调度方面,简单场景用time.sleep()就够了,但如果你有多个监控任务、需要按cron表达式跑,还是老老实实用APScheduler。它支持interval和cron两种触发方式,配合BlockingScheduler或BackgroundScheduler可以灵活嵌入不同项目。后面第3章我会给一个可跑的调度框架。
2.3 并发与重试:快不一定赢,稳才不封
很多初学者拿到需求后的第一反应是开多线程,50个线程同时查接口,似乎“抢”到的概率更高。实际效果往往相反:所有线程都卡在等待响应上,服务器一看同一个IP瞬间涌进来几十个请求,直接触发限流,连正常请求都被拒掉。
正确的做法是用有限并发 + 超时控制 + 指数退避重试,把请求频率控制在安全线以内。这里我给一个很实用的Session封装,使用urllib3的Retry机制,对超时和5xx错误做自动重试:
import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def build_session_with_retry(retries: int = 3, backoff_factor: float = 1.0): session = requests.Session() retry = Retry( total=retries, connect=retries, read=retries, backoff_factor=backoff_factor, # 每次重试等待:backoff_factor * (2 ** retry_count) status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET", "POST"], ) adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter) return session注意backoff_factor的语义:假设重试等待时间为backoff_factor * (2 ** 当前重试次数),所以设成1.0,第一次重试等2秒,第二次4秒,第三次8秒。指数退避的核心思想是:一旦服务端不稳定,立刻把请求频率降下来,让服务端喘口气,而不是疯狂重试加剧问题。
如果你是做查询监控,用单线程或双线程就够了,重点在“稳定地间歇请求”,而不是“瞬时高并发”。
2.4 验证码这道坎:自动化与公平的边界
所有抢号类脚本最绕不开的就是验证码。滑块验证码、点选验证码、短信验证码,层层加码之后,纯HTTP请求已经很难独立完成全流程。这里我说句透底的话:任何针对真实业务系统的验证码自动识别方案,我都不建议写,也不建议用。
验证码存在的意义就是区分人和自动化工具。你写代码绕过验证码,本质上就是在和服务端的安全策略对抗。工程上碰到验证码,正确的处理姿势是:代码里识别到“需要验证”的响应特征后,立即停止后续自动操作,通过通知模块把“需要人工过验证码”这个信号推给用户。比如:
if "captcha" in resp.text or resp.status_code == 460: # 示例状态码 notify("检测到验证码,需要人工介入") return这套逻辑放在正规自动化测试里也成立:测试环境一般通过白名单、测试后门或人工介入来处理验证码,不会在线上环境硬破解。理解了这条边界,你在设计调度框架时就不会把“打码平台”这类灰色方案当核心模块来耦合。
3. 一套可复现的调度框架示例:号源余量监控提醒器
3.1 框架定位:只监控、不抢,先跑通调度逻辑
为了让你能安全地跑通整套思路,我设计了一个“号源余量监控提醒器”。它不绑定任何真实医院系统,只演示三件事:定时轮询一个接口、解析返回状态、状态变化时触发通知。这套逻辑就是所有抢号脚本最核心的骨架,你完全可以改成查余票、查库存、查设备状态。
演示环境用的是https://httpbin.org/delay/1,它会延迟1秒返回JSON,用来模拟一个真实的慢接口。先装依赖:
pip install requests apscheduler3.2 请求执行器:Session、超时与重试的封装
请求执行器是整个框架的底座,负责所有HTTP请求的发送、超时控制和重试。直接上一段完整代码:
import logging import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") def build_session_with_retry(retries: int = 3, backoff_factor: float = 1.0) -> requests.Session: session = requests.Session() retry = Retry( total=retries, connect=retries, read=retries, backoff_factor=backoff_factor, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET", "POST"], ) adapter = HTTPAdapter(max_retries=retry) session.mount("https://", adapter) session.mount("http://", adapter) return session def fetch_data(session: requests.Session, url: str) -> dict: resp = session.get(url, timeout=5) resp.raise_for_status() # 4xx/5xx会抛出异常,由重试机制接手 return resp.json()这里有一个容易忽略的点:raise_for_status()会主动抛出HTTP错误,配合Retry才能触发重试。如果你自己写了try/except把异常吞掉,重试机制就不会生效。
3.3 轮询任务与通知钩子
接下来是核心的检查逻辑和通知逻辑。我用一个假设的接口响应结构来演示,实际使用时把CHECK_URL换成你自己合法有权访问的接口即可:
CHECK_URL = "https://httpbin.org/delay/1" def check_and_notify(): session = build_session_with_retry() try: data = fetch_data(session, CHECK_URL) # 假设接口返回里有个字段标识“当前可预约数量” available_count = data.get("available", 0) if available_count > 0: notify(f"检测到可预约号源,剩余 {available_count} 个") else: logging.info("当前无余量,继续监控") except requests.RequestException as e: logging.warning(f"请求失败:{e}") def notify(message: str): # 这里先打日志,实际项目可以替换成企业微信机器人、邮件或短信 logging.info(f"[NOTIFY] {message}")主调度用APScheduler实现,每30秒检查一次:
from apscheduler.schedulers.blocking import BlockingScheduler def main(): scheduler = BlockingScheduler(timezone="Asia/Shanghai") scheduler.add_job(check_and_notify, "interval", seconds=30) scheduler.start() if __name__ == "__main__": main()BlockingScheduler会阻塞主线程,适合独立脚本。如果你想把监控任务嵌进已有的Web服务,改用BackgroundScheduler即可。
3.4 本地跑通的验证方式
直接运行上面这段代码,你会看到日志里每30秒输出一次“当前无余量”,这是因为httpbin.org的响应里根本没有available字段,data.get("available", 0)取到的默认值是0。这正说明了一个重要机制:框架是通的,只是你的“业务字段解析”和真实接口不匹配。
想真正验证通知逻辑,可以换一个返回自定义JSON的本地Mock服务。用Flask起一个几行的接口:
from flask import Flask, jsonify import random app = Flask(__name__) @app.route("/api/quota") def quota(): return jsonify({"available": random.randint(0, 3)}) app.run(port=5000)然后把CHECK_URL改成http://127.0.0.1:5000/api/quota,再把轮询间隔改成10秒。这样你能很快观察到[NOTIFY]日志出现,完整走通“轮询—解析—通知”的链路。先跑通这套,后面的问题排查才有基础。
4. 实测中容易翻车的细节与应对思路
4.1 接口返回结构变化:解析层必须做容错
做这类脚本,最崩溃的事不是请求失败,而是请求突然“成功”了,但解析不到你要的数据。医院系统或者任何业务系统,前端一改版,接口返回的字段名就可能从number变成remain_count,旧脚本直接KeyError或者解析出None。
我的习惯是解析层全部用get()加默认值,不加裸下标访问,并且对关键字段缺失记WARN日志。上面的示例里data.get("available", 0)就是这么做的。你会发现,这样写出来的脚本在字段变化时不会崩溃,只是拿不到值,日志会提示你“字段可能变了”,方便你更新解析逻辑。
4.2 限流与封禁:不是请求越多越好
请求频率控制是另一道生死线。服务端常见的限流表现是:某个IP在短时间内请求次数超过阈值,后续请求直接返回429 Too Many Requests,或者干脆返回空数据。这属于服务端的自我保护,脚本如果不认识这个信号,会一直重试,最后被临时封禁IP。
下表是我在实际项目中总结的常见表现和应对思路:
| 现象 | 可能原因 | 应对思路 |
|---|---|---|
| 返回429 | 请求频率过高 | 降低轮询间隔,加指数退避重试 |
| 偶尔超时 | 接口性能波动 | 增加超时时间,重试放宽 |
| 返回数据为空 | 参数错误或接口升级 | 重新核对请求参数和接口文档 |
| 需要验证码 | 触发风控 | 停止自动操作,转人工处理 |
也就是说,脚本设计阶段就应该假设请求大概率失败,而不是假设每次都能成功。我见过的很多翻车事故,本质都是把“监控”做成了“攻击”,频率一高,账号和IP一起凉。
4.3 验证码与风控升级:自动化遇到“人工校验”
在真实系统里,验证码不是静态的,它会升级。之前可能只是图形验证码,后来变成滑块,再后来变成需要手机短信二次确认。这种升级在响应里的表现通常是:接口返回一个特殊状态码,或者响应体里出现captcha、verify等关键字。
我在框架里专门预留了“检测到验证码就停止并通知”的钩子,就是因为它太容易翻车了。很多脚本不是死在请求上,而是死在验证码出现后仍然无脑重试,最后被系统判定为恶意请求。记住:一旦检测到验证码,立刻降级为人工处理,这是最稳妥的兜底策略。
4.4 环境与依赖问题:命令找不到、依赖装不上
最后说一个特别新手向但出现率极高的问题。很多人在Windows上下载了Python、写好了脚本,一运行却报“无法将‘python’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”,或者运行claude、pip之类的命令时提示找不到。这几乎都是环境变量没配对,系统找不到Python的可执行文件路径。
解决办法不复杂:安装Python时勾选“Add Python to PATH”,装完后重开终端;如果之前没勾选,去“系统属性—环境变量—Path”里手动把Python安装目录和Python安装目录\Scripts加进去。依赖装不上的时候,优先检查是不是pip源网络问题,可以用国内镜像源装:pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple。这类问题属于基础设施问题,配置好了能省掉后面一大半的折腾。
5. 合规边界与技术用途延伸
5.1 为什么不直接贴一份“能用的抢号代码”
到这一步,你大概能理解我为什么坚持不给一份“复制就能用”的真实抢号代码了。这类代码有三个绕不开的硬伤。第一,有效期极短。业务系统接口一改,脚本立刻报废,这份代码对你没有任何长期价值。第二,维护成本高。你要持续跟进接口变化、风控升级,等于每天都在和平台安全策略赛跑。第三,公平性问题。预约号源是稀缺资源,自动化脚本本质上是在挤占其他普通用户的候诊机会。技术可以用来提升效率,但用在零和博弈的场景里,副作用会被放大。
所以我前面所有内容都在讲原理、讲工程框架,而不是给一套针对某个真实系统的“抢”代码。你真正该带走的是监控轮询、容错重试、身份保持这些通用能力,这些能力在任何一个合法场景都能复用。
5.2 同一套技术栈的正确打开方式
把上面这套调度框架改一改,就能落到很多合规场景里。举三个我自己做过的方向:
第一个是号源余量提醒。只查询、不提交,发现有余量就第一时间推送通知,由用户自己打开App或小程序完成预约。这个方案完全站在辅助用户的一侧,不破坏公平性,而且技术难度低很多。第二个是接口健康巡检。定时探测公司内部服务的健康检查接口,响应变慢或返回错误时自动告警,这就是一个非常标准的可观测性小工具。第三个是自动化冒烟测试。把核心业务链路用脚本串起来,定时跑一遍,失败就发通知,这也是自动化测试落地的最低成本形态。
我自己做自动化多年,最大的体会是:自动化是用来解决重复劳动的,不是用来和规则对抗的。把请求调度、容错重试、状态机设计这些基本功练好,你会发现需要写脚本的场景永远不缺。那些靠绕过风控、抢占资源做的事,既走不远,也带不来真正的技术积累。
本文还有配套的精品资源,点击获取