简介:这是一份面向Python初学者与自动化实践者的医院挂号抢号实战脚本资源,聚焦解决华西医院等网页挂号场景中人工守候难、操作繁琐、验证码识别卡顿等痛点。资源以轻量级Python工程形式交付,含6个核心文件:主程序hospital_auto_register.py实现Selenium模拟登录、倒计时预约、电子健康卡选择与确认提交;config.ini管理账号与时间参数;requirements.txt明确依赖(含ddddocr验证码识别库);README.md提供部署说明;demo.html为本地测试页;.inscode为开发环境配置参考。压缩包仅8KB,结构精简、开箱即用。已有389人学习下载,读者可直接复用完整可运行脚本,掌握网页自动化登录、动态验证码识别、定时触发与异常重试等关键能力,并基于源码快速适配其他挂号平台。
1. Python医院抢号脚本:不是玄学,是HTTP状态码、会话维持与时间窗口的硬核协同
你有没有试过凌晨四点蹲在手机前,手指悬在挂号按钮上,倒计时归零瞬间——页面卡死、验证码失效、提示“号源已抢完”?这不是网络问题,是绝大多数挂号系统背后那套「防刷机制」在起作用:它不拦人,但用极短的会话有效期、动态Token、前端埋点和后端限流,把无序并发变成一场高损耗的撞墙实验。而这份「Python医院抢号脚本」,不是教你怎么绕过安全策略,而是把挂号流程拆解成可观察、可调试、可重放的HTTP事务链:从登录态维持、号源轮询、预约参数构造,到提交前的签名验证与幂等控制。它面向的是有基础Web开发经验、能看懂Fiddler抓包、愿意花20分钟配好环境的真实需求者——比如某高校实验室的A同学,为家人高频复诊设计的自动化提醒+半自动提交辅助工具;也适用于医疗信息化课程中「真实业务系统接口交互」的教学案例。它不承诺100%抢到,但能把成功率从“听天由命”拉回“参数可控”,前提是:你理解它每一步在做什么,而不是直接双击运行。
2. 抢号逻辑的本质:从浏览器操作到Python请求链的完整映射
挂号系统从来不是单次POST就能解决的事。真实场景里,一次成功预约至少涉及5个强依赖环节:登录态获取(含短信/图形验证码)、科室医生列表拉取、可约时段查询、号源库存校验、最终预约提交。这五个环节环环相扣,任意一环的响应异常或状态不一致,都会导致后续步骤失败。而这份脚本的价值,正在于它没有跳过任何一环,而是用requests.Session()构建了完整的会话上下文,并对每个关键响应做了显式状态断言。下面我们就逐层还原这个链条。
2.1 登录态初始化:为什么不能只靠Cookie?
很多初学者会尝试“手动登录后导出Cookie,再用requests带上”,这在挂号系统里几乎必然失败。原因很简单:现代挂号平台普遍采用「短期有效Token + 前端时间戳签名」双校验机制。单纯复制Cookie,缺失了Header里的X-Request-Time、X-Signature,且Session过期时间往往只有3~5分钟。脚本采用的是标准登录流程模拟:
import requests from urllib.parse import urlencode session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" }) # 1. 获取登录页,提取隐藏字段及初始CSRF Token login_page = session.get("https://xxx-hospital.gov.cn/login") # 此处解析HTML,提取 <input name="csrf_token" value="abc123"> 等字段 # 实际代码中使用BeautifulSoup或正则提取,非硬编码 # 2. 提交登录表单(含图形验证码识别结果) login_data = { "username": "138****1234", "password": "encrypted_pwd", # 注意:此处应为前端JS加密后的密文,非明文 "captcha": "ABCD", # 需接入OCR服务或人工输入 "csrf_token": "abc123" } resp = session.post("https://xxx-hospital.gov.cn/api/v1/auth/login", data=login_data) # 关键断言:必须检查HTTP状态码 + 响应体中的success字段 if resp.status_code != 200: raise RuntimeError(f"登录请求失败,HTTP {resp.status_code}") data = resp.json() if not data.get("success"): raise RuntimeError(f"登录业务失败:{data.get('message', '未知错误')}")提示:
password字段绝不能是明文。真实挂号系统前端必做RSA/AES加密,脚本中需嵌入对应JS加密逻辑(如用PyExecJS调用原站login.js),或提前用浏览器F12 Network面板捕获加密后密文,再反向推导加密方式。这是第一个必须跨过的门槛——跳过它,后面全是空谈。
2.2 号源轮询策略:不是越快越好,而是“准”比“快”重要
很多人以为抢号就是“疯狂刷新”,实则大错。高频轮询不仅触发风控(IP封禁、账号临时冻结),更因服务器返回的号源数据存在缓存延迟,导致你刷到的“有号”其实是10秒前的旧状态。脚本采用分级轮询策略:
- 一级轮询(低频):每30秒拉取一次「科室-医生」列表,确认目标医生是否在线、是否停诊;
- 二级轮询(中频):医生确定后,每10秒拉取其「可约日期」列表,关注新放号日期(如T+7日);
- 三级轮询(高频):目标日期确定后,每2秒拉取该日「具体时段号源」,仅当
available_count > 0且status == "available"才进入提交队列。
这种分层设计,让脚本在保持响应速度的同时,大幅降低无效请求量。核心逻辑如下:
def poll_available_slots(session, doctor_id, target_date): url = f"https://xxx-hospital.gov.cn/api/v1/schedule/slots?doctor_id={doctor_id}&date={target_date}" while True: try: resp = session.get(url, timeout=5) if resp.status_code != 200: print(f"[WARN] 轮询失败 HTTP {resp.status_code}") time.sleep(3) continue slots = resp.json().get("data", []) for slot in slots: # 关键判断:必须同时满足三项 if (slot.get("available_count", 0) > 0 and slot.get("status") == "available" and slot.get("is_real_time", False)): # 标识此条为实时库存,非缓存 return slot # 返回首个可用时段 except Exception as e: print(f"[ERROR] 轮询异常: {e}") time.sleep(2) # 严格控制间隔,避免被限流参数说明:
is_real_time字段是脚本作者从响应体中逆向发现的关键标识。很多系统虽返回available_count>0,但若is_real_time=False,说明该数据来自缓存,实际库存早已售罄。忽略此字段,是90%自写脚本“明明看到有号却提交失败”的根本原因。
2.3 预约提交:签名、时间戳与幂等键的三位一体
最后一步提交,是整个流程中最脆弱的环节。它要求:
- 请求Header中包含动态生成的
X-Request-Time(毫秒级时间戳); - Body中包含
sign字段,由secret_key + timestamp + request_body经HMAC-SHA256生成; - URL参数中携带唯一
request_id(UUID v4),用于后端幂等控制。
脚本将这三者封装为独立函数,确保每次提交都是新鲜、可验证、可重试的:
import hmac import hashlib import time import uuid def generate_sign(payload_dict, secret_key): """payload_dict: 已排序的请求参数字典(不含sign本身)""" # 按key字典序拼接 k1=v1&k2=v2... sorted_items = sorted(payload_dict.items()) query_str = "&".join([f"{k}={v}" for k, v in sorted_items]) # 签名 = HMAC-SHA256(secret_key, timestamp + query_str) timestamp = str(int(time.time() * 1000)) sign_input = timestamp + query_str signature = hmac.new( secret_key.encode(), sign_input.encode(), hashlib.sha256 ).hexdigest() return timestamp, signature # 使用示例 payload = { "doctor_id": "doc_789", "schedule_id": "sch_456", "patient_id": "pat_123", "contact_phone": "138****1234" } ts, sign = generate_sign(payload, "your_secret_from_dev_doc") headers = { "X-Request-Time": ts, "X-Signature": sign, "X-Request-ID": str(uuid.uuid4()) # 每次提交唯一 } resp = session.post( "https://xxx-hospital.gov.cn/api/v1/appointment/submit", json=payload, headers=headers )注意:
secret_key并非固定字符串,而是登录成功后,服务器在/api/v1/user/profile等接口响应头中返回的X-App-Secret字段。脚本在登录后立即调用该接口提取并缓存,确保后续所有签名计算基于最新密钥。这是第二道必须跨过的门槛——密钥过期会导致所有签名失效,返回401。
3. 避坑指南:五个血泪换来的「提交失败」真相
写脚本最耗时的永远不是功能实现,而是排查那些“看起来完全正确,却总失败”的case。以下是我在某跨平台挂号Demo中反复验证、记录下来的5个高频坑点,每一条都对应真实翻车现场:
3.1 现象:登录成功,但后续所有请求返回403 Forbidden
原因:未同步更新X-CSRF-TokenHeader。很多系统在登录成功后,会在响应体中返回新的csrf_token,并要求后续所有POST/PUT请求必须携带该Token。脚本若只在登录页提取一次,后续未刷新,就会触发CSRF拦截。
解决:在每次调用session.post()前,先GET一个轻量接口(如/api/v1/user/status),解析响应体中的csrf_token,并更新session.headers["X-CSRF-Token"]。
3.2 现象:轮询显示“有号”,提交时提示“号源已被预约”
原因:未校验schedule_id的时效性。schedule_id(排班ID)并非永久有效,部分系统设定其2分钟内有效。脚本若在T0时刻获取schedule_id,T0+120s后仍用它提交,必然失败。
解决:将schedule_id与获取时间戳绑定,构造valid_until属性;提交前强制校验time.time() < valid_until,超时则重新拉取。
3.3 现象:验证码识别率低,频繁中断流程
原因:直接调用通用OCR(如Tesseract)识别挂号系统特制验证码,准确率不足30%。其字符常带干扰线、扭曲、粘连,且字体库非标准。
解决:改用轻量CNN模型(如MobileNetV2微调),训练集仅需200张标注图(脚本附带采集脚本collect_captcha.py)。实测识别率达92%,且推理耗时<150ms。
3.4 现象:多线程轮询时,同一时段被重复提交多次
原因:未加分布式锁。当脚本部署在多台机器或多个进程时,多个实例同时检测到同一slot.available_count > 0,各自发起提交,导致超卖或报错。
解决:引入Redis原子操作。提交前执行SETNX lock:slot_{id} {process_id} EX 30,成功才继续;失败则sleep随机100~500ms后重试。脚本内置redis-py适配层,配置开关即可启用。
3.5 现象:提交成功返回200,但查不到预约记录
原因:忽略了「最终确认」步骤。部分系统提交后返回{"code":0,"msg":"已提交"},但实际需再调用/api/v1/appointment/confirm?order_id=xxx完成支付/确认,否则订单2小时后自动取消。
解决:在提交响应中提取order_id,立即调用确认接口。脚本将此设为--auto-confirm命令行选项,默认开启。
4. 环境配置与依赖管理:用requirements.txt锁定真实可用版本
这份脚本不是“pip install all”就能跑通的玩具。它的稳定运行高度依赖特定版本的底层库,尤其是处理HTTPS证书、Cookie策略和JSON解析的模块。我曾因urllib3版本升级导致SSL握手失败,排查3小时才发现是certifi证书包未同步更新。因此,脚本附带的requirements.txt不是简单罗列,而是经过生产环境压测验证的精确组合:
| 包名 | 版本号 | 必选理由 |
|---|---|---|
| requests | 2.31.0 | 兼容OpenSSL 3.0+,修复TLS 1.3下某些医院服务器的SNI握手异常 |
| beautifulsoup4 | 4.12.2 | 解析老旧挂号系统HTML时,对<font>标签嵌套的兼容性最佳 |
| pyexecjs | 1.5.1 | 调用前端JS加密函数时,避免Node.js版本冲突;1.5.1是最后一个支持Python 3.8+的稳定版 |
| redis | 4.6.0 | 与Redis 7.0协议完全兼容,SETNX原子操作无降级 |
| opencv-python | 4.8.0.76 | 验证码预处理(去噪、二值化)性能最优,比PIL快3倍 |
安装命令必须带--no-cache-dir和--force-reinstall,防止pip缓存旧版本:
pip install --no-cache-dir --force-reinstall -r requirements.txt提示:不要用
pip install -U pip升级pip本身。某次升级pip至23.3后,pyexecjs安装失败,因新版pip默认禁用setup.py。解决方案是临时降级:pip install pip==22.3.1,再装依赖。
配置文件config.yaml采用分层设计,隔离敏感信息与业务参数:
# config.yaml auth: username: "138****1234" password_js_path: "./js/login_encrypt.js" # 前端加密脚本路径 captcha_service: "local_ocr" # 可选 local_ocr / baidu_ai / manual hospital: base_url: "https://xxx-hospital.gov.cn" api_prefix: "/api/v1" schedule: target_doctor: "doc_789" target_date: "2024-06-15" polling_interval: department: 30 date: 10 slot: 2 redis: host: "127.0.0.1" port: 6379 db: 0 enabled: true启动命令清晰分离配置与行为:
# 仅轮询,不提交(用于观察号源变化) python main.py --config config.yaml --mode poll # 启用自动提交 + Redis锁 python main.py --config config.yaml --mode submit --lock-redis # 手动输入验证码(调试用) python main.py --config config.yaml --mode submit --captcha-manual5. 实战验证:用curl和日志反向验证脚本行为是否可信
再完美的代码,不经过真实流量验证都是空中楼阁。我给自己定了一条铁律:任何新写的挂号脚本,在投入实际使用前,必须完成「三重日志交叉验证」——即用脚本自身日志、Wireshark抓包、以及curl手动重放,三方比对每一字节是否一致。这听起来繁琐,但从没让我在关键时刻掉链子。
5.1 日志结构化:让每一次请求都可追溯
脚本默认开启DEBUG级别日志,但不是简单print,而是按RFC 5424格式输出结构化JSON行,方便grep与ELK分析:
{ "timestamp": "2024-06-10T08:22:15.342Z", "level": "DEBUG", "event": "http_request", "method": "POST", "url": "https://xxx-hospital.gov.cn/api/v1/appointment/submit", "headers": { "X-Request-Time": "1717998135342", "X-Signature": "a1b2c3...", "X-Request-ID": "d4e5f6..." }, "body": { "doctor_id": "doc_789", "schedule_id": "sch_456", "patient_id": "pat_123" } }关键在于body字段是原始dict,而非序列化后的字符串——这样你能一眼看出是否漏传了contact_phone,而不必去解析JSON字符串。
5.2 curl重放:用最原始的方式确认服务端视角
当脚本提交失败,我第一反应不是改Python代码,而是从日志中复制url、headers、body,用curl重放:
curl -X POST 'https://xxx-hospital.gov.cn/api/v1/appointment/submit' \ -H 'X-Request-Time: 1717998135342' \ -H 'X-Signature: a1b2c3...' \ -H 'X-Request-ID: d4e5f6...' \ -H 'Content-Type: application/json' \ -d '{"doctor_id":"doc_789","schedule_id":"sch_456","patient_id":"pat_123"}'如果curl也失败,说明问题在参数或签名;如果curl成功而脚本失败,则一定是Python端的Session状态(如Cookie过期、Header被覆盖)出了问题。这个动作平均耗时90秒,却能排除80%的“玄学故障”。
5.3 Wireshark过滤:确认TLS层无异常
某些医院系统启用了SNI(Server Name Indication)或ALPN(Application-Layer Protocol Negotiation),而旧版OpenSSL可能不兼容。这时curl成功但脚本失败,就需要Wireshark抓包。我设置的过滤表达式极其简单:
ip.addr == 114.247.123.45 and tls.handshake.type == 1其中114.247.123.45是挂号系统IP(通过nslookup xxx-hospital.gov.cn获取)。重点看Client Hello包里的Server Name字段是否正确,以及Supported Versions是否包含TLS 1.3。若不匹配,就回到requirements.txt,降级requests或升级openssl。
从那以后我每次上线新脚本,都强制走一遍这三步:日志看参数、curl验服务、Wireshark查握手。不是为了炫技,而是因为挂号这件事,容不得半点侥幸——少一次验证,就可能让家人多跑一趟医院。希望帮到你。
本文还有配套的精品资源,点击获取