1. 从个人脚本到企业级项目的蜕变之路几年前为了帮朋友抢一张回家的火车票我写下了第一个基于Selenium的12306抢票脚本。那时的代码现在回头看简直不忍直视所有逻辑都塞在一个几百行的.py文件里账号密码硬编码验证码识别靠截图后手动点运行起来就像在钢丝上跳舞随时可能因为网络波动、页面元素加载延迟而崩溃。它确实在某个深夜帮我抢到过票但那种脆弱和不确定性让我意识到这远远不够。这个最初的“玩具脚本”恰恰是很多自动化测试工程师或开发者入门时的缩影。我们用Selenium模拟点击、填充表单完成一个特定任务但代码缺乏结构、容错性差、无法复用。而“企业级项目”意味着什么它不仅仅是“能用”更要可靠、可维护、可监控、可扩展。意味着你的代码需要经受住高并发、反爬策略、复杂业务流程的考验并且能被团队其他成员轻松理解、部署和运维。把我的12306抢票脚本打磨成这样一个项目整个过程就是一次完整的自动化测试工程化实践。这不仅仅是优化一个脚本更是构建一个高可用、高可维护的Web自动化测试框架的绝佳案例。无论你是想深入Selenium还是希望将手头的自动化测试代码提升到生产级别这里的思路和踩过的坑都值得你仔细琢磨。2. 项目整体架构设计与核心思路拆解2.1 原始脚本的痛点分析与重构目标最初的脚本是一个典型的“面条式代码”。所有功能——从登录、查询余票、选择乘客、识别验证码、提交订单——都线性地排列在同一个主函数里。这种结构的弊端非常明显单点故障任何一个步骤失败如验证码识别错误整个流程就会中断需要人工介入从头开始。难以维护想修改查询条件或增加一个乘客类型需要在几百行代码里小心翼翼地寻找对应逻辑。无法复用登录模块、查询模块、订单模块紧密耦合无法被其他脚本或测试用例调用。缺乏监控脚本运行时在后台“黑盒”运行成功与否、卡在哪个环节完全靠猜或者看终端输出。配置僵化车次、日期、乘客等信息直接写在代码里每次修改都需要动代码。基于这些痛点我设定了明确的重构目标模块化将核心功能拆分为独立、职责单一的模块。鲁棒性引入重试机制、异常处理、完备的日志系统。可配置化所有可变参数如账号、行程、策略通过配置文件或外部接口管理。状态可观测实时监控脚本运行状态、关键步骤结果。策略可插拔例如验证码识别可以随时切换不同的服务商打码平台、本地OCR模型。2.2 企业级自动化测试项目架构选型为了实现上述目标我设计了一个分层架构这同样是构建健壮UI自动化测试框架的通用思路。1. 驱动层这是最底层直接与浏览器交互。核心是WebDriver的管理与封装。我放弃了每次运行都创建新Driver的方式转而使用WebDriverPool连接池的概念。这允许脚本在遇到某些致命错误时如浏览器崩溃能快速从池中获取一个新的、已登录状态的Driver实例继续执行任务而不是整个脚本失败。同时在这一层集中处理了Selenium常见的痛点反爬对抗通过CDPChrome DevTools Protocol协议注入脚本以隐藏WebDriver特征如navigator.webdriver属性并随机化浏览器指纹User-Agent, Viewport等。智能等待封装了显式等待WebDriverWait与自定义等待条件例如等待某个元素可点击且未被遮挡而不仅仅是存在。2. 核心能力层这一层封装了针对12306网站的具体操作但以“页面对象模型Page Object Model, POM”的思想进行设计。我为登录页、查询页、订单确认页等都创建了独立的类。每个类内部封装了该页面的元素定位器和操作方法。例如class LoginPage: def __init__(self, driver): self.driver driver self.username_input (By.ID, username) self.password_input (By.ID, password) self.login_button (By.ID, loginSub) def login(self, username, password): self._input_username(username) self._input_password(password) self._handle_slider_captcha() # 处理滑动验证码 self._click_login_button()POM模式的最大好处是将页面元素的定位与业务操作分离。当12306前端页面改版只需要修改对应Page类中的元素定位器所有调用该页面操作的业务逻辑代码都无需改动。3. 业务逻辑层这一层编排核心能力层的操作形成完整的业务流程。例如“抢票”这个业务会被拆解为登录 - 查询余票 - 选择车次和座位 - 填写乘客 - 处理验证码 - 提交订单 - 支付。每个步骤都是一个独立的“任务Task”或“用例”。业务逻辑层负责调用不同的Page对象方法并处理步骤之间的状态传递和决策例如如果首选车次无票则自动查询备选车次。4. 策略与配置层这是项目的“大脑”。所有动态决策都放在这里。购票策略定义优先级是时间优先还是车次优先是否接受无座。重试策略某个步骤失败后重试几次重试的间隔如何设置建议使用指数退避避免频繁请求验证码策略使用哪个识别接口本地识别失败后是否自动切换为第三方打码平台通知策略成功、失败或出现异常时通过什么渠道邮件、钉钉、微信机器人通知用户所有这些策略都通过配置文件如YAML、JSON或数据库来管理实现业务逻辑与控制的彻底解耦。5. 服务与支撑层这是企业级项目的标志包括日志系统使用logging模块进行分级DEBUG, INFO, WARNING, ERROR日志记录不仅输出到控制台还写入文件方便事后追溯问题。日志内容要包含关键信息如用户ID、车次、当前执行步骤等。监控告警在关键步骤埋点记录耗时和成功与否。可以将这些指标发送到监控系统如Prometheus并配置告警规则例如“连续3次验证码识别失败”触发告警。数据持久化将抢票记录、成功/失败日志存入数据库如SQLite/MySQL便于生成报表和分析成功率。任务调度与管理如果需要定时抢票或管理多个抢票任务可以引入简单的任务队列如Redis RQ或调度器如APScheduler。2.3 技术栈选型背后的思考Selenium 4.x这是基础。选择它是因为其广泛的社区支持、成熟的API以及对现代Web标准的良好支持。相较于Playwright或CypressSelenium在中文社区的资源更丰富遇到问题更容易找到解决方案。Playwright 作为备选/互补在项目后期我评估了Playwright。它的确在速度、自动等待和录制工具上有优势。我的策略是核心稳定流程用Selenium因为已有大量稳定代码对于一些新的、对性能要求高的爬取或测试任务可以尝试用Playwright实现作为技术储备。两者可以共存通过一个统一的Driver抽象层来调用。验证码识别这是12306自动化最大的挑战。我采用了混合策略本地OCR如ddddocr、paddleocr处理简单的数字、字母验证码。速度快、零成本。第三方打码平台如超级鹰、图鉴处理复杂的滑动、点选验证码。作为降级方案当本地识别失败或置信度低时自动调用。需要将平台API进行封装做到可插拔替换。手动兜底在关键流程如提交订单前设置超时若自动识别失败则通过系统通知或日志高亮提示用户手动干预。消息通知集成钉钉/企业微信机器人。脚本状态变化时自动发送Markdown格式的消息到群聊实现“无人值守但状态可知”。3. 核心模块深度解析与避坑指南3.1 对抗反爬让Selenium“隐身”12306等现代网站都有完善的反爬机制来检测自动化工具。直接使用原生Selenium的WebDriver很容易被识别。以下是我实测有效的几种“隐身”技巧1. 修改CDP参数Chrome DevTools Protocol这是目前最有效的方法之一。Selenium 4提供了直接执行CDP命令的能力。from selenium import webdriver from selenium.webdriver import ChromeOptions options ChromeOptions() # 添加一些常用选项避免基本检测 options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions) # 关键执行CDP命令覆盖navigator.webdriver属性 driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, { get: () undefined }); // 可以继续覆盖其他属性如plugins, languages等 })这段代码会在每个新页面加载前注入JavaScript将navigator.webdriver属性设置为undefined从而绕过许多基于此属性的检测。2. 随机化浏览器指纹固定的User-Agent、屏幕分辨率、时区等都是指纹的一部分。需要随机化或模拟真实用户。import random user_agents [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ..., # ... 更多UA ] options.add_argument(fuser-agent{random.choice(user_agents)}) # 也可以随机化窗口大小 driver.set_window_size(random.randint(1200, 1920), random.randint(800, 1080))3. 模拟人类操作行为完全程序化的操作如瞬间移动鼠标、匀速点击容易被识别。需要加入随机延迟和人类化的移动轨迹。from selenium.webdriver.common.action_chains import ActionChains import time import random def human_like_click(element): 模拟人类点击先移动停顿再点击 action ActionChains(driver) # 将鼠标移动到元素附近的一个随机点 action.move_to_element_with_offset(element, random.randint(-5, 5), random.randint(-5, 5)) action.pause(random.uniform(0.1, 0.3)) # 随机停顿 action.click() action.perform() # 对于滑动验证码更需要模拟加速度移动轨迹而非匀速直线。 def human_like_drag_and_drop(source, target_offset_x): action ActionChains(driver) action.click_and_hold(source) total_time random.uniform(0.5, 2.0) # 总滑动时间随机 steps 30 step_time total_time / steps current_x 0 for i in range(steps): # 加速度模型开始慢中间快结束慢 progress i / steps # 使用贝塞尔曲线或正弦函数计算每步位移更接近真人 step_distance target_offset_x * (progress ** 0.5) # 简单示例 move_x step_distance - current_x action.move_by_offset(int(move_x), 0) action.pause(step_time) current_x step_distance action.release().perform()避坑指南反爬策略是不断升级的。没有一劳永逸的方案。上述方法需要组合使用并定期更新。一个重要的原则是不要过于频繁地访问目标网站设置合理的请求间隔这是最有效也是最道德的“隐身”方式。3.2 验证码识别混合策略与降级方案12306的验证码经历了多次升级从简单的数字字母到现在的滑动、点选图片。单一识别方法风险极高。1. 本地OCR识别针对传统验证码对于尚未升级为交互式验证码的环节如某些旧版登录入口可以使用本地OCR。import ddddocr # 一个识别率不错的开源OCR库 def recognize_captcha_local(image_path): ocr ddddocr.DdddOcr() with open(image_path, rb) as f: img_bytes f.read() captcha_text ocr.classification(img_bytes) return captcha_text注意事项本地OCR需要下载模型文件首次使用可能较慢。识别准确率并非100%需要设置置信度阈值低于阈值则触发降级方案。2. 第三方打码平台集成这是处理复杂验证码的可靠方案。以“超级鹰”为例你需要将其API封装成一个服务类。import requests from typing import Optional class ChaoJiYingClient: def __init__(self, username, password, soft_id): self.username username self.password password self.soft_id soft_id self.base_url http://upload.chaojiying.net/Upload/Processing.php def recognize(self, image_bytes, codetype: int) - Optional[str]: 识别验证码返回识别结果或None data { user: self.username, pass: self.password, softid: self.soft_id, codetype: codetype, # 验证码类型代码 } files {userfile: (captcha.jpg, image_bytes)} try: resp requests.post(self.base_url, datadata, filesfiles, timeout10) result resp.json() if result.get(err_no) 0: return result.get(pic_str) else: self.logger.error(f打码平台识别失败: {result}) return None except Exception as e: self.logger.error(f调用打码平台API异常: {e}) return None关键点codetype参数至关重要必须根据验证码类型如4位数字、5位英文、滑动缺口等选择正确的代码。这需要查阅打码平台的文档。3. 混合策略调度器在实际业务中我设计了一个CaptchaSolver类来统一调度。class CaptchaSolver: def __init__(self, local_ocr_enabledTrue, third_party_clientNone): self.local_ocr_enabled local_ocr_enabled self.third_party_client third_party_client self.local_confidence_threshold 0.8 # 本地识别置信度阈值 def solve(self, image_bytes, captcha_type) - str: result None # 策略1优先使用本地OCR如果启用且类型匹配 if self.local_ocr_enabled and captcha_type in [digit, letter]: result, confidence self._solve_local(image_bytes) if confidence self.local_confidence_threshold: return result # 置信度不足继续尝试策略2 # 策略2使用第三方打码平台 if self.third_party_client: result self.third_party_client.recognize(image_bytes, self._map_to_third_party_code(captcha_type)) if result: return result # 策略3所有自动方案失败抛出异常由上层业务逻辑决定是重试还是转为手动 raise CaptchaSolveFailedException(所有验证码识别方案均失败)这种设计保证了识别流程的弹性和高可用性。3.3 状态管理与异常恢复机制企业级项目必须能优雅地处理失败并从断点恢复。1. 上下文状态管理定义一个TicketContext或TaskContext类贯穿整个业务流程。它记录当前任务的所有状态登录会话、查询条件、选中的车次、乘客信息、当前执行步骤等。这个上下文对象可以被序列化如转换成JSON保存到文件或Redis中。2. 步骤重试与断路器模式对于网络请求、元素点击等可能失败的操作必须包装重试逻辑。我使用了tenacity库它提供了强大的重试装饰器。from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type from selenium.common.exceptions import NoSuchElementException, TimeoutException, StaleElementReferenceException retry( stopstop_after_attempt(3), # 最多重试3次 waitwait_exponential(multiplier1, min2, max10), # 指数退避等待 retryretry_if_exception_type((NoSuchElementException, TimeoutException, StaleElementReferenceException)), reraiseTrue # 重试次数用尽后抛出原始异常 ) def safe_find_element(driver, locator, timeout10): 查找元素内置重试机制 # 这里可以使用WebDriverWait但retry提供了更灵活的重试条件控制 from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC return WebDriverWait(driver, timeout).until(EC.presence_of_element_located(locator))对于验证码识别这种高失败率的操作可以引入**断路器Circuit Breaker**模式。连续失败多次后断路器“跳闸”短时间内直接拒绝调用该服务如第三方打码平台转而使用备用方案或直接失败防止雪崩。3. 持久化与断点续跑在关键步骤如登录成功、查询到车次、提交订单成功之后立即将TicketContext序列化并持久化。如果脚本因任何原因崩溃网络中断、程序错误重启后可以先加载最近的上下文判断上次执行到了哪一步然后从该步骤的后一步开始继续执行而不是从头开始。这极大地提高了抢票的成功率尤其是在秒杀场景下。4. 完整抢票流程的工程化实现4.1 环境准备与依赖管理使用poetry或pipenv管理项目依赖并明确区分开发环境和生产环境。requirements.txt或pyproject.toml文件必须清晰。# pyproject.toml 示例 [tool.poetry.dependencies] python ^3.8 selenium ^4.10.0 webdriver-manager ^3.8.6 # 自动管理浏览器驱动 requests ^2.31.0 ddddocr ^1.4.11 tenacity ^8.2.2 pydantic ^2.0.0 # 用于数据验证和设置管理 pyyaml ^6.0 # 读取配置文件 schedule ^1.2.0 # 定时任务如果需要 python-dotenv ^1.0.0 # 管理环境变量 [tool.poetry.group.dev.dependencies] pytest ^7.4.0 pytest-html ^3.2.0 selenium-wire ^5.1.0 # 用于抓包和调试使用webdriver-manager可以自动下载和匹配对应版本的ChromeDriver省去手动管理的麻烦。4.2 配置文件与参数设计采用YAML格式的配置文件结构清晰易读。# config.yaml user: username: your_12306_account password: your_password # 强烈建议从环境变量读取不直接写在配置文件里 journey: from_station: 上海 to_station: 北京 date: 2024-01-01 train_numbers: [G1, G3, G5] # 优先车次 seat_type: 二等座 passenger_names: [张三] strategy: retry_times: 3 query_interval_seconds: 5 # 查询余票频率不宜过短 enable_notification: true notification_webhook: https://oapi.dingtalk.com/robot/send?access_tokenxxx captcha: primary_solver: local # local / third_party third_party: name: chaojiying username: ${CJY_USERNAME} # 使用环境变量 password: ${CJY_PASSWORD} soft_id: 123456使用pydantic来验证配置数据的结构和类型确保配置正确无误。from pydantic import BaseModel, Field from typing import List, Optional class JourneyConfig(BaseModel): from_station: str to_station: str date: str # 可以进一步用datetime.date验证 train_numbers: List[str] Field(default_factorylist) seat_type: str 二等座 passenger_names: List[str] class AppConfig(BaseModel): user: UserConfig journey: JourneyConfig strategy: StrategyConfig captcha: CaptchaConfig # 加载和验证配置 import yaml with open(config.yaml, r) as f: config_data yaml.safe_load(f) config AppConfig(**config_data)4.3 核心业务流程代码框架以下是高度简化的主流程框架展示了各模块如何协同工作import logging from context import TicketContext from driver_pool import WebDriverPool from pages import LoginPage, QueryPage, OrderPage from captcha_solver import CaptchaSolver from notifier import DingTalkNotifier from config import load_config class TicketGrabber: def __init__(self, config): self.config config self.logger logging.getLogger(__name__) self.driver_pool WebDriverPool(size2) # 维护一个小型Driver池 self.context TicketContext() self.captcha_solver CaptchaSolver(...) self.notifier DingTalkNotifier(...) def run(self): self.notifier.send(抢票任务开始启动) try: driver self.driver_pool.acquire() self.context.driver driver # 1. 登录 if not self._login(driver): self.logger.error(登录失败任务终止) return self._save_context() # 登录成功保存状态 # 2. 循环查询并尝试下单 while not self.context.order_created: available_trains self._query_tickets(driver) if available_trains: selected_train self._select_train(available_trains) success self._submit_order(driver, selected_train) if success: self.context.order_created True self.logger.info(订单提交成功) self.notifier.send(f抢票成功车次{selected_train.number}) break # 跳出循环 # 未找到票或下单失败等待后继续查询 time.sleep(self.config.strategy.query_interval_seconds) # 3. 后续支付等流程可选 # self._handle_payment(driver) except Exception as e: self.logger.exception(抢票流程发生未预期异常) self.notifier.send(f抢票任务异常终止: {str(e)}) finally: if hasattr(self, context) and self.context.driver: self.driver_pool.release(self.context.driver) def _login(self, driver) - bool: 登录流程包含验证码处理 login_page LoginPage(driver) login_page.go_to() login_page.input_username(self.config.user.username) login_page.input_password(self.config.user.password) # 处理验证码 captcha_image login_page.get_captcha_image() captcha_text self.captcha_solver.solve(captcha_image, slide) # 假设是滑动验证码 login_page.input_captcha(captcha_text) login_page.click_login_button() return login_page.is_login_successful(timeout10) def _query_tickets(self, driver) - List[TrainInfo]: 查询余票返回符合条件的有票车次列表 query_page QueryPage(driver) query_page.search(self.context.journey) # 解析页面获取车次、余票、座位信息 trains query_page.parse_available_trains() # 根据策略过滤车次如只选高铁、优先时间等 filtered_trains self._apply_strategy(trains) return filtered_trains def _submit_order(self, driver, train) - bool: 选择车次、乘客、提交订单 order_page OrderPage(driver) order_page.select_train(train) order_page.select_passengers(self.context.passenger_names) # 可能再次出现验证码 if order_page.has_confirm_captcha(): captcha_img order_page.get_confirm_captcha_image() captcha_result self.captcha_solver.solve(captcha_img, click) # 点选验证码 order_page.input_confirm_captcha(captcha_result) return order_page.submit_order()这个框架将复杂的流程分解为一个个可测试、可维护的方法并且通过TicketContext贯穿始终通过DriverPool管理资源通过CaptchaSolver处理难点通过Notifier反馈状态具备了企业级应用的雏形。5. 部署、监控与持续优化5.1 部署方式从本地到服务器个人脚本在本地IDE里运行企业级项目则需要稳定、持久的运行环境。本地开发/调试使用IDE直接运行配合pdb或ipdb进行调试。利用selenium-wire拦截和检查网络请求对于分析登录和下单接口非常有帮助。服务器部署推荐环境选择一台稳定的Linux服务器如Ubuntu。无头浏览器在服务器上运行必须使用无头模式--headlessnew。Chrome的无头模式现在已经非常成熟几乎可以模拟所有有头浏览器的行为。进程管理使用systemd或supervisor来管理抢票脚本进程。这样可以实现开机自启、崩溃自动重启、日志轮转等功能。下面是一个简单的systemd服务单元文件示例# /etc/systemd/system/ticket-grabber.service [Unit] DescriptionTicket Grabber Service Afternetwork.target [Service] Typesimple Userubuntu WorkingDirectory/opt/ticket-grabber EnvironmentPATH/usr/local/bin:/usr/bin EnvironmentDISPLAY:99 # 如果需要虚拟显示可以安装Xvfb ExecStart/usr/local/bin/poetry run python main.py --config /opt/ticket-grabber/config.yaml Restarton-failure RestartSec10s StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target容器化进阶使用Docker封装整个环境包括Chrome、Chromedriver、Python依赖。这能保证环境一致性方便迁移和扩展。Dockerfile中需要安装Chrome浏览器和对应的驱动。5.2 日志、监控与告警日志使用Python标准库logging配置不同的Handler将INFO及以上日志输出到控制台和文件将ERROR日志单独发送到错误追踪服务如Sentry或通过通知器告警。import logging from logging.handlers import RotatingFileHandler def setup_logging(): logger logging.getLogger() logger.setLevel(logging.INFO) # 控制台Handler c_handler logging.StreamHandler() c_format logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - %(message)s) c_handler.setFormatter(c_format) logger.addHandler(c_handler) # 文件Handler按大小轮转 f_handler RotatingFileHandler(ticket_grabber.log, maxBytes10*1024*1024, backupCount5) f_format logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s) f_handler.setFormatter(f_format) logger.addHandler(f_handler)监控在代码关键节点埋点记录指标。例如使用time模块记录每个步骤的耗时使用计数器记录验证码识别成功/失败次数。这些数据可以推送到Prometheus然后用Grafana制作仪表盘实时查看脚本健康度、成功率、耗时趋势。告警除了通过通知器发送成功/失败消息外可以设置更智能的告警。例如如果连续10次查询都因“网络错误”失败可能意味着IP被限制需要触发高级别告警。或者验证码识别成功率在1小时内低于50%也需要通知维护人员检查识别服务。5.3 常见问题排查与性能调优问题1脚本运行一段时间后浏览器卡死或无响应。排查检查内存使用情况。Selenium的Driver和浏览器实例会占用较多内存尤其是长时间运行或有内存泄漏时。解决定期重启Driver。可以在成功下单后或每运行一定时间如2小时后主动调用driver.quit()然后从DriverPool获取新的实例。使用driver.execute_script(window.close();)关闭不必要的标签页。确保在finally块或异常处理中正确释放Driver资源将其归还给连接池或退出。问题2元素找不到NoSuchElementException或状态不对StaleElementReferenceException。排查这是UI自动化最常见的问题。通常是页面尚未加载完成、元素被动态加载、或iframe未切换。解决强制使用显式等待几乎所有的find_element操作都应该包裹在WebDriverWait中等待元素满足某个条件如可点击、可见。更稳定的定位器优先使用id其次是name、css_selector。避免使用绝对XPath。如果元素是动态生成的尝试用包含部分文本或属性的XPath或CSS选择器。处理iframe在操作iframe内的元素前务必使用driver.switch_to.frame(frame_reference)进行切换操作完后用driver.switch_to.default_content()切回。问题3被12306直接屏蔽或要求短信验证。排查行为模式被识别为机器人。操作太快、频率太高、Cookie异常都可能导致。解决降低频率增加查询和操作之间的随机延迟。模拟真人如前所述加入随机鼠标移动、滚动页面等操作。维护会话尝试保存成功的登录Cookie下次启动时复用避免频繁登录。可以使用pickle库序列化driver.get_cookies()然后通过driver.add_cookie(cookie)来恢复。IP代理如果服务器IP被封锁可以考虑使用高质量的住宅代理IP池进行轮换。但这会显著增加复杂度和成本。性能调优建议并行与并发如果需要监控多个车次或日期可以考虑使用多线程或异步IO如asyncioplaywright的异步API。但要注意12306对同一账号的并发请求很可能有严格限制盲目并发可能导致账号被风控。更安全的做法是单线程多任务队列有序处理。资源复用WebDriverPool和CaptchaSolver的实例都应设计为单例或共享资源避免重复创建的开销。缓存对相对静态的数据进行缓存如车站名称与代码的映射关系避免每次查询都去解析页面获取。把这个过程走完你收获的不仅仅是一个能稳定抢票的工具更是一套应对复杂Web自动化测试场景的工程化方法论。从混沌的脚本到清晰的项目从脆弱的执行到鲁棒的流程每一步的思考与重构都是你作为开发者或测试工程师能力的坚实沉淀。记住好的自动化项目代码只是载体其灵魂在于对业务深刻的理解、对异常周全的考量以及对用户体验不懈的追求。
从Selenium脚本到企业级自动化测试框架:12306抢票项目工程化实践
1. 从个人脚本到企业级项目的蜕变之路几年前为了帮朋友抢一张回家的火车票我写下了第一个基于Selenium的12306抢票脚本。那时的代码现在回头看简直不忍直视所有逻辑都塞在一个几百行的.py文件里账号密码硬编码验证码识别靠截图后手动点运行起来就像在钢丝上跳舞随时可能因为网络波动、页面元素加载延迟而崩溃。它确实在某个深夜帮我抢到过票但那种脆弱和不确定性让我意识到这远远不够。这个最初的“玩具脚本”恰恰是很多自动化测试工程师或开发者入门时的缩影。我们用Selenium模拟点击、填充表单完成一个特定任务但代码缺乏结构、容错性差、无法复用。而“企业级项目”意味着什么它不仅仅是“能用”更要可靠、可维护、可监控、可扩展。意味着你的代码需要经受住高并发、反爬策略、复杂业务流程的考验并且能被团队其他成员轻松理解、部署和运维。把我的12306抢票脚本打磨成这样一个项目整个过程就是一次完整的自动化测试工程化实践。这不仅仅是优化一个脚本更是构建一个高可用、高可维护的Web自动化测试框架的绝佳案例。无论你是想深入Selenium还是希望将手头的自动化测试代码提升到生产级别这里的思路和踩过的坑都值得你仔细琢磨。2. 项目整体架构设计与核心思路拆解2.1 原始脚本的痛点分析与重构目标最初的脚本是一个典型的“面条式代码”。所有功能——从登录、查询余票、选择乘客、识别验证码、提交订单——都线性地排列在同一个主函数里。这种结构的弊端非常明显单点故障任何一个步骤失败如验证码识别错误整个流程就会中断需要人工介入从头开始。难以维护想修改查询条件或增加一个乘客类型需要在几百行代码里小心翼翼地寻找对应逻辑。无法复用登录模块、查询模块、订单模块紧密耦合无法被其他脚本或测试用例调用。缺乏监控脚本运行时在后台“黑盒”运行成功与否、卡在哪个环节完全靠猜或者看终端输出。配置僵化车次、日期、乘客等信息直接写在代码里每次修改都需要动代码。基于这些痛点我设定了明确的重构目标模块化将核心功能拆分为独立、职责单一的模块。鲁棒性引入重试机制、异常处理、完备的日志系统。可配置化所有可变参数如账号、行程、策略通过配置文件或外部接口管理。状态可观测实时监控脚本运行状态、关键步骤结果。策略可插拔例如验证码识别可以随时切换不同的服务商打码平台、本地OCR模型。2.2 企业级自动化测试项目架构选型为了实现上述目标我设计了一个分层架构这同样是构建健壮UI自动化测试框架的通用思路。1. 驱动层这是最底层直接与浏览器交互。核心是WebDriver的管理与封装。我放弃了每次运行都创建新Driver的方式转而使用WebDriverPool连接池的概念。这允许脚本在遇到某些致命错误时如浏览器崩溃能快速从池中获取一个新的、已登录状态的Driver实例继续执行任务而不是整个脚本失败。同时在这一层集中处理了Selenium常见的痛点反爬对抗通过CDPChrome DevTools Protocol协议注入脚本以隐藏WebDriver特征如navigator.webdriver属性并随机化浏览器指纹User-Agent, Viewport等。智能等待封装了显式等待WebDriverWait与自定义等待条件例如等待某个元素可点击且未被遮挡而不仅仅是存在。2. 核心能力层这一层封装了针对12306网站的具体操作但以“页面对象模型Page Object Model, POM”的思想进行设计。我为登录页、查询页、订单确认页等都创建了独立的类。每个类内部封装了该页面的元素定位器和操作方法。例如class LoginPage: def __init__(self, driver): self.driver driver self.username_input (By.ID, username) self.password_input (By.ID, password) self.login_button (By.ID, loginSub) def login(self, username, password): self._input_username(username) self._input_password(password) self._handle_slider_captcha() # 处理滑动验证码 self._click_login_button()POM模式的最大好处是将页面元素的定位与业务操作分离。当12306前端页面改版只需要修改对应Page类中的元素定位器所有调用该页面操作的业务逻辑代码都无需改动。3. 业务逻辑层这一层编排核心能力层的操作形成完整的业务流程。例如“抢票”这个业务会被拆解为登录 - 查询余票 - 选择车次和座位 - 填写乘客 - 处理验证码 - 提交订单 - 支付。每个步骤都是一个独立的“任务Task”或“用例”。业务逻辑层负责调用不同的Page对象方法并处理步骤之间的状态传递和决策例如如果首选车次无票则自动查询备选车次。4. 策略与配置层这是项目的“大脑”。所有动态决策都放在这里。购票策略定义优先级是时间优先还是车次优先是否接受无座。重试策略某个步骤失败后重试几次重试的间隔如何设置建议使用指数退避避免频繁请求验证码策略使用哪个识别接口本地识别失败后是否自动切换为第三方打码平台通知策略成功、失败或出现异常时通过什么渠道邮件、钉钉、微信机器人通知用户所有这些策略都通过配置文件如YAML、JSON或数据库来管理实现业务逻辑与控制的彻底解耦。5. 服务与支撑层这是企业级项目的标志包括日志系统使用logging模块进行分级DEBUG, INFO, WARNING, ERROR日志记录不仅输出到控制台还写入文件方便事后追溯问题。日志内容要包含关键信息如用户ID、车次、当前执行步骤等。监控告警在关键步骤埋点记录耗时和成功与否。可以将这些指标发送到监控系统如Prometheus并配置告警规则例如“连续3次验证码识别失败”触发告警。数据持久化将抢票记录、成功/失败日志存入数据库如SQLite/MySQL便于生成报表和分析成功率。任务调度与管理如果需要定时抢票或管理多个抢票任务可以引入简单的任务队列如Redis RQ或调度器如APScheduler。2.3 技术栈选型背后的思考Selenium 4.x这是基础。选择它是因为其广泛的社区支持、成熟的API以及对现代Web标准的良好支持。相较于Playwright或CypressSelenium在中文社区的资源更丰富遇到问题更容易找到解决方案。Playwright 作为备选/互补在项目后期我评估了Playwright。它的确在速度、自动等待和录制工具上有优势。我的策略是核心稳定流程用Selenium因为已有大量稳定代码对于一些新的、对性能要求高的爬取或测试任务可以尝试用Playwright实现作为技术储备。两者可以共存通过一个统一的Driver抽象层来调用。验证码识别这是12306自动化最大的挑战。我采用了混合策略本地OCR如ddddocr、paddleocr处理简单的数字、字母验证码。速度快、零成本。第三方打码平台如超级鹰、图鉴处理复杂的滑动、点选验证码。作为降级方案当本地识别失败或置信度低时自动调用。需要将平台API进行封装做到可插拔替换。手动兜底在关键流程如提交订单前设置超时若自动识别失败则通过系统通知或日志高亮提示用户手动干预。消息通知集成钉钉/企业微信机器人。脚本状态变化时自动发送Markdown格式的消息到群聊实现“无人值守但状态可知”。3. 核心模块深度解析与避坑指南3.1 对抗反爬让Selenium“隐身”12306等现代网站都有完善的反爬机制来检测自动化工具。直接使用原生Selenium的WebDriver很容易被识别。以下是我实测有效的几种“隐身”技巧1. 修改CDP参数Chrome DevTools Protocol这是目前最有效的方法之一。Selenium 4提供了直接执行CDP命令的能力。from selenium import webdriver from selenium.webdriver import ChromeOptions options ChromeOptions() # 添加一些常用选项避免基本检测 options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions) # 关键执行CDP命令覆盖navigator.webdriver属性 driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, { get: () undefined }); // 可以继续覆盖其他属性如plugins, languages等 })这段代码会在每个新页面加载前注入JavaScript将navigator.webdriver属性设置为undefined从而绕过许多基于此属性的检测。2. 随机化浏览器指纹固定的User-Agent、屏幕分辨率、时区等都是指纹的一部分。需要随机化或模拟真实用户。import random user_agents [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ..., # ... 更多UA ] options.add_argument(fuser-agent{random.choice(user_agents)}) # 也可以随机化窗口大小 driver.set_window_size(random.randint(1200, 1920), random.randint(800, 1080))3. 模拟人类操作行为完全程序化的操作如瞬间移动鼠标、匀速点击容易被识别。需要加入随机延迟和人类化的移动轨迹。from selenium.webdriver.common.action_chains import ActionChains import time import random def human_like_click(element): 模拟人类点击先移动停顿再点击 action ActionChains(driver) # 将鼠标移动到元素附近的一个随机点 action.move_to_element_with_offset(element, random.randint(-5, 5), random.randint(-5, 5)) action.pause(random.uniform(0.1, 0.3)) # 随机停顿 action.click() action.perform() # 对于滑动验证码更需要模拟加速度移动轨迹而非匀速直线。 def human_like_drag_and_drop(source, target_offset_x): action ActionChains(driver) action.click_and_hold(source) total_time random.uniform(0.5, 2.0) # 总滑动时间随机 steps 30 step_time total_time / steps current_x 0 for i in range(steps): # 加速度模型开始慢中间快结束慢 progress i / steps # 使用贝塞尔曲线或正弦函数计算每步位移更接近真人 step_distance target_offset_x * (progress ** 0.5) # 简单示例 move_x step_distance - current_x action.move_by_offset(int(move_x), 0) action.pause(step_time) current_x step_distance action.release().perform()避坑指南反爬策略是不断升级的。没有一劳永逸的方案。上述方法需要组合使用并定期更新。一个重要的原则是不要过于频繁地访问目标网站设置合理的请求间隔这是最有效也是最道德的“隐身”方式。3.2 验证码识别混合策略与降级方案12306的验证码经历了多次升级从简单的数字字母到现在的滑动、点选图片。单一识别方法风险极高。1. 本地OCR识别针对传统验证码对于尚未升级为交互式验证码的环节如某些旧版登录入口可以使用本地OCR。import ddddocr # 一个识别率不错的开源OCR库 def recognize_captcha_local(image_path): ocr ddddocr.DdddOcr() with open(image_path, rb) as f: img_bytes f.read() captcha_text ocr.classification(img_bytes) return captcha_text注意事项本地OCR需要下载模型文件首次使用可能较慢。识别准确率并非100%需要设置置信度阈值低于阈值则触发降级方案。2. 第三方打码平台集成这是处理复杂验证码的可靠方案。以“超级鹰”为例你需要将其API封装成一个服务类。import requests from typing import Optional class ChaoJiYingClient: def __init__(self, username, password, soft_id): self.username username self.password password self.soft_id soft_id self.base_url http://upload.chaojiying.net/Upload/Processing.php def recognize(self, image_bytes, codetype: int) - Optional[str]: 识别验证码返回识别结果或None data { user: self.username, pass: self.password, softid: self.soft_id, codetype: codetype, # 验证码类型代码 } files {userfile: (captcha.jpg, image_bytes)} try: resp requests.post(self.base_url, datadata, filesfiles, timeout10) result resp.json() if result.get(err_no) 0: return result.get(pic_str) else: self.logger.error(f打码平台识别失败: {result}) return None except Exception as e: self.logger.error(f调用打码平台API异常: {e}) return None关键点codetype参数至关重要必须根据验证码类型如4位数字、5位英文、滑动缺口等选择正确的代码。这需要查阅打码平台的文档。3. 混合策略调度器在实际业务中我设计了一个CaptchaSolver类来统一调度。class CaptchaSolver: def __init__(self, local_ocr_enabledTrue, third_party_clientNone): self.local_ocr_enabled local_ocr_enabled self.third_party_client third_party_client self.local_confidence_threshold 0.8 # 本地识别置信度阈值 def solve(self, image_bytes, captcha_type) - str: result None # 策略1优先使用本地OCR如果启用且类型匹配 if self.local_ocr_enabled and captcha_type in [digit, letter]: result, confidence self._solve_local(image_bytes) if confidence self.local_confidence_threshold: return result # 置信度不足继续尝试策略2 # 策略2使用第三方打码平台 if self.third_party_client: result self.third_party_client.recognize(image_bytes, self._map_to_third_party_code(captcha_type)) if result: return result # 策略3所有自动方案失败抛出异常由上层业务逻辑决定是重试还是转为手动 raise CaptchaSolveFailedException(所有验证码识别方案均失败)这种设计保证了识别流程的弹性和高可用性。3.3 状态管理与异常恢复机制企业级项目必须能优雅地处理失败并从断点恢复。1. 上下文状态管理定义一个TicketContext或TaskContext类贯穿整个业务流程。它记录当前任务的所有状态登录会话、查询条件、选中的车次、乘客信息、当前执行步骤等。这个上下文对象可以被序列化如转换成JSON保存到文件或Redis中。2. 步骤重试与断路器模式对于网络请求、元素点击等可能失败的操作必须包装重试逻辑。我使用了tenacity库它提供了强大的重试装饰器。from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type from selenium.common.exceptions import NoSuchElementException, TimeoutException, StaleElementReferenceException retry( stopstop_after_attempt(3), # 最多重试3次 waitwait_exponential(multiplier1, min2, max10), # 指数退避等待 retryretry_if_exception_type((NoSuchElementException, TimeoutException, StaleElementReferenceException)), reraiseTrue # 重试次数用尽后抛出原始异常 ) def safe_find_element(driver, locator, timeout10): 查找元素内置重试机制 # 这里可以使用WebDriverWait但retry提供了更灵活的重试条件控制 from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC return WebDriverWait(driver, timeout).until(EC.presence_of_element_located(locator))对于验证码识别这种高失败率的操作可以引入**断路器Circuit Breaker**模式。连续失败多次后断路器“跳闸”短时间内直接拒绝调用该服务如第三方打码平台转而使用备用方案或直接失败防止雪崩。3. 持久化与断点续跑在关键步骤如登录成功、查询到车次、提交订单成功之后立即将TicketContext序列化并持久化。如果脚本因任何原因崩溃网络中断、程序错误重启后可以先加载最近的上下文判断上次执行到了哪一步然后从该步骤的后一步开始继续执行而不是从头开始。这极大地提高了抢票的成功率尤其是在秒杀场景下。4. 完整抢票流程的工程化实现4.1 环境准备与依赖管理使用poetry或pipenv管理项目依赖并明确区分开发环境和生产环境。requirements.txt或pyproject.toml文件必须清晰。# pyproject.toml 示例 [tool.poetry.dependencies] python ^3.8 selenium ^4.10.0 webdriver-manager ^3.8.6 # 自动管理浏览器驱动 requests ^2.31.0 ddddocr ^1.4.11 tenacity ^8.2.2 pydantic ^2.0.0 # 用于数据验证和设置管理 pyyaml ^6.0 # 读取配置文件 schedule ^1.2.0 # 定时任务如果需要 python-dotenv ^1.0.0 # 管理环境变量 [tool.poetry.group.dev.dependencies] pytest ^7.4.0 pytest-html ^3.2.0 selenium-wire ^5.1.0 # 用于抓包和调试使用webdriver-manager可以自动下载和匹配对应版本的ChromeDriver省去手动管理的麻烦。4.2 配置文件与参数设计采用YAML格式的配置文件结构清晰易读。# config.yaml user: username: your_12306_account password: your_password # 强烈建议从环境变量读取不直接写在配置文件里 journey: from_station: 上海 to_station: 北京 date: 2024-01-01 train_numbers: [G1, G3, G5] # 优先车次 seat_type: 二等座 passenger_names: [张三] strategy: retry_times: 3 query_interval_seconds: 5 # 查询余票频率不宜过短 enable_notification: true notification_webhook: https://oapi.dingtalk.com/robot/send?access_tokenxxx captcha: primary_solver: local # local / third_party third_party: name: chaojiying username: ${CJY_USERNAME} # 使用环境变量 password: ${CJY_PASSWORD} soft_id: 123456使用pydantic来验证配置数据的结构和类型确保配置正确无误。from pydantic import BaseModel, Field from typing import List, Optional class JourneyConfig(BaseModel): from_station: str to_station: str date: str # 可以进一步用datetime.date验证 train_numbers: List[str] Field(default_factorylist) seat_type: str 二等座 passenger_names: List[str] class AppConfig(BaseModel): user: UserConfig journey: JourneyConfig strategy: StrategyConfig captcha: CaptchaConfig # 加载和验证配置 import yaml with open(config.yaml, r) as f: config_data yaml.safe_load(f) config AppConfig(**config_data)4.3 核心业务流程代码框架以下是高度简化的主流程框架展示了各模块如何协同工作import logging from context import TicketContext from driver_pool import WebDriverPool from pages import LoginPage, QueryPage, OrderPage from captcha_solver import CaptchaSolver from notifier import DingTalkNotifier from config import load_config class TicketGrabber: def __init__(self, config): self.config config self.logger logging.getLogger(__name__) self.driver_pool WebDriverPool(size2) # 维护一个小型Driver池 self.context TicketContext() self.captcha_solver CaptchaSolver(...) self.notifier DingTalkNotifier(...) def run(self): self.notifier.send(抢票任务开始启动) try: driver self.driver_pool.acquire() self.context.driver driver # 1. 登录 if not self._login(driver): self.logger.error(登录失败任务终止) return self._save_context() # 登录成功保存状态 # 2. 循环查询并尝试下单 while not self.context.order_created: available_trains self._query_tickets(driver) if available_trains: selected_train self._select_train(available_trains) success self._submit_order(driver, selected_train) if success: self.context.order_created True self.logger.info(订单提交成功) self.notifier.send(f抢票成功车次{selected_train.number}) break # 跳出循环 # 未找到票或下单失败等待后继续查询 time.sleep(self.config.strategy.query_interval_seconds) # 3. 后续支付等流程可选 # self._handle_payment(driver) except Exception as e: self.logger.exception(抢票流程发生未预期异常) self.notifier.send(f抢票任务异常终止: {str(e)}) finally: if hasattr(self, context) and self.context.driver: self.driver_pool.release(self.context.driver) def _login(self, driver) - bool: 登录流程包含验证码处理 login_page LoginPage(driver) login_page.go_to() login_page.input_username(self.config.user.username) login_page.input_password(self.config.user.password) # 处理验证码 captcha_image login_page.get_captcha_image() captcha_text self.captcha_solver.solve(captcha_image, slide) # 假设是滑动验证码 login_page.input_captcha(captcha_text) login_page.click_login_button() return login_page.is_login_successful(timeout10) def _query_tickets(self, driver) - List[TrainInfo]: 查询余票返回符合条件的有票车次列表 query_page QueryPage(driver) query_page.search(self.context.journey) # 解析页面获取车次、余票、座位信息 trains query_page.parse_available_trains() # 根据策略过滤车次如只选高铁、优先时间等 filtered_trains self._apply_strategy(trains) return filtered_trains def _submit_order(self, driver, train) - bool: 选择车次、乘客、提交订单 order_page OrderPage(driver) order_page.select_train(train) order_page.select_passengers(self.context.passenger_names) # 可能再次出现验证码 if order_page.has_confirm_captcha(): captcha_img order_page.get_confirm_captcha_image() captcha_result self.captcha_solver.solve(captcha_img, click) # 点选验证码 order_page.input_confirm_captcha(captcha_result) return order_page.submit_order()这个框架将复杂的流程分解为一个个可测试、可维护的方法并且通过TicketContext贯穿始终通过DriverPool管理资源通过CaptchaSolver处理难点通过Notifier反馈状态具备了企业级应用的雏形。5. 部署、监控与持续优化5.1 部署方式从本地到服务器个人脚本在本地IDE里运行企业级项目则需要稳定、持久的运行环境。本地开发/调试使用IDE直接运行配合pdb或ipdb进行调试。利用selenium-wire拦截和检查网络请求对于分析登录和下单接口非常有帮助。服务器部署推荐环境选择一台稳定的Linux服务器如Ubuntu。无头浏览器在服务器上运行必须使用无头模式--headlessnew。Chrome的无头模式现在已经非常成熟几乎可以模拟所有有头浏览器的行为。进程管理使用systemd或supervisor来管理抢票脚本进程。这样可以实现开机自启、崩溃自动重启、日志轮转等功能。下面是一个简单的systemd服务单元文件示例# /etc/systemd/system/ticket-grabber.service [Unit] DescriptionTicket Grabber Service Afternetwork.target [Service] Typesimple Userubuntu WorkingDirectory/opt/ticket-grabber EnvironmentPATH/usr/local/bin:/usr/bin EnvironmentDISPLAY:99 # 如果需要虚拟显示可以安装Xvfb ExecStart/usr/local/bin/poetry run python main.py --config /opt/ticket-grabber/config.yaml Restarton-failure RestartSec10s StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target容器化进阶使用Docker封装整个环境包括Chrome、Chromedriver、Python依赖。这能保证环境一致性方便迁移和扩展。Dockerfile中需要安装Chrome浏览器和对应的驱动。5.2 日志、监控与告警日志使用Python标准库logging配置不同的Handler将INFO及以上日志输出到控制台和文件将ERROR日志单独发送到错误追踪服务如Sentry或通过通知器告警。import logging from logging.handlers import RotatingFileHandler def setup_logging(): logger logging.getLogger() logger.setLevel(logging.INFO) # 控制台Handler c_handler logging.StreamHandler() c_format logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - %(message)s) c_handler.setFormatter(c_format) logger.addHandler(c_handler) # 文件Handler按大小轮转 f_handler RotatingFileHandler(ticket_grabber.log, maxBytes10*1024*1024, backupCount5) f_format logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s) f_handler.setFormatter(f_format) logger.addHandler(f_handler)监控在代码关键节点埋点记录指标。例如使用time模块记录每个步骤的耗时使用计数器记录验证码识别成功/失败次数。这些数据可以推送到Prometheus然后用Grafana制作仪表盘实时查看脚本健康度、成功率、耗时趋势。告警除了通过通知器发送成功/失败消息外可以设置更智能的告警。例如如果连续10次查询都因“网络错误”失败可能意味着IP被限制需要触发高级别告警。或者验证码识别成功率在1小时内低于50%也需要通知维护人员检查识别服务。5.3 常见问题排查与性能调优问题1脚本运行一段时间后浏览器卡死或无响应。排查检查内存使用情况。Selenium的Driver和浏览器实例会占用较多内存尤其是长时间运行或有内存泄漏时。解决定期重启Driver。可以在成功下单后或每运行一定时间如2小时后主动调用driver.quit()然后从DriverPool获取新的实例。使用driver.execute_script(window.close();)关闭不必要的标签页。确保在finally块或异常处理中正确释放Driver资源将其归还给连接池或退出。问题2元素找不到NoSuchElementException或状态不对StaleElementReferenceException。排查这是UI自动化最常见的问题。通常是页面尚未加载完成、元素被动态加载、或iframe未切换。解决强制使用显式等待几乎所有的find_element操作都应该包裹在WebDriverWait中等待元素满足某个条件如可点击、可见。更稳定的定位器优先使用id其次是name、css_selector。避免使用绝对XPath。如果元素是动态生成的尝试用包含部分文本或属性的XPath或CSS选择器。处理iframe在操作iframe内的元素前务必使用driver.switch_to.frame(frame_reference)进行切换操作完后用driver.switch_to.default_content()切回。问题3被12306直接屏蔽或要求短信验证。排查行为模式被识别为机器人。操作太快、频率太高、Cookie异常都可能导致。解决降低频率增加查询和操作之间的随机延迟。模拟真人如前所述加入随机鼠标移动、滚动页面等操作。维护会话尝试保存成功的登录Cookie下次启动时复用避免频繁登录。可以使用pickle库序列化driver.get_cookies()然后通过driver.add_cookie(cookie)来恢复。IP代理如果服务器IP被封锁可以考虑使用高质量的住宅代理IP池进行轮换。但这会显著增加复杂度和成本。性能调优建议并行与并发如果需要监控多个车次或日期可以考虑使用多线程或异步IO如asyncioplaywright的异步API。但要注意12306对同一账号的并发请求很可能有严格限制盲目并发可能导致账号被风控。更安全的做法是单线程多任务队列有序处理。资源复用WebDriverPool和CaptchaSolver的实例都应设计为单例或共享资源避免重复创建的开销。缓存对相对静态的数据进行缓存如车站名称与代码的映射关系避免每次查询都去解析页面获取。把这个过程走完你收获的不仅仅是一个能稳定抢票的工具更是一套应对复杂Web自动化测试场景的工程化方法论。从混沌的脚本到清晰的项目从脆弱的执行到鲁棒的流程每一步的思考与重构都是你作为开发者或测试工程师能力的坚实沉淀。记住好的自动化项目代码只是载体其灵魂在于对业务深刻的理解、对异常周全的考量以及对用户体验不懈的追求。