Python Selenium自动化抢票脚本开发:从环境搭建到部署实践 1. 项目概述与核心思路又到了一年一度的出行高峰季抢票大战的硝烟味隔着屏幕都能闻到。作为一名常年和代码打交道的开发者看着身边的朋友们一遍遍刷新12306页面手指都快戳出火星子我就在想能不能用技术手段把我们从这种重复劳动中解放出来答案是肯定的。今天要聊的就是如何用Python和Selenium这两个老搭档打造一个能帮你远程自动监控、提交订单的抢票助手并且在抢票成功或失败时通过邮件第一时间通知你让你彻底告别手动刷票的焦虑。这个项目的核心简单来说就是模拟一个真实用户在12306网站上的操作流程登录、查询车次、选择乘客、提交订单。但难点在于12306的页面交互复杂验证码机制虽然现在简化了很多和动态加载元素对自动化脚本是个不小的挑战。Selenium作为一个浏览器自动化工具能完美地模拟人的点击、输入、下拉等操作是处理这类Web交互任务的首选。而Python则负责整个流程的逻辑控制和邮件通知的发送。整个脚本可以部署在你的个人电脑、云服务器甚至树莓派上实现7x24小时不间断的“蹲守”。注意本项目仅用于个人学习Python自动化技术和Web交互原理旨在提高编程技能。请务必遵守12306网站的用户协议和相关法律法规合理使用购票服务切勿用于干扰正常售票秩序或进行任何形式的恶意刷票。脚本运行频率应模拟正常人工操作间隔避免对服务器造成不必要的压力。2. 环境准备与核心工具选型工欲善其事必先利其器。在开始写代码之前我们需要把开发环境搭建好。这里我选择的是最通用、问题最少的组合确保你能跟着一步步走下来。2.1 Python环境与IDE配置首先你需要一个Python环境。我强烈建议使用Python 3.7及以上版本兼容性和库支持都更好。如果你还没安装可以去Python官网下载安装包记得安装时勾选“Add Python to PATH”这样在命令行里就能直接使用python和pip命令了。至于集成开发环境IDEVSCode和PyCharm都是极好的选择。VSCode轻量灵活通过安装Python插件就能获得很好的开发体验PyCharm则是专为Python设计的“重型武器”开箱即用调试功能强大。对于这个项目两者皆可我个人更习惯用VSCode因为启动快配置也简单。在VSCode里你只需要安装官方的“Python”扩展它就会自动帮你识别Python解释器、提供代码补全和调试功能。2.2 Selenium与WebDriver的安装这是项目的核心驱动。安装Selenium库非常简单只需要一条pip命令pip install selenium但光有Selenium库还不够它需要一个“桥梁”来实际控制浏览器这个桥梁就是WebDriver。不同的浏览器需要不同的WebDriver。考虑到兼容性和无头模式Headless即不显示浏览器界面的稳定性我推荐使用Chrome浏览器和对应的ChromeDriver。安装Chrome浏览器确保你电脑上安装了最新版本的Google Chrome。下载ChromeDriver访问ChromeDriver的官方下载站点下载与你的Chrome浏览器版本号匹配的驱动程序。查看Chrome版本的方法是在浏览器地址栏输入chrome://version/。下载后你会得到一个chromedriver.exeWindows或chromedriverMac/Linux文件。配置WebDriver路径有两种常用方法。一是将chromedriver文件放在系统环境变量PATH包含的目录下比如Windows的C:\Windows\二是在代码中指定它的绝对路径。为了灵活性我通常在代码里指定路径。2.3 邮件通知功能依赖库为了实现抢票结果通知我们需要用到Python内置的smtplib库来发送邮件以及email库来构造邮件内容。这两个都是标准库无需额外安装。但是你需要提前准备好一个邮箱的SMTP授权码。以QQ邮箱为例你需要登录网页版QQ邮箱在“设置”-“账户”中找到“开启SMTP服务”按照提示生成一个授权码。这个授权码相当于邮箱的临时密码用于在代码中登录SMTP服务器发送邮件。切记不要直接使用你的邮箱登录密码3. 核心代码结构与流程拆解整个脚本的骨架可以清晰地分为几个模块这样不仅代码结构清晰也便于后期维护和调试。我习惯将主要流程写在一个主函数里然后拆分成几个功能函数。3.1 脚本主体框架设计一个健壮的抢票脚本应该包含以下核心环节并且要考虑异常处理和状态判断import time from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def main(): # 1. 初始化浏览器驱动 driver init_browser() try: # 2. 登录12306 login_12306(driver) # 3. 查询车票 search_ticket(driver, ‘北京‘, ‘上海‘, ‘2024-10-01‘) # 4. 循环监控余票并尝试下单 ticket_found monitor_and_submit(driver) # 5. 根据结果发送邮件通知 send_email_notification(ticket_found) except Exception as e: print(f“程序运行出错 {e}“) send_email_notification(False, error_msgstr(e)) finally: # 6. 关闭浏览器释放资源 driver.quit() if __name__ “__main__“: main()这个框架把复杂的流程分解成了六个步骤每个步骤用一个函数实现。try...except...finally结构保证了即使程序中途出错也能发送失败通知并安全关闭浏览器避免资源泄露。3.2 登录模块的难点与破解12306的登录页面是第一个拦路虎。它可能有滑动验证码、也可能需要点击图中物品。完全自动破解图形验证码是一个复杂的AI问题超出了本项目的范畴。我们采取更务实的策略人工介入一次获取持久化的登录状态Cookie。思路是脚本打开登录页面然后暂停等待用户手动完成登录操作包括处理验证码。登录成功后脚本将浏览器所有的Cookies保存到本地文件。下次运行时直接加载这个Cookies文件就可以绕过登录页面直接进入已登录状态。import pickle import os COOKIE_FILE ‘12306_cookies.pkl‘ def login_12306(driver): driver.get(‘https://kyfw.12306.cn/otn/resources/login.html‘) # 尝试加载已保存的Cookies if os.path.exists(COOKIE_FILE): cookies pickle.load(open(COOKIE_FILE, “rb“)) for cookie in cookies: driver.add_cookie(cookie) driver.refresh() # 刷新页面使Cookies生效 time.sleep(2) # 检查是否登录成功例如查找‘我的12306‘链接 try: driver.find_element(By.LINK_TEXT, ‘我的12306‘) print(“Cookies登录成功“) return except: print(“Cookies已失效需重新登录。“) os.remove(COOKIE_FILE) # 删除无效Cookies文件 # 手动登录流程 print(“请在弹出的浏览器窗口中手动完成登录...“) input(“登录完成后请回到此窗口按回车键继续...“) # 保存本次登录的Cookies pickle.dump(driver.get_cookies(), open(COOKIE_FILE, “wb“)) print(“登录成功Cookies已保存。“)这个方法极大地提高了脚本的可用性。你只需要在第一次运行或者Cookies过期时手动登录一次即可。注意Cookies有有效期通常几天到几周失效后脚本会自动提示你重新登录。3.3 车票查询与监控逻辑登录成功后就进入车票查询页面。这里的核心是精确地定位页面元素并与之交互。12306的页面元素ID或Class可能会变动所以不能写死要用更稳定的方式定位。填写查询条件使用find_element方法找到“出发地”、“目的地”、“出发日”的输入框清空原有内容后填入你的信息。对于日期12306通常有一个日历控件直接对输入框执行send_keys(‘2024-10-01‘)有时可行更稳妥的方法是先点击日期输入框再通过Selenium操作日历组件选择日期。点击查询找到“查询”按钮并点击。解析车次列表查询结果是一个动态加载的表格。我们需要等待表格加载完成。使用WebDriverWait配合EC.presence_of_element_located是标准做法。然后使用find_elements注意是复数获取所有车次行tr。筛选目标车次遍历每一行车次数据提取车次号、出发时间、到达时间、座位类型如“二等座”、“一等座”和余票状态“有”、“无”或数字。这里需要仔细分析表格的HTML结构。def search_ticket(driver, from_station, to_station, train_date): driver.get(‘https://kyfw.12306.cn/otn/leftTicket/init‘) # 等待页面加载关键元素 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, “fromStationText“)) ) # 填写出发地需要先点击再输入然后选择下拉提示 from_input driver.find_element(By.ID, “fromStationText“) from_input.click() from_input.clear() from_input.send_keys(from_station) time.sleep(1) # 等待下拉列表出现 # 这里通常需要触发一个选择事件例如按下回车或点击下拉列表的第一项 from_input.send_keys(Keys.ENTER) # ... 类似地填写目的地、日期 # 点击查询 driver.find_element(By.ID, “query_ticket“).click() print(“已提交查询请求等待结果...“)监控逻辑则是在一个循环里定期比如每5秒或10秒执行一次“查询-解析-判断”的过程直到找到符合条件例如车次为G123且二等座有票的列车。3.4 订单提交与乘客选择一旦监控到有票脚本需要立即“点击”该车次的“预订”按钮。这里的关键是速度和准确性。点击后页面会跳转到订单确认页面。选择乘客在订单页面通常有一个乘客列表。脚本需要根据预设的乘客姓名勾选对应的复选框。这里同样需要稳定地定位这些复选框。处理潜在验证在提交订单前有时可能会弹出额外的确认对话框如“请选座”或提示无座需要脚本能识别并处理例如对于选座提示直接点击“暂不选座”。提交订单最后找到“提交订单”按钮并点击。提交成功后页面会跳转到支付页面。对我们这个脚本来说到这一步就已经是“抢票成功”了。支付环节由于涉及银行网关且需要短信验证等自动化复杂且风险高不建议自动化。脚本的任务是抢到订单并通知你支付由你手动完成。这个环节的代码需要非常小心因为页面跳转快元素加载可能存在延迟。大量使用WebDriverWait来等待特定元素出现是必不可少的。def submit_order(driver, target_train_no, seat_type): # 在车次列表中找到目标车次的行 # ... (解析逻辑) # 找到‘预订‘按钮并点击 book_btn train_row.find_element(By.CLASS_NAME, ‘btn72‘) book_btn.click() # 等待订单页面加载 WebDriverWait(driver, 10).until( EC.url_contains(‘/otn/confirmPassenger/initDc‘) ) # 选择乘客 passenger_list driver.find_elements(By.XPATH, “//input[type‘checkbox‘ and contains(name, ‘normalPassenger‘)]“) for passenger in passenger_list: # 通过相邻的文本节点判断乘客名这里简化处理 if ‘张三‘ in passenger.find_element(By.XPATH, “./following-sibling::label“).text: if not passenger.is_selected(): passenger.click() break # 提交订单 driver.find_element(By.ID, ‘submitOrder_id‘).click() # 处理可能出现的模态框如选座提示 try: WebDriverWait(driver, 3).until(EC.alert_is_present()) alert driver.switch_to.alert alert.accept() # 点击确认 except: pass # 没有弹出框继续 # 判断是否成功到达支付页面 if ‘payAction‘ in driver.current_url: return True else: return False3.5 邮件通知模块实现无论成功与否一个及时的通知都至关重要。我们使用QQ邮箱的SMTP服务作为例子。import smtplib from email.mime.text import MIMEText from email.header import Header def send_email_notification(success, error_msg““): # 邮件服务器配置 mail_host “smtp.qq.com“ # QQ邮箱SMTP服务器 mail_user “your_emailqq.com“ # 你的QQ邮箱 mail_pass “your_authorization_code“ # 你的SMTP授权码不是密码 sender mail_user receivers [‘receiverexample.com‘] # 接收邮箱可以是你自己 if success: subject ‘【抢票成功】请及时支付‘ content ‘恭喜12306抢票脚本已成功提交订单请立即登录12306APP或网站完成支付订单保留时间有限‘ else: subject ‘【抢票脚本运行异常】‘ content f‘抢票脚本运行过程中出现错误或已停止。错误信息{error_msg}。请检查脚本或手动操作。‘ message MIMEText(content, ‘plain‘, ‘utf-8‘) message[‘From‘] Header(“自动抢票机器人“, ‘utf-8‘) message[‘To‘] Header(“主人“, ‘utf-8‘) message[‘Subject‘] Header(subject, ‘utf-8‘) try: smtpObj smtplib.SMTP_SSL(mail_host, 465) # QQ邮箱使用SSL端口465 smtpObj.login(mail_user, mail_pass) smtpObj.sendmail(sender, receivers, message.as_string()) print(“邮件发送成功“) except smtplib.SMTPException as e: print(f“邮件发送失败 {e}“)将这个函数集成到主流程的try...except块和成功判断后就能实现全自动的通知闭环。4. 关键难点与避坑指南实录在实际开发和运行过程中你会遇到各种各样的问题。下面是我踩过坑后总结出的核心要点。4.1 元素定位失败与等待策略这是Selenium自动化中最常见的问题。页面还没加载完你的代码就去查找元素自然会失败。绝对不要用time.sleep硬等待这是新手最爱但效率最低且不可靠。网络慢一点sleep(5)可能不够网络快又白白浪费4秒。务必使用显式等待WebDriverWait配合expected_conditions是黄金法则。它会让WebDriver等待某个条件成立如元素出现、可点击最多等一段时间条件成立就立即继续否则超时抛异常。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 等待最多10秒直到ID为‘query_ticket‘的按钮可以点击 query_button WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, “query_ticket“)) ) query_button.click()定位器优先顺序优先使用ID因为它是唯一的。其次是Name、Class Name。对于复杂或动态元素XPath和CSS Selector是利器但要注意其稳定性。尽量使用相对路径和属性组合避免使用绝对路径和可能变化的索引。4.2 反爬机制与浏览器特征隐藏12306等大型网站都有反爬虫措施。直接使用Selenium打开的浏览器可能会被检测到并屏蔽。使用无头模式在服务器上运行时不需要图形界面。from selenium.webdriver.chrome.options import Options chrome_options Options() chrome_options.add_argument(‘--headless‘) # 开启无头模式 chrome_options.add_argument(‘--disable-gpu‘) # 禁用GPU某些系统需要 driver webdriver.Chrome(optionschrome_options)添加常见参数隐藏WebDriver特征chrome_options.add_argument(‘--no-sandbox‘) # 解决Linux下的权限问题 chrome_options.add_argument(‘--disable-dev-shm-usage‘) # 解决共享内存问题 chrome_options.add_argument(‘--disable-blink-featuresAutomationControlled‘) # 隐藏自动化控制特征 chrome_options.add_experimental_option(“excludeSwitches“, [“enable-automation“]) # 移除“正受到自动测试软件控制“提示 chrome_options.add_experimental_option(‘useAutomationExtension‘, False)修改navigator.webdriver属性这是最关键的指纹之一。driver.execute_cdp_cmd(‘Page.addScriptToEvaluateOnNewDocument‘, { ‘source‘: ‘ Object.defineProperty(navigator, ‘webdriver‘, { get: () undefined }) ‘ })这些措施能显著降低被识别为自动脚本的概率但无法保证100%。更高级的反爬可能需要轮换User-Agent、使用代理IP等这超出了基础脚本的范围。4.3 稳定性优化与异常处理抢票脚本需要长时间稳定运行。网络异常处理任何网络请求都可能失败。对driver.get()、click()等操作进行try...except包装捕获TimeoutException、WebDriverException等并进行重试或记录日志。状态检查与恢复脚本运行一段时间后可能会因为页面跳转异常、登录态失效等原因“迷路”。可以在主循环中加入状态检查点比如定期检查当前页面URL是否在预期的范围内或者检查“我的12306”链接是否存在。如果状态异常可以尝试刷新页面或重新初始化流程。日志记录不要只依赖print。使用Python的logging模块将运行信息如查询时间、余票状态、操作步骤、错误信息记录到文件便于事后排查问题。资源释放确保在脚本结束无论正常还是异常时调用driver.quit()来关闭浏览器和WebDriver进程避免在后台留下“僵尸”进程。5. 脚本部署与长期运行方案写好的脚本在本地电脑上跑没问题但总不能一直开着电脑。以下是几种部署方案个人电脑后台运行最简单用命令行运行python ticket_robot.py保持电脑不休眠即可。缺点是耗电、有噪音。旧手机/树莓派如果你有闲置的安卓旧手机可以安装Termux环境来运行Python脚本。树莓派更是天然的微型服务器功耗极低7x24小时运行毫无压力。这两种方式都需要一定的Linux环境配置能力。云服务器推荐这是最稳定、最省心的方案。购买一台最基础的云服务器如腾讯云、阿里云的轻量应用服务器通常新用户成本很低。在服务器上配置好Python环境、Chrome浏览器和ChromeDriver注意服务器是Linux系统需下载Linux版的Chrome和ChromeDriver然后将脚本上传。使用nohup或systemd服务让脚本在后台持续运行。云服务器的优势是网络稳定、永不关机你可以随时随地通过SSH查看运行状态。提示在服务器无头模式下运行可能会遇到Chrome启动失败的问题。通常是因为缺少一些图形库依赖。在Ubuntu/Debian系统上可以尝试安装sudo apt-get install -y wget chromium-chromedriver xvfb libxi6 libgconf-2-4。使用xvfb虚拟显示缓冲区也是一种解决方案可以模拟一个显示环境。6. 法律、道德与使用边界最后也是最重要的一点我们必须严肃地讨论使用边界。技术是一把双刃剑。遵守用户协议12306的用户协议中明确禁止使用任何软件、插件等方式干扰其正常的售票服务。本脚本的初衷是个人学习与技术实践其查询频率应严格模拟人工操作如间隔5-10秒绝对禁止使用极高频率的请求对服务器造成攻击性负载。公平性原则我们利用技术是为了方便自己而不是剥夺他人的购票机会。请勿滥用脚本大量抢票囤票或用于任何商业牟利行为。技术学习为主这个项目的价值在于让你深入理解Web自动化、HTTP请求、会话管理Cookie、反爬与反反爬等知识。这些技能在软件测试、数据采集合规范围内、RPA流程自动化等领域有广泛的应用。风险自担任何自动化操作12306账户的行为都存在账号被暂时锁定或限制购票的风险。请知晓并谨慎评估。我个人在编写和运行这类脚本时始终抱着学习和研究的态度。我会将查询间隔设置得足够长并且只在真正需要为自己购票时启用它。技术应该让生活更美好而不是制造新的不公。希望你在学习这项技能的同时也能牢记这一点。