三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Python自动化脚本实战:医院预约抢号的技术原理与合规实现

Python自动化脚本实战:医院预约抢号的技术原理与合规实现

1. 从需求到实现:为什么我们需要自动化抢号脚本?

最近几年,但凡去过三甲医院挂过专家号的朋友,大概都经历过那种“秒光”的绝望。早上8点整,你准时打开手机App,手指悬在屏幕上方,心跳加速,默念倒数。时间一到,你以最快的速度点击那个“预约”按钮,然后屏幕转圈、卡顿,最后弹出一个冰冷的提示:“号源已满”。整个过程可能不到5秒。这不是你手速不够快,而是你正在和成千上万个同样焦急的患者,以及背后可能存在的、由代码驱动的“对手”进行一场不公平的竞赛。

这就是“医院自动化抢号脚本”诞生的最直接背景。它本质上是一个用程序模拟人类操作,自动完成登录、查询号源、提交预约等动作的工具。在理想情况下,它的速度、精准度和不知疲倦的特性,远超人肉操作。我最初接触这个需求,是帮一位家里有老人需要定期复诊的朋友。他每次挂号都像打仗一样紧张,还经常挂不上,严重影响了治疗。在研究了医院预约平台的规则和流程后,我意识到,用Python写一个轻量级的自动化脚本,在合规的前提下辅助操作,是技术上可行且能解决实际痛点的方案。

这里必须明确一个核心前提:我们讨论的脚本,其目的应是“辅助”而非“恶意抢占”。它应该遵守平台的访问频率限制(Robots协议),模拟正常人类操作间隔,避免对服务器造成压力。它的价值在于帮助那些不熟悉网络操作、或确实因客观原因(如身体不便、工作繁忙)难以抢号的人群,提供一个相对公平的“工具”,而不是成为黄牛牟利的武器。基于这个出发点,我们的技术选型、代码设计和操作逻辑,都需要围绕“合规”、“稳健”和“用户友好”来展开。

2. 技术栈选型与核心原理拆解

要构建一个可靠的自动化脚本,我们首先得拆解“抢号”这个动作包含了哪些技术环节,并为之选择合适的“武器”。

2.1 网络请求的核心:Requests vs. Selenium vs. Playwright

这是最关键的决策点,直接决定了脚本的复杂度、稳定性和运行环境。

1. Requests + BeautifulSoup(纯HTTP请求方案)这是最轻量、最高效的方案。其原理是直接模拟浏览器向服务器发送HTTP请求(GET/POST),并解析返回的HTML或JSON数据。

  • 优点:速度快,资源占用极低,可以部署在服务器或树莓派上7x24小时运行。适合目标预约平台API接口清晰、没有复杂前端验证(如高强度动态加密)的场景。
  • 缺点:无法直接执行JavaScript。如果登录或提交预约的关键步骤依赖JS生成令牌(Token)、计算加密参数或渲染动态内容,单纯使用Requests会非常困难,甚至无法实现。
  • 典型工作流:脚本先发送登录请求(携带用户名、密码或验证码),服务器返回一个会话Cookie(Session)。后续查询号源、提交预约的请求,都需要携带这个Cookie,以表明你的登录状态。

2. Selenium(浏览器自动化方案)这是一个“重量级”但功能强大的方案。它通过WebDriver驱动一个真实的浏览器(如Chrome, Firefox)实例,完全模拟人的所有操作:打开网页、点击、输入、滚动等。

  • 优点:能处理所有前端JavaScript逻辑,包括最复杂的图形验证码(虽然不能识别,但可以渲染出来让人工处理)。几乎可以应对任何网站,通用性最强。
  • 缺点:速度慢,资源消耗大(每个实例都是一个完整的浏览器进程)。部署相对复杂,需要匹配对应版本的浏览器和WebDriver。

3. Playwright(新一代浏览器自动化方案)可以看作是Selenium的现代升级版,由微软开发。它同样驱动真实浏览器,但API更现代,性能更好,内置了自动等待等智能机制,减少了脚本中“sleep”的滥用。

  • 优点:比Selenium更快更稳定,录制生成脚本的功能强大。对单页面应用(SPA)支持更好。
  • 缺点:较新,社区资源相对Selenium少一些。本质上仍属于浏览器自动化,资源开销问题依旧存在。

我的选型建议与理由:对于医院预约这种对时效性要求极高的场景,优先尝试Requests方案。原因如下:

  1. 速度是生命线:浏览器启动、加载页面需要数秒时间,而Requests发送一个请求通常在毫秒级。在“秒杀”场景下,这数秒的差距就是成功与失败的天堑。
  2. 资源与部署:一个轻量的Requests脚本可以很容易地部署在云服务器或家用电脑后台运行,而同时运行多个浏览器实例对硬件要求很高。
  3. 分析过程即是学习:即使最终因为反爬策略太强而不得不使用Selenium/Playwright,前期用Requests去分析网络请求的过程,也能让你彻底理解整个预约流程的数据交互,这对于后续调试和优化至关重要。

因此,本篇文章将主要以“Requests为主,必要时辅以简单浏览器自动化进行登录”的混合架构作为核心思路进行展开。这是一种兼顾效率和通用性的务实选择。

2.2 关键环节技术剖析

确定了核心工具,我们来看看抢号流程中几个必须攻克的技术点。

1. 会话(Session)保持HTTP协议是无状态的。你登录成功后,服务器如何知道下一个查询请求是你发出的?答案就是Session和Cookie。Requests库的Session对象会自动处理Cookie。你的代码应该像这样:

import requests session = requests.Session() # 创建一个会话对象 login_data = {'username': 'your_user', 'password': 'your_pwd'} # 登录请求,session会自动保存服务器返回的cookie login_resp = session.post(login_url, data=login_data) # 后续所有请求都使用这个session,cookie会自动携带 query_resp = session.get(query_url)

这是自动化脚本的基石,务必保证整个流程中使用同一个session对象。

2. 定时与并发策略“抢号”不是简单的一次性请求。它包含监控冲刺两个阶段。

  • 监控阶段:在放号时间点之前,以较低的频率(如每分钟1次)检查页面状态,判断是否即将放号或页面结构是否变化。这里可以使用time.sleep()进行间歇。
  • 冲刺阶段:在预设的放号时间点(如7:59:58),开始以高频率(如每秒2-4次,需谨慎避免被封IP)循环发送预约请求。这里的循环不是for i in range(10),而是while True配合一个退出条件(如预约成功,或超过时间窗口)。

绝对禁止使用多线程/进程对同一账号进行高频并发请求,这无异于对服务器发起DoS攻击,会迅速导致IP或账号被封。正确的“并发”是针对多个不同的、合法的账号,且每个账号仍应遵循合理的请求间隔。

3. 验证码处理这是自动化脚本最大的障碍。常见的验证码有数字字母、滑动拼图、点选文字、算术题等。

  • 简单数字/字母验证码:可以尝试使用OCR库(如ddddocrpytesseract)进行识别。但医院系统验证码通常扭曲严重,识别率不高。
  • 复杂验证码(滑动、点选):目前没有完全可靠的通用破解方案。最实用、最合规的方法是人工干预。脚本运行到验证码步骤时,弹出验证码图片,等待用户手动输入,或者将图片发送到手机,人工识别后回填。虽然牺牲了一点自动化程度,但保证了脚本的可用性和安全性。
  • “讨巧”方案:有些系统的验证码只在登录时出现。我们可以用Selenium/Playwright完成登录(人工处理验证码),获取登录后的Cookie,再交给Requests会话继续后续的自动化操作。这就是“混合架构”的用武之地。

4. 异常处理与日志网络可能波动,服务器可能返回错误,号源可能瞬间消失。一个健壮的脚本必须有完善的异常处理和日志记录。

import logging import traceback logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) try: resp = session.post(submit_url, data=order_data, timeout=5) resp.raise_for_status() # 如果HTTP状态码不是200,抛出异常 result = resp.json() if result['code'] == 0: logger.info("预约成功!订单号:%s", result['orderId']) break # 退出冲刺循环 else: logger.warning("预约失败,原因:%s", result['msg']) except requests.exceptions.Timeout: logger.error("请求超时,网络可能不稳定") except requests.exceptions.RequestException as e: logger.error("网络请求异常:%s", e) except Exception as e: logger.error("发生未知错误:%s", traceback.format_exc())

详细的日志能让你在脚本无声无息失败时,快速定位问题所在。

3. 实战:构建一个稳健的Requests核心脚本

让我们抛开理论,动手搭建一个脚本的骨架。假设我们的目标医院预约平台,登录后查询和提交的接口是相对清晰的API。

3.1 环境准备与依赖安装

首先,确保你的Python环境(建议3.8以上)已经就绪。创建一个新的项目目录,并安装核心库:

pip install requests

如果需要解析HTML,再安装:

pip install beautifulsoup4 lxml

对于验证码OCR,可以尝试安装轻量级的ddddocr

pip install ddddocr

使用requirements.txt来管理依赖是个好习惯。

3.2 核心代码结构拆解

一个完整的脚本通常包含以下几个模块,我们将它们写在一个主文件中,但逻辑上分块。

1. 配置模块将所有可变的参数放在脚本开头,方便修改。

# config.py 或直接写在文件开头 import time # 用户配置 USERNAME = "你的手机号/身份证号" PASSWORD = "你的密码" HOSPITAL_ID = "xxxx" # 医院编号 DEPARTMENT_ID = "xxxx" # 科室编号 DOCTOR_ID = "xxxx" # 医生编号 TARGET_DATE = "2023-12-01" # 目标预约日期 # 网络配置 LOGIN_URL = "https://xxx.com/api/login" QUERY_URL = "https://xxx.com/api/schedule" SUBMIT_URL = "https://xxx.com/api/submitOrder" HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Referer': 'https://xxx.com/appointment', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' } # 策略配置 POLL_INTERVAL = 60 # 监控阶段轮询间隔(秒) RUSH_INTERVAL = 0.3 # 冲刺阶段请求间隔(秒),谨慎设置! RUSH_TIMEOUT = 30 # 冲刺阶段最长持续时间(秒) START_RUSH_TIME = "07:59:58" # 开始冲刺的时钟时间

2. 登录模块这是获取合法会话的关键。

def login(session): """处理登录,返回登录成功的session""" # 第一步:可能先需要GET请求获取登录页面的token或cookie login_page_resp = session.get(LOGIN_URL, headers=HEADERS) # 这里可能需要用BeautifulSoup解析页面,提取隐藏的input值,如csrf_token # soup = BeautifulSoup(login_page_resp.text, 'lxml') # csrf_token = soup.find('input', {'name': '_csrf'})['value'] # 第二步:准备登录数据 login_data = { 'username': USERNAME, 'password': PASSWORD, # '_csrf': csrf_token, # 如果有的话 } # 第三步:处理验证码(如果有) # 方案A:OCR识别(简单验证码) # captcha_url = "https://xxx.com/captcha.jpg" # captcha_resp = session.get(captcha_url) # import ddddocr # ocr = ddddocr.DdddOcr() # captcha_code = ocr.classification(captcha_resp.content) # login_data['captcha'] = captcha_code # 方案B:人工识别(推荐,更稳妥) # 将验证码图片保存到本地或显示出来 # with open('captcha.jpg', 'wb') as f: # f.write(captcha_resp.content) # print("请查看当前目录下的captcha.jpg,并输入验证码:") # captcha_code = input() # login_data['captcha'] = captcha_code # 第四步:发送登录请求 resp = session.post(LOGIN_URL, data=login_data, headers=HEADERS) resp.raise_for_status() # 第五步:检查登录是否成功 # 通常成功后会跳转,或者返回一个包含用户信息的JSON if "登录成功" in resp.text or resp.json().get('success'): print("登录成功!") return True else: print("登录失败,响应:", resp.text[:200]) return False

关键经验:登录是最容易失败的一环。务必使用工具(如浏览器开发者工具的Network面板)仔细查看真实登录过程中的每一个请求,包括那些不起眼的GET请求。很多时候,登录前的那个获取Token的请求和登录请求本身同等重要。

3. 号源查询模块此模块负责在监控阶段检查是否放号,并在冲刺阶段确认号源状态。

def query_available(session, date, hospital_id, dept_id, doctor_id): """查询指定日期、科室、医生的可用号源""" params = { 'hospitalId': hospital_id, 'deptId': dept_id, 'doctorId': doctor_id, 'date': date } try: resp = session.get(QUERY_URL, params=params, headers=HEADERS, timeout=5) data = resp.json() # 假设返回数据结构为:{'code':0, 'data': {'schedules': [{'period':'上午', 'num': 5}, ...]}} if data['code'] == 0 and data['data']['schedules']: available_list = [s for s in data['data']['schedules'] if s['num'] > 0] return available_list return [] except Exception as e: print(f"查询号源失败:{e}") return None # 返回None表示网络或请求错误,与空列表区分

4. 预约提交模块这是最后临门一脚,必须确保数据准确无误。

def submit_order(session, schedule_item): """提交预约订单""" submit_data = { 'hospitalId': HOSPITAL_ID, 'deptId': DEPARTMENT_ID, 'doctorId': DOCTOR_ID, 'scheduleId': schedule_item['id'], # 号源ID 'visitDate': TARGET_DATE, 'period': schedule_item['period'], 'patientId': '就诊人ID', # 通常需要提前在个人中心绑定 'mobile': '手机号' } try: resp = session.post(SUBMIT_URL, data=submit_data, headers=HEADERS, timeout=3) # 冲刺时超时设短 result = resp.json() return result except requests.exceptions.Timeout: return {'code': -1, 'msg': '请求超时'} except Exception as e: return {'code': -1, 'msg': str(e)}

5. 主控调度逻辑这是脚本的大脑,将以上模块串联起来。

def main(): import datetime session = requests.Session() session.headers.update(HEADERS) # 1. 登录 if not login(session): print("登录失败,程序退出") return print("登录成功,开始监控号源...") monitor_start_time = datetime.datetime.now() # 2. 监控阶段 while True: current_time = datetime.datetime.now().strftime("%H:%M:%S") print(f"[{current_time}] 检查号源...") available = query_available(session, TARGET_DATE, HOSPITAL_ID, DEPARTMENT_ID, DOCTOR_ID) if available is None: # 查询出错,等待后继续 time.sleep(POLL_INTERVAL) continue if available: print(f"发现可用号源:{available}") # 如果发现提前放号,可以立即进入冲刺 break else: print("暂无号源") # 判断是否到达冲刺时间 if current_time >= START_RUSH_TIME: print("到达冲刺时间,开始尝试抢号!") break time.sleep(POLL_INTERVAL) # 3. 冲刺阶段 print("进入冲刺提交阶段...") rush_end_time = datetime.datetime.now() + datetime.timedelta(seconds=RUSH_TIMEOUT) success = False while datetime.datetime.now() < rush_end_time and not success: # 冲刺阶段每次请求前都重新查询一次最新号源,确保不提交无效ID available = query_available(session, TARGET_DATE, HOSPITAL_ID, DEPARTMENT_ID, DOCTOR_ID) if not available: # 号源被抢光或查询失败 time.sleep(RUSH_INTERVAL) continue # 遍历所有可用号源尝试提交 for schedule in available: result = submit_order(session, schedule) print(f"提交结果:{result}") if result.get('code') == 0: print(f"!!!预约成功!!!订单信息:{result.get('data')}") success = True break elif result.get('code') == 1001: # 假设1001表示号源已被占用 print(f"号源 {schedule['id']} 已被抢,尝试下一个...") continue else: # 其他错误,如参数错误、重复预约等,根据情况处理 print(f"提交失败:{result.get('msg')}") time.sleep(RUSH_INTERVAL) # 避免死循环无间隔请求 if not success: print("冲刺阶段结束,未能成功预约。") if __name__ == '__main__': main()

4. 高级策略、反爬应对与伦理边界

一个能真正工作的脚本,绝不仅仅是跑通流程。你需要考虑更多现实问题。

4.1 应对反爬虫机制

医院预约平台为了公平和安全,一定会设置反爬措施。

1. 请求头(Headers)伪装这是最基本的一步。你的脚本的User-Agent不能是python-requests/2.28.1,而应该使用一个常见的浏览器标识。Referer(来源页)、Accept-Language等字段也最好一并设置,模拟真实浏览器。如前文HEADERS配置所示。

2. IP限制与代理池单个IP在短时间内发起大量请求,极易被封锁。解决方案是使用代理IP。

  • 免费代理:质量差,不稳定,不推荐用于抢号这种关键任务。
  • 付费代理池服务:一些云服务商提供按需收费的代理IP,质量较高。在代码中,可以在请求时随机切换代理。
    proxies = { 'http': 'http://user:pass@proxy_ip:port', 'https': 'https://user:pass@proxy_ip:port', } resp = session.get(url, proxies=proxies)
    重要警告:对于需要保持会话(登录状态)的请求,切换代理可能导致会话失效,因为服务器可能将IP和Session绑定。通常只在未登录的公开查询请求中使用代理池。

3. 请求参数签名与加密这是最棘手的反爬手段。前端在提交关键请求(如登录、提交订单)时,可能会对参数进行加密,或者添加一个由时间戳、参数等计算出来的sign签名。服务器会验证这个签名,无效则拒绝请求。

  • 如何应对:你需要仔细分析前端JavaScript代码(通常在Chrome开发者工具的Sources或Network面板中找.js文件),找到生成签名或加密参数的函数。然后用Python重写(execjs库可以执行JS代码)或理解其算法后用Python实现。这个过程需要一定的前端逆向工程能力。
  • 一个常见技巧:如果加密逻辑过于复杂,可以退而求其次,使用Selenium/Playwright执行关键动作(如点击提交按钮),让浏览器自然完成JS计算。这就是我们之前提到的“混合架构”——用Requests处理大部分查询,用Selenium处理最复杂的加密提交。

4. 行为指纹检测高级反爬会检测鼠标移动轨迹、点击速度、页面停留时间等。纯Requests脚本没有这些行为,可能被识别。应对方法是在请求间隔中加入随机延时,模拟人类操作的“不规律性”。

import random, time def human_like_delay(base=1, variance=0.5): """模拟人类操作的随机延迟""" delay = base + random.uniform(-variance, variance) time.sleep(max(delay, 0.1)) # 确保延迟不为负

4.2 提升成功率的工程化技巧

1. 时间同步与校准你的脚本时间必须和服务器时间高度同步。使用NTP(网络时间协议)校准本地时间。在冲刺前,可以多次访问一个公共的、提供精确时间的API来估算网络延迟,并据此调整冲刺发起的时间点。比如,服务器时间是8:00:00放号,你发现你的请求到达服务器有200毫秒延迟,那么你应该在7:59:59.800左右开始发送请求。

2. 多时段/多备选方案不要只盯着一个医生的一个时段。在配置中设置多个优先级选项(如第一志愿:医生A上午号;第二志愿:医生A下午号;第三志愿:医生B上午号)。脚本按优先级顺序尝试。

3. 失败重试与状态持久化脚本可能因为网络问题崩溃。可以考虑将登录后的Cookie、当前的尝试状态保存到文件或简单的数据库中。脚本重启后可以读取状态,继续执行,而不是从头开始。

4. 通知机制脚本成功或失败后,应该通知你。最简单的就是播放一个提示音,或者发送一封邮件、一条微信消息(可以通过Server酱、PushPlus等工具实现)。这样你无需一直盯着屏幕。

4.3 必须遵守的伦理与法律边界

这是开发和使用此类脚本的底线,比技术更重要。

  • 尊重Robots协议:检查目标网站的robots.txt文件。如果明确禁止了预约相关路径的爬虫,请慎重考虑。
  • 遵守访问频率限制:将请求频率控制在合理范围,绝对不要试图“压垮”服务器。你的RUSH_INTERVAL设置不应低于0.2秒。高频请求不仅是道德问题,还可能构成对计算机信息系统的非法侵入或破坏,面临法律风险。
  • 禁止商业用途与黄牛行为:脚本应用于本人、家人或朋友的合理就医需求。任何用于牟利、抢号加价出售的行为都是违法的,也是对所有普通患者的严重不公。
  • 个人信息安全:脚本中会保存你的账号密码、个人信息。务必妥善保管代码,不要上传到公开的GitHub仓库。可以在代码中读取环境变量或外部配置文件来存储敏感信息。
  • 技术讨论的尺度:在技术社区分享时,应侧重于通用的网络请求、自动化技术原理和反爬应对策略,避免提供针对特定医院平台的、可直接运行的完整脚本,更不应提供绕过安全措施的详细方法。

技术是一把双刃剑。我们学习并运用自动化技术,是为了提高效率、解决现实困难,而不是制造新的不公。在编写和使用这类脚本时,请时刻保持这份敬畏之心。

← 返回列表