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会非常困难甚至无法实现。典型工作流脚本先发送登录请求携带用户名、密码或验证码服务器返回一个会话CookieSession。后续查询号源、提交预约的请求都需要携带这个Cookie以表明你的登录状态。2. Selenium浏览器自动化方案这是一个“重量级”但功能强大的方案。它通过WebDriver驱动一个真实的浏览器如Chrome, Firefox实例完全模拟人的所有操作打开网页、点击、输入、滚动等。优点能处理所有前端JavaScript逻辑包括最复杂的图形验证码虽然不能识别但可以渲染出来让人工处理。几乎可以应对任何网站通用性最强。缺点速度慢资源消耗大每个实例都是一个完整的浏览器进程。部署相对复杂需要匹配对应版本的浏览器和WebDriver。3. Playwright新一代浏览器自动化方案可以看作是Selenium的现代升级版由微软开发。它同样驱动真实浏览器但API更现代性能更好内置了自动等待等智能机制减少了脚本中“sleep”的滥用。优点比Selenium更快更稳定录制生成脚本的功能强大。对单页面应用SPA支持更好。缺点较新社区资源相对Selenium少一些。本质上仍属于浏览器自动化资源开销问题依旧存在。我的选型建议与理由对于医院预约这种对时效性要求极高的场景优先尝试Requests方案。原因如下速度是生命线浏览器启动、加载页面需要数秒时间而Requests发送一个请求通常在毫秒级。在“秒杀”场景下这数秒的差距就是成功与失败的天堑。资源与部署一个轻量的Requests脚本可以很容易地部署在云服务器或家用电脑后台运行而同时运行多个浏览器实例对硬件要求很高。分析过程即是学习即使最终因为反爬策略太强而不得不使用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, datalogin_data) # 后续所有请求都使用这个sessioncookie会自动携带 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库如ddddocr、pytesseract进行识别。但医院系统验证码通常扭曲严重识别率不高。复杂验证码滑动、点选目前没有完全可靠的通用破解方案。最实用、最合规的方法是人工干预。脚本运行到验证码步骤时弹出验证码图片等待用户手动输入或者将图片发送到手机人工识别后回填。虽然牺牲了一点自动化程度但保证了脚本的可用性和安全性。“讨巧”方案有些系统的验证码只在登录时出现。我们可以用Selenium/Playwright完成登录人工处理验证码获取登录后的Cookie再交给Requests会话继续后续的自动化操作。这就是“混合架构”的用武之地。4. 异常处理与日志网络可能波动服务器可能返回错误号源可能瞬间消失。一个健壮的脚本必须有完善的异常处理和日志记录。import logging import traceback logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) try: resp session.post(submit_url, dataorder_data, timeout5) 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可以尝试安装轻量级的ddddocrpip 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; charsetUTF-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, headersHEADERS) # 这里可能需要用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, # 如果有的话 } # 第三步处理验证码如果有 # 方案AOCR识别简单验证码 # 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, datalogin_data, headersHEADERS) 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, paramsparams, headersHEADERS, timeout5) 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, datasubmit_data, headersHEADERS, timeout3) # 冲刺时超时设短 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(secondsRUSH_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:passproxy_ip:port, https: https://user:passproxy_ip:port, } resp session.get(url, proxiesproxies)重要警告对于需要保持会话登录状态的请求切换代理可能导致会话失效因为服务器可能将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(base1, variance0.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仓库。可以在代码中读取环境变量或外部配置文件来存储敏感信息。技术讨论的尺度在技术社区分享时应侧重于通用的网络请求、自动化技术原理和反爬应对策略避免提供针对特定医院平台的、可直接运行的完整脚本更不应提供绕过安全措施的详细方法。技术是一把双刃剑。我们学习并运用自动化技术是为了提高效率、解决现实困难而不是制造新的不公。在编写和使用这类脚本时请时刻保持这份敬畏之心。
返回列表