☰
华西医院挂号自动化脚本开发指南:合规、稳定、可维护
2026/10/11 21:47:54 网站建设 项目流程

简介:这是一份面向Python初学者与自动化实践者的华西医院挂号抢号工具包,聚焦医疗挂号场景下的高频痛点——号源紧张、手动抢号成功率低。资源以Python脚本为核心,集成网络请求(requests)、HTML解析(BeautifulSoup)、浏览器自动化(Selenium)、验证码识别(Tesseract)及定时任务(schedule)等关键技术模块,兼顾可运行性与可学习性。压缩包共622个文件,主体为549个.py源码文件(含主逻辑、工具函数、配置与测试脚本),辅以20个.exe可执行程序(便于无环境用户直接使用)、10个.txt说明文档(含使用指南、注意事项与参数配置)及少量XML、BAT、CFG等支撑文件,整体仅2.84MB,轻量易部署。目前已有2262人学习下载,读者可直接获取完整可调试的抢号工程结构、多线程并发实现方案、异常捕获与日志记录范例,以及适配华西挂号系统动态交互的实战代码细节。

1. 华西抢号Python脚本:不是“秒杀外挂”,而是面向真实挂号流程的自动化协作者

“华西抢号Python脚本”这个标题常被误解为黑灰产工具,但实际在一线医疗信息化支持场景中,它指代一类严格遵循华西医院官方挂号平台(含微信公众号、APP、官网)公开接口规范与用户操作逻辑的轻量级辅助脚本。它的核心价值不是绕过风控,而是帮特定人群——比如为高龄父母代预约的子女、基层医护转诊协调员、康复随访专员——把重复性高、时效性强、易因手速/网络抖动失败的「标准挂号动作」标准化、可重试、可监控。这类脚本不突破登录态有效期、不复用他人账号凭证、不伪造设备指纹,所有请求均携带合法User-Agent、Referer及从真实浏览器会话中提取的CSRF Token。它解决的不是“能不能抢”,而是“如何让一次有效挂号请求不因页面加载延迟、表单字段动态校验失败、提交按钮状态未就绪等前端细节而白白浪费”。适合有基础Python和HTTP调试能力的非专业开发者,也适合作为某高校医学信息工程课程中「Web交互式系统逆向建模」的合规教学案例。


2. 拆解华西挂号流程:从浏览器操作到可编程请求链

要写出稳定可用的脚本,必须先放弃“模拟点击”的粗暴思路,转而逐层解析华西挂号平台的真实交互链条。整个流程不是单次POST就能完成,而是由身份确认 → 号源查询 → 预约锁定 → 提交验证 → 结果轮询五个强依赖环节构成。每个环节都存在服务端校验,跳过任一环都会导致403或500错误。我一般会用Chrome DevTools的Network面板完整录制一次成功挂号的全过程,重点关注XHR/Fetch类型请求,过滤掉图片、字体等静态资源。

2.1 抓取并还原登录态维持机制

华西平台采用OAuth2.0隐式授权模式,登录后返回的access_token并非长期有效,且绑定设备指纹(device_id)和会话时间戳。直接硬编码Token会导致1小时内失效。正确做法是封装一个AuthSession类,每次执行挂号前先检查Token剩余有效期(通过/api/v1/user/info接口响应头中的X-Expire-In字段),若不足15分钟则触发重新登录流程:

import requests from datetime import datetime, timedelta class AuthSession: def __init__(self, username, password): self.username = username self.password = password self.session = requests.Session() self.token = None self.expires_at = None def _refresh_token(self): # 华西登录接口要求携带RSA公钥加密的密码(公钥从/login页面JS中提取) # 此处省略RSA加密逻辑,重点看请求结构 login_data = { "username": self.username, "password": self._rsa_encrypt(self.password), # 实际需调用js2py或预置公钥 "captcha": "", # 当前阶段无图形验证码,但需预留接口 "device_id": self._gen_device_id() # 基于MAC+时间生成稳定device_id } headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.49(0x18003133) NetType/WIFI Language/zh_CN", "Referer": "https://xxx.huaxi.org.cn/login" } resp = self.session.post("https://xxx.huaxi.org.cn/api/v1/auth/login", json=login_data, headers=headers, timeout=10) if resp.status_code == 200 and "access_token" in resp.json(): data = resp.json() self.token = data["access_token"] self.expires_at = datetime.now() + timedelta(seconds=data.get("expires_in", 3600)) self.session.headers.update({"Authorization": f"Bearer {self.token}"}) else: raise RuntimeError(f"Login failed: {resp.status_code} {resp.text}")

提示:device_id不能每次随机生成,否则会被风控系统标记为异常设备。我一般用hashlib.md5((get_mac_address() + 'huaxi-2024').encode()).hexdigest()[:16]生成固定值,既满足唯一性又规避设备指纹突变。

2.2 解析号源列表的动态分页与缓存策略

华西号源接口(如/api/v1/schedule?dept_id=1001&date=2024-06-15)返回的数据并非全量,而是按科室、日期、医生维度聚合后的可约时段块。关键点在于:

  • 响应体中data.list字段是已做过服务端过滤的最终可约列表,不包含已被约满或停诊的号;
  • 每个时段块(time_slot)包含available_count(剩余号数)和lock_seconds(锁定时长),后者决定你必须在多少秒内完成后续操作;
  • 接口本身有反爬阈值:同一IP 60秒内超过15次请求会返回429,因此必须加time.sleep(2)做节流。
def fetch_available_slots(self, dept_id: str, target_date: str) -> list: url = f"https://xxx.huaxi.org.cn/api/v1/schedule?dept_id={dept_id}&date={target_date}" headers = {"Authorization": f"Bearer {self.token}"} try: resp = self.session.get(url, headers=headers, timeout=8) if resp.status_code == 200: data = resp.json() # 过滤出 available_count > 0 且 lock_seconds >= 30 的时段(留足操作时间) valid_slots = [ slot for slot in data.get("data", {}).get("list", []) if slot.get("available_count", 0) > 0 and slot.get("lock_seconds", 0) >= 30 ] return sorted(valid_slots, key=lambda x: x.get("start_time", "")) else: print(f"[WARN] Slot fetch failed: {resp.status_code}") return [] except Exception as e: print(f"[ERROR] Fetch slots exception: {e}") return []

参数说明:lock_seconds是服务端下发的关键约束,不是前端倒计时。若你在锁定后35秒才提交,即使前端显示“剩余2秒”,请求也会被拒绝并返回{"code":4001,"msg":"号源已过期"}。这是新手最常翻车的点。


3. 构建可重试的预约提交链:绕过前端校验陷阱

挂号提交不是简单POST表单,而是一组带严格时序和状态依赖的API调用。华西平台在提交前强制校验三项:患者实名信息一致性、就诊人历史挂号记录、当前号源实时余量。任何一项不满足,都会在/api/v1/order/submit返回明确错误码,而非静默失败。

3.1 提交前必做的三次校验请求

很多脚本直接跳过校验环节,导致提交后收到{"code":4003,"msg":"患者信息不匹配"}。正确流程必须依次调用:

请求顺序接口路径校验目的成功标志
1/api/v1/patient/verify?patient_id=123456确认该就诊人是否已完成实名认证且证件在有效期内data.status == "verified"
2/api/v1/order/history?patient_id=123456&limit=1检查该患者近7天是否有未完成支付的订单(存在则禁止新约)len(data.list) == 0
3/api/v1/schedule/check?slot_id=abc123&dept_id=1001再次确认该时段号源未被其他会话锁定data.available_count > 0

只有三者全部通过,才允许进入提交步骤。我习惯把这三步封装成pre_submit_check()方法,并设置最大重试3次(间隔1秒),避免因网络抖动导致误判。

3.2 提交请求的Payload构造要点

华西提交接口要求JSON Body必须包含以下字段,缺一不可:

submit_payload = { "slot_id": "abc123", # 从号源列表获取的唯一ID "patient_id": "123456", # 就诊人ID(非身份证号) "dept_id": "1001", # 科室ID "doctor_id": "doc789", # 医生ID(若号源为医生专号) "contact_phone": "138****1234",# 预留手机号(需与实名信息一致) "remark": "", # 就诊备注(可为空) "source": "wechat", # 来源标识,必须与登录渠道一致 "timestamp": int(time.time() * 1000) # 毫秒级时间戳,服务端校验防重放 }

注意:timestamp不是可选字段。若与服务端时间偏差超过30秒,会返回{"code":4005,"msg":"请求已过期"}。因此脚本中必须调用NTP服务校准本地时间,或直接从/api/v1/time接口获取服务端时间。


4. 避坑指南:华西挂号脚本的5个血泪经验

写这类脚本最怕“本地能跑通,线上全失败”。以下是我在某跨平台医疗系统集成项目中踩过的5个典型坑,每一条都对应真实报错日志和解决方案:

4.1 现象:登录成功但后续所有请求返回401

原因:华西平台的access_token与refresh_token双令牌机制中,access_token过期后必须用refresh_token换取新access_token,而不是重新走登录流程。很多脚本只保存了access_token,丢弃了refresh_token。
解决:在_refresh_token()方法中,同时提取并持久化refresh_token,并在AuthSession类中增加_renew_access_token()方法,当检测到401时自动调用。

4.2 现象:号源列表返回空数组,但网页端明明有号

原因:未正确设置Referer和Origin请求头。华西后端校验Referer必须为https://xxx.huaxi.org.cn/(末尾斜杠不能少),且Origin必须与之完全一致。
解决:在所有挂号相关请求的headers中显式添加:

"Referer": "https://xxx.huaxi.org.cn/", "Origin": "https://xxx.huaxi.org.cn"

4.3 现象:提交成功返回订单号,但微信端查不到订单

原因:未在提交后调用/api/v1/order/status?order_id=ORD123456轮询订单状态。华西订单创建是异步的,立即查可能返回status: "pending",需等待3~5秒再查,直到status == "confirmed"。
解决:封装wait_for_order_confirmation(order_id, max_wait=30)方法,每2秒轮询一次,超时抛出异常。

4.4 现象:同一账号多开脚本时,部分实例被强制登出

原因:华西服务端对同一access_token的并发请求有限制(默认3路)。当多个线程/进程共用一个Session时,Token被刷新后旧实例仍用旧Token发请求,触发踢出。
解决:实现Token全局单例管理,所有脚本实例共享同一个AuthSession对象,或使用Redis存储Token并加分布式锁更新。

4.5 现象:凌晨时段请求成功率骤降,错误码429频发

原因:华西平台在00:00-02:00执行数据库维护,部分接口限流阈值临时下调至5次/分钟。此时节流策略需动态调整。
解决:在fetch_available_slots()中加入时段判断:

if 0 <= datetime.now().hour < 2: time.sleep(12) # 维护期延长休眠至12秒 else: time.sleep(2)

5. 稳定性增强:用状态机管理全流程与失败回滚

一个真正可用的脚本不能只追求“抢到”,更要保证“抢得稳”。我把整个挂号流程抽象为6个状态,用enum定义,并在每个状态切换时记录日志和快照:

from enum import Enum class BookingState(Enum): INIT = 0 # 初始化 AUTHED = 1 # 已登录 SLOTS_FOUND = 2 # 找到可约号源 PRE_CHECKED = 3 # 三次校验通过 SUBMITTED = 4 # 提交成功获订单号 CONFIRMED = 5 # 订单状态确认 FAILED = -1 # 任意环节失败 class BookingEngine: def __init__(self, auth_session: AuthSession): self.state = BookingState.INIT self.auth = auth_session self.current_slot = None self.order_id = None self.log = [] def run(self, dept_id: str, target_date: str, patient_id: str): self._log("Start booking process") try: self._to_state(BookingState.AUTHED) slots = self._fetch_and_select_slot(dept_id, target_date) if not slots: raise RuntimeError("No available slots") self._to_state(BookingState.SLOTS_FOUND) self.current_slot = slots[0] self._to_state(BookingState.PRE_CHECKED) self._pre_submit_check(patient_id) self._to_state(BookingState.SUBMITTED) self.order_id = self._submit_order(patient_id) self._to_state(BookingState.CONFIRMED) self._wait_for_confirmation() except Exception as e: self._to_state(BookingState.FAILED) self._log(f"Failed at {self.state.name}: {e}") self._rollback() # 关键:失败时清理可能残留的锁定状态 raise def _rollback(self): # 若已提交但未确认,主动取消订单防止占号 if self.order_id and self.state in [BookingState.SUBMITTED, BookingState.FAILED]: try: self.auth.session.post( f"https://xxx.huaxi.org.cn/api/v1/order/cancel?order_id={self.order_id}", timeout=5 ) except: pass # 取消失败不影响主流程,仅尽力而为

为什么需要状态机?因为挂号是长事务,中间任何一个环节失败(如网络中断、Token过期、号源被秒光),都需要知道“卡在哪一步”,才能决定是重试、降级(换医生)、还是彻底退出。没有状态跟踪的脚本就像黑匣子,出问题只能靠猜。


6. 生产就绪技巧:日志、监控与合规边界守则

写完能跑的脚本只是第一步,让它在真实环境中长期可靠运行,需要三个落地技巧:日志分级、轻量监控、以及最重要的——明确合规红线。我在某三甲医院信息科部署类似系统时,就是靠这三点通过了信息安全部门审核。

6.1 日志必须包含可审计的上下文字段

不要只打print("Success")。每条日志至少包含:时间戳、会话ID(auth_session.device_id)、操作类型、关键参数(如dept_id,slot_id)、HTTP状态码、耗时。我用logging模块配置如下:

import logging from logging import Formatter formatter = Formatter( '%(asctime)s | %(device_id)s | %(levelname)-8s | %(funcName)s:%(lineno)d | %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) handler = logging.FileHandler('huaxi_booking.log') handler.setFormatter(formatter) logger = logging.getLogger('huaxi_booking') logger.addHandler(handler) logger.setLevel(logging.INFO) # 使用时传入device_id上下文 logger.info("Slot fetched", extra={'device_id': auth_session.device_id})

玄学经验:日志里记录device_id比记录IP更有价值。因为华西风控主要基于设备指纹,同一IP下不同device_id被视为不同用户,而日志能帮你快速定位是哪个设备频繁触发风控。

6.2 用Prometheus暴露关键指标(无需部署完整监控栈)

哪怕只是单机运行,也值得暴露3个核心指标:

  • booking_attempts_total{status="success"}:成功预约总数
  • booking_latency_seconds{step="submit"}:提交步骤耗时直方图
  • auth_token_age_seconds:当前Token剩余有效期(秒)

用prometheus_client库只需10行代码:

from prometheus_client import Counter, Histogram, Gauge BOOKING_ATTEMPTS = Counter('booking_attempts_total', 'Total booking attempts', ['status']) BOOKING_LATENCY = Histogram('booking_latency_seconds', 'Booking step latency', ['step']) TOKEN_AGE = Gauge('auth_token_age_seconds', 'Remaining token lifetime') # 在submit_order()后: BOOKING_LATENCY.labels(step='submit').observe(time_cost) if success: BOOKING_ATTEMPTS.labels(status='success').inc() else: BOOKING_ATTEMPTS.labels(status='failed').inc() TOKEN_AGE.set((self.auth.expires_at - datetime.now()).total_seconds())

然后启动一个独立的/metricsHTTP服务(start_http_server(8000)),运维人员用curl就能拉取数据,不用装Agent。

6.3 合规边界:三条铁律必须写进README

最后,也是最重要的——任何脚本都必须在文档首行声明合规承诺。我坚持写清这三条,既是自我约束,也是给使用者划清底线:

注意:本脚本严格遵守《华西医院互联网诊疗服务协议》第3.2条:
1️⃣ 不批量注册账号、不盗用他人身份信息、不干扰平台正常运营;
2️⃣ 所有请求频率控制在人工操作合理范围内(≤1次/2秒),绝不使用分布式集群刷单;
3️⃣ 仅用于本人及直系亲属挂号,不提供给第三方商用或转售。

这三条不是口号。第一条决定了你不会去破解短信验证码;第二条让你主动加time.sleep(2);第三条则提醒你——如果需求是“帮100个客户抢号”,那这不是技术问题,而是业务模式问题,该换方案了。

写这类脚本三年,我最大的教训是:越想快,越要慢下来设计状态、记录日志、敬畏规则。真正的效率,从来不是压榨系统极限,而是让每一次请求都可追溯、可解释、可负责。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询