ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python编写114预约挂号自动监控脚本:从接口分析到验证码处理

Python编写114预约挂号自动监控脚本:从接口分析到验证码处理

简介:基于114平台挂号场景编写的抢号辅助工具,采用HTML技术结合Node-WebKit桌面运行环境,主要面向反复登录114预约挂号的个人用户。工具在放号后只需输入手机验证码即可完成挂号,设计成功率接近100%,用于简化预约流程、提高抢号效率。资源以ZIP压缩包形式提供,整体体积约107.44MB,内含NW.js运行目录以及Windows、Mac系统启动脚本;由于114网站域名已调整,原始代码可能无法直接运行,但清晰的功能划分与作者列出的修复计划仍有借鉴意义。从修复进展可见:获取病人列表、医院科室信息、验证Cookie有效性等模块已经完成,自动登录、医院列表与科室值班信息尚在处理,医生抓取以及Cookie自动更新待后续优化;未放号前无法提前获取医生信息也是抢号链路中的关键难点,读者可借此了解基于NW.js的桌面挂号工具实现思路,或参考其中的Cookie维护、自动交互与数据抓取逻辑。目前已有1013人学习浏览,适合对Web自动化与桌面应用开发有兴趣的开发者。

1. 114刷号到底在刷什么:先讲清边界,再谈脚本

早上 8 点的三甲医院放号,号源从「可预约」变成「约满」往往只需要十几秒,手速再快也拼不过专业挂号的。所谓「114刷号」,就是用脚本盯着 114 预约挂号平台的号源接口,在放号瞬间自动查询、筛选剩余号源并提交预约。它解决的不是「抢号」本身,而是「盯梢」的效率问题——人不可能盯着手机刷新一小时,脚本可以。适合谁?自己或家人有明确就诊需求、愿意花半小时配置一次脚本的普通用户,以及需要给老人定期复查挂号的子女。但先说清楚:这不是黄牛工具,不绕过验证码、不做高并发爆破,只是在合规前提下把「查到号」和「挂上号」这两个动作自动化。

2. 刷号之前先把规则摸清楚:114放号机制与合规边界

2.1 114平台号源是怎么放出来的:固定时段与随机补号

我最早做这个东西的时候,以为放号就是定时刷新一次接口那么简单,结果连续三天什么都没刷到,后来才发现不同医院、不同科室的放号时间根本不在一个点上。114 预约挂号平台的放号规则大致分三档:一部分医院是提前 7 天凌晨 0 点放号,一部分是每天早 8 点放当天的剩余号源,还有一部分是「随机补号」——比如上午 10 点有人退号,系统不定时把号放回池子里。

这三点决定了刷号脚本的第一个核心设计:不能只盯一个时间点,要盯「时间段」。常见做法是维护一张放号时间表,把医院的放号时刻、补号规律记录进去,脚本在对应时段提高轮询频率,其他时间低频巡检就行。很多人把刷号失败归咎于网速,其实输在信息上——你根本不知道号是什么时候放的,脚本自然也不知道。

2.2 合规边界:什么能自动,什么必须人工

刷号脚本最容易翻车的地方不是代码写错,是规则意识。114 平台的用户协议明确禁止用自动化工具干扰正常预约秩序,所以设计脚本时必须划三条线:第一,不绕过验证码。平台要求填写的图形验证码、短信验证码,要么靠人工输入,要么靠 OCR 识别后仍然走正常的校验流程,不能直接跳过。第二,不暴力并发。单账号每秒最多发一两次请求,多账号并行这种操作想都别想,既违反协议又容易被封。第三,不在放号时间之外高频空转。

我一般会把脚本定位成「监控提醒 + 半自动提交」:脚本负责盯号源,一旦发现有余号就立刻通知你,由你决定是否提交预约;如果你确认开启自动提交模式,也只在验证码识别通过后发起一次请求。这套边界既能满足实际需求,又能让脚本长期稳定运行而不被平台拉黑。

2.3 分析预约接口:从浏览器开发者工具里拿到关键请求

在写代码之前,先得搞明白 114 平台的前端是怎么查号、怎么预约的。具体方法是:打开浏览器开发者工具的 Network 面板,勾选 Fetch/XHR,然后在预约页面手动操作一遍「选择科室 → 选择日期 → 查询号源 → 提交预约」,把过程中产生的所有接口请求记录下来。

我整理过一轮常见接口,大致是这样一张表:

请求用途接口路径(示例)关键参数
登录/api/loginphone、password
查询号源/api/schedule/queryhospitalId、departmentId、date
提交预约/api/bookingscheduleId、patientId、captcha
获取验证码/api/captcha无(返回图片)

重点关注三件事:请求头里的 Cookie 是否包含登录态、查询接口返回的 JSON 结构、提交预约时必填的参数有哪些。最简单的方法是在 Network 面板里右键某个请求,选择 Copy as cURL,然后在终端里跑一遍,看返回结果。拿到这手信息后,脚本的骨架基本就清楚了。

3. 从登录到拿到号源列表:一个能跑起来的最小脚本

3.1 环境准备与会话保持

环境上我推荐 Python 3.9 以上版本,配合 requests 库就够了。不要一上来就上 Selenium、Playwright 这类浏览器自动化工具——它们启动慢、占用高、容易被页面检测到,而且 114 这类平台的页面结构一变,整个脚本就得跟着重写。用 requests 直接调接口,反而稳定得多。

第一步是创建会话并登录,代码里最关键的是 Session 对象的复用:

import requests session = requests.Session() # 模拟浏览器请求头,避免被服务端识别为脚本 session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://www.114gh.com/", # 示例域名,替换为实际地址 "Origin": "https://www.114gh.com" }) # 登录接口 login_url = "https://www.114gh.com/api/login" login_data = { "phone": "138****1234", "password": "your_password", "captcha": "" # 登录验证码,如有则人工输入或OCR } resp = session.post(login_url, json=login_data) print(resp.json())

代码逻辑很简单:先创建一个 requests.Session(),后续所有请求都用这个 session 发,因为 Session 会自动保存服务端返回的 Cookie,登录态就这么一路带过去。运行登录请求后,打印返回的 JSON,确认 code 字段是否为 0 或 success。

参数说明里有两个容易被忽略的点:User-Agent 不要用默认的 python-requests,会被服务端直接拒绝或要求验证;Referer 必须写成平台页面地址,否则部分接口会拦截跨域请求。登录接口如果返回「验证码错误」之类的提示,说明登录环节也有图形验证码,这时候需要单独处理。

3.2 查询指定科室号源:把接口返回的 JSON 变成可读清单

登录成功后的第一个正事是查号源。这里最麻烦的不是接口本身,而是参数里的 hospitalId 和 departmentId 从哪拿。常见做法是打开目标医院的科室预约页,地址栏 URL 里通常带一长串参数,其中就有这两个 ID。我一般会写个小函数把它们解析出来,然后动态传入。

def query_schedule(session, hospital_id, department_id, target_date): """查询指定医院、科室、日期的号源列表""" url = "https://www.114gh.com/api/schedule/query" params = { "hospitalId": hospital_id, "departmentId": department_id, "date": target_date } resp = session.get(url, params=params) data = resp.json() if data.get("code") != 0: print("查询失败:", data.get("msg")) return [] # 过滤出还有剩余号源的时间段 available = [ item for item in data.get("items", []) if item.get("remaining", 0) > 0 ] for item in available: print(f"{item['timeRange']} 剩余 {item['remaining']} 个号") return available

这段代码做了两件事:构造查询参数并发送 GET 请求,然后把返回的 JSON 里 remaining 大于 0 的时间段过滤出来。日期参数target_date格式必须是YYYY-MM-DD,可以是固定写死的,也可以动态生成——比如想挂下周一,就datetime.date.today() + datetime.timedelta(days=...)。

有个坑值得提前说:部分科室返回的items里可能包含「停诊」状态,这时候remaining为 0 但停诊原因写在stopReason字段里。我在初版脚本里没判断这个字段,结果某天查到某个时间段显示 0 个号,就误以为没放号,实际是医生临时停诊。建议在过滤条件里加一层item.get("status") != "stopped"。

3.3 定时轮询:用简单循环代替复杂调度

号源查询接口写好后,剩下的就是轮询的节奏问题。很多人一上来就用while True + time.sleep(1)高频刷新,结果半小时内就被服务端限流,IP 被封了一整天。正确的做法是分时段调整频率。

import time import random TARGET_HOUR = 8 # 目标放号时间点 TARGET_MINUTE = 0 while True: now = time.localtime() # 放号前 30 秒到放号后 60 秒:高频轮询 is_hot_window = ( now.tm_hour == TARGET_HOUR and now.tm_min == 0 and now.tm_sec >= 30 ) or ( now.tm_hour == TARGET_HOUR and now.tm_min == 1 and now.tm_sec <= 0 ) if is_hot_window: items = query_schedule(session, hospital_id, department_id, target_date) if items: print("发现余号,开始提交预约流程") # 这里接入后面的提交逻辑 break # 放号高峰期用 0.8~1.5 秒随机间隔 time.sleep(random.uniform(0.8, 1.5)) else: # 非高峰期每 30 秒巡检一次 time.sleep(30)

代码里的is_hot_window定义了一个时间窗口:放号前 30 秒到放号后 1 分钟。这个窗口内用随机间隔的高频轮询,窗口外降到 30 秒一次。为什么用random.uniform而不是固定 1 秒?因为固定间隔的请求在服务端看来就是机器行为,随机抖动才会被当作正常用户操作。

如果你不想让脚本一直挂着,也可以用系统的 cron 或 Windows 计划任务在放号时间前几分钟启动脚本,跑完自动退出,更省资源。但前提是你得清楚目标医院的确切放号时间,否则脚本可能在错误的时间窗口里空转。

4. 从「查到号」到「挂上号」:提交预约与验证码处理

4.1 提交预约请求:字段时序与成功判定

查到了余号,真正的考验才开始。提交预约和查询号源最大的区别是:查询是幂等的,多查几次没问题;提交是非幂等的,发一次请求就可能真的挂上一个号,发两次就可能重复预约或者触发风控。所以提交请求的代码要写得特别小心。

def submit_booking(session, schedule_id, patient_id, captcha_text): """提交预约请求""" url = "https://www.114gh.com/api/booking" booking_data = { "scheduleId": schedule_id, # 时间段ID,来自查询结果 "patientId": patient_id, # 就诊人ID,提前维护 "captcha": captcha_text # 验证码,必须正确 } resp = session.post(url, json=booking_data) result = resp.json() # 以业务码为准判断成功,而不是 HTTP 状态码 if result.get("code") == 0: print("预约成功,订单号:", result.get("orderId")) return True else: print("预约失败:", result.get("msg")) return False

这里的核心逻辑在最后几行:HTTP 200 不代表成功,必须看业务码code。我见过有人用resp.status_code == 200判断成功,结果预约失败但脚本显示成功,最后白白错过了真正能挂上号的机会窗口。返回码的语义可以在登录和查询接口上先验证一遍,摸清平台的统一格式。

参数说明里,scheduleId必须来自查询结果中具体时间段对应的 ID,不能自己拼接;patientId是就诊人的唯一标识,可以在平台的「就诊人管理」页面里找到。首次使用建议先手动操作一次预约,然后在 Network 面板里对比参数名,确认完全一致。

4.2 验证码这块:OCR 只能当辅助,人工兜底才稳

114 平台的验证码主要是图形验证码,偶尔会有滑块。滑块验证码不要想着自动过,直接走人工输入最稳。图形验证码可以用 OCR 识别,我用的是 ddddocr 这个开源库,识别率在简单背景下能到 85% 左右,但碰到扭曲字体或彩色干扰线就会翻车。

import ddddocr # 初始化 OCR 引擎 ocr = ddddocr.DdddOcr(show_ad=False) def get_captcha_text(session): """获取验证码图片并识别""" captcha_url = "https://www.114gh.com/api/captcha" resp = session.get(captcha_url) # 直接识别 code = ocr.classification(resp.content) print("OCR识别结果:", code) return code.strip()

这段代码流程很简单:请求验证码图片接口,拿到图片的二进制内容,直接丢给 ddddocr 识别。如果识别出来的结果长度和验证码要求不一致(比如验证码是 4 位,识别出 3 位),就再试一次,最多重试 3 次。

但这里我必须说得坦白一点:OCR 在验证码上的真实表现并不稳定,我实测的准确率在 60% 到 90% 之间波动,完全取决于当时的图片风格。所以我在正式脚本里做了一个折中:先用 OCR 自动识别,如果提交预约后返回「验证码错误」,脚本会暂停并推送通知给手机,由你人工打开平台页面完成预约。听起来很原始,但这恰恰是最不会被风控的方案——人工兜底意味着你永远不会因为验证码环节的失败而错过号源。

4.3 并发竞争的现实:同一秒大家都在点,脚本怎么减少失败

就算你秒速提交了预约,也不一定抢到号源,因为在同一秒可能有几十个人在提交,平台的库存扣减是实时的、不加锁的。这不是脚本能解决的问题,但可以通过策略把失败概率压低。

第一个策略是「提交前再确认一次」:在提交预约前 0.3 秒再调用一次查询接口,确认目标时间段的remaining仍然大于 0,否则就换下一个时间段。第二个策略是「失败后快速重试一次」:如果首次提交返回「号源已被抢完」,立即用下一个候选时间段重新提交。第三个策略是不要用多个 Session 并发提交同一个号源,反而会触发风控,让账号被临时锁定。

def try_booking_with_fallback(session, available_items, patient_id): """按优先级依次尝试提交预约""" for item in available_items: # 提交前再确认一次号源状态 check = query_schedule( session, hospital_id, department_id, target_date ) if not any(x["scheduleId"] == item["scheduleId"] for x in check): continue # 获取验证码并提交 captcha = get_captcha_text(session) ok = submit_booking(session, item["scheduleId"], patient_id, captcha) if ok: return True time.sleep(1) return False

这段代码是把前面几个模块串起来的完整提交逻辑。available_items按你想要的优先级排序——比如最想要的时间段排在最前,代码会依次尝试;每次提交前会重新确认号源状态,避免白提交;失败后隔 1 秒再试下一个,而不是同一时间段死磕。

5. 114刷号避坑指南:五次踩坑换来的五个教训

现象一:登录成功但查询接口返回 401

排在第一位的坑是登录态丢失。我初版脚本在登录后,直接拿着 session 去请求查询接口,前几次没问题,跑了半小时后就突然全部返回 401 未认证。查了半天才发现,平台的某些接口要求附带一个自定义请求头X-Token,这个值在登录接口的返回体里,需要手动提取并塞进后续请求的 Header 里。

解决方法是登录成功后立即解析返回 JSON,把 token 存到变量,并更新 Session 的默认请求头:

login_result = resp.json() token = login_result.get("data", {}).get("token", "") if token: session.headers.update({"X-Token": token})

这个坑的启示是:114 这类平台的认证不只有 Cookie 一种方式,很多接口同时校验 Cookie 和自定义 Token。遇到 401 不要怀疑人生,先去 Network 面板里复制一个成功请求的 Header,逐项对比差异。

现象二:刷了两小时一个号没查到,第二天手一点就刷到了

这个坑是我第一次做刷号脚本时踩的。我先把脚本挂在 8 点放号时段跑,连续三天没查到任何号源,一度以为是接口参数写错了。后来第四天我闲着没事手动打开预约页面看了一眼,发现科室列表里根本没有我要挂的那位医生的排班——医生当天停诊了。问题根源是脚本只管查号源,没管排班状态。

解决办法是在查询函数里增加对status字段的判断逻辑:

# 过滤停诊时间段 available = [ item for item in data.get("items", []) if item.get("remaining", 0) > 0 and item.get("status") != "stopped" and item.get("doctorStatus") != "off" ]

踩完这个坑之后,我养成了一个习惯:每天第一次运行脚本时,先打印完整的号源列表并记录到日志里,而不是只打印有余号的时间段。这样即使当天一个号都没有,你也能区分「没有放号」和「医生停诊」两种情况。

现象三:请求过于频繁被限流,IP 被封了一天

限流是刷号脚本最容易踩的坑,也是我最开始提到的「不要暴力并发」的原因。有一次我为了测试并发效果,开了三个 Session 同时高频查询,结果不到五分钟,所有请求都返回了code=1003和一段提示文本——IP 直接被临时封禁了,连浏览器手动访问都进不去。

解决方法是把请求频率控制在「单账号每秒最多 1.5 个请求」以内,并且加上随机间隔。我在轮询代码里的random.uniform(0.8, 1.5)就是这么来的。如果被封了,最常见的做法是等 1 到 24 小时自动解封,没有别的捷径可走。另外要特别注意:即使是监控提醒模式,轮询间隔也不要低于 5 秒,否则长时间挂机仍然可能触发限流。

现象四:日期格式不统一,查询永远返回「无号源」

这个坑非常隐蔽。平台的日期参数在查询接口里要求2025-06-01这种带横杠的格式,但在提交预约接口里却要求20250601这种紧凑格式。如果复用同一个日期变量,查询正常,提交预约就会失败,而且报错信息是「日期格式错误」,很容易被忽略。

解决办法是在脚本里单独封装两个格式化函数:

def format_date_dash(dt): return dt.strftime("%Y-%m-%d") # 查询接口用 def format_date_compact(dt): return dt.strftime("%Y%m%d") # 提交接口用

凡是跨接口复用参数,都必须逐个接口确认格式。养成一个习惯:在日志里打印每次请求的完整 URL 和参数,哪怕只是自查也能省下大量排查时间。

现象五:预约成功但订单没生成,提示「就诊人信息不完整」

最后一个坑出在就诊人信息上。我的脚本提交预约时用了就诊人 ID,但平台要求提交时携带就诊人的身份证号、医保类型等额外字段。脚本里只传了一个 ID,导致预约请求返回「就诊人信息不完整」,而手动操作平台页面却没有这个问题——因为页面上已经把信息填好了。

解决方法是提交预约的参数里,把就诊人的关键信息都带上,身份证号码脱敏处理:

booking_data = { "scheduleId": schedule_id, "patientId": patient_id, "patientName": "张三", # 替换为实际就诊人 "idCard": "410***********1234", # 脱敏后的身份证号 "captcha": captcha_text }

这个坑的教训是:脚本不能完全照抄抓包结果,要结合平台页面的实际提交逻辑,把缺失的业务字段补全。

6. 从脚本到日常工具:定时任务、通知推送与验证习惯

经过前面几步,你已经有了一个「能查号、能提交、不会踩大坑」的刷号脚本。但脚本只是第一步,真正的价值在于把它部署成一个每周自动运行的「号源哨兵」,并且在你睡觉时也能把消息推到手机上。

我的部署方式是直接挂系统 cron,每天在放号时间前 5 分钟启动一次,跑完自动退出,不常驻内存:

# 每天 07:55 启动刷号脚本,8:00 放号前就绪 55 7 * * * cd /opt/114guard && /usr/bin/python3 guard_114.py >> logs/guard.log 2>&1

通知推送用的是 Server酱或钉钉机器人这类现成方案。查到号源时推一条带时间段的微信/钉钉消息,预约成功再推一条包含订单号的确认消息:

def send_notify(title, content): """发送通知到手机""" url = "https://sctapi.ftqq.com/{token}.send" data = {"title": title, "desp": content} resp = requests.post(url, data=data) if resp.status_code == 200: print("通知已发送")

日志这一步很多人会省略,但它可能是整个脚本里最重要的基础设施。我的习惯是每次运行都记录这些信息:查询的时间点、接口返回的原始 JSON、发现余号的时间、提交预约的结果。连续记录两周之后,你就能总结出目标医院的真实放号规律——比如某医院名义上是 8 点放号,实际 7 点 58 分就出了第一批号源。这个认知比任何代码优化都值钱。

我自己的使用习惯是:脚本从不真正全自动提交,永远开着人工确认这道闸。每次推送消息到手机后,我手动打开平台页面核对一下时间段和医生,再确认提交。这个过程多花十秒钟,但换来的是一年多没有被平台风控、没有被取消过订单的安心。刷号这个方向,做的越稳,跑的越久。

最后说一句实在话:如果你的目标是给家人挂一个专家号,这个监控思路完全够用;如果目标是做号源生意,趁早收手。工具从来是双刃剑,用在对的地方才算本事。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进