1. 项目概述:为什么选择Python+Selenium来“智取”12306?

又到了一年一度的出行高峰季,抢票大战准时上演。守在电脑前不断刷新,眼睁睁看着票从“有”变“无”,这种体验相信很多人都经历过。作为一名技术从业者,我一直在想,能不能用技术手段把我们从这种重复、枯燥且充满不确定性的体力劳动中解放出来?答案是肯定的,而且实现它的核心工具,就是Python和Selenium。

这个项目,本质上是一个 基于浏览器自动化的模拟操作脚本 。它不涉及破解、攻击或任何违规行为,其原理是模拟一个真实用户的所有操作流程:打开浏览器、登录12306、查询车次、选择乘客、提交订单。Selenium就像一个不知疲倦的“机器人手”,可以精确地执行我们预设的点击、输入、跳转等指令。而Python作为“大脑”,负责逻辑控制、数据处理和异常处理。两者的结合,让我们能够以远超人工的速度和精度,完成抢票流程。

为什么是Selenium,而不是直接调用12306的API?这里有几个核心考量。首先,直接调用官方未公开的API存在法律和技术风险,且接口变动频繁,维护成本极高。其次,12306的登录和下单流程中包含了复杂的动态验证(如滑块验证、点选验证),这些验证机制正是为了防范机器请求。Selenium直接操作浏览器,可以完整地加载和执行这些前端验证逻辑,本质上是在“合规”的框架内进行自动化。最后,浏览器环境能更好地模拟人类行为,降低被风控系统识别为机器的风险。

这个脚本适合谁?如果你是有一定Python基础,对Web自动化感兴趣,并且受够了手动抢票的折磨,那么这个项目将是一个绝佳的实战练手机会。它不仅涵盖了Selenium的核心操作,还涉及网络请求处理、多线程/协程控制、邮件服务集成等实用技能。整个过程,就像在编写一个能帮你干活的数字助手,成就感十足。

2. 核心思路与架构设计:从人工到自动化的思维转变

手动抢票的核心动作可以抽象为几个关键节点:登录、查询、监控、下单。自动化脚本的设计,就是将这些节点串联起来,并用代码逻辑驱动。但直接照搬人工操作是行不通的,我们必须进行“机器友好”的改造。

2.1 整体流程设计

一个健壮的抢票脚本,其核心流程应该是一个包含状态判断和异常处理的循环。我设计的核心流程如下:

  1. 环境初始化与登录 :启动浏览器,加载12306登录页,处理登录流程(包括账号密码输入和可能的图形验证码)。
  2. 票务查询与监控 :根据用户配置的日期、车次、坐席等信息,循环查询余票。这里的关键是查询频率的控制,过快容易被封,过慢则可能错过票。
  3. 有票判定与乘客选择 :当查询到符合条件的余票时,脚本需要立刻锁定,并自动跳转到订单提交页面,选择预设的乘客。
  4. 提交订单与二次确认 :提交订单后,通常会有一个倒计时的确认页面。脚本需要自动完成确认。
  5. 结果通知与状态同步 :无论成功与否,都需要通过一个可靠的渠道(如邮箱)将结果通知用户。对于失败情况(如网络异常、验证失败),脚本应能自动重试或记录日志。

这个流程看似线性,但每个环节都可能“卡壳”。比如登录时的验证码识别、查询时的网络波动、提交订单时的排队拥挤。因此,我们的脚本必须在每个环节都内置 重试机制 超时处理

2.2 技术选型与工具准备

  • Python 3.7+ : 语言本体,选择较新的版本以获得更好的异步支持。
  • Selenium 4.x : 核心自动化库。请注意,Selenium 4在API上与3.x有部分不兼容,建议直接使用4.x版本。
  • WebDriver : 这是Selenium控制浏览器的桥梁。我们必须根据使用的浏览器下载对应的驱动。
    • Chrome/Edge : 下载 chromedriver ,版本号必须与本地安装的Chrome浏览器 大版本号 完全一致。
    • Firefox : 下载 geckodriver
    • 将下载的驱动文件所在目录添加到系统环境变量PATH中,或者直接在代码里指定驱动路径。
  • 第三方Python库
    • selenium : 主库。
    • requests : 用于辅助的网络请求,例如获取车站编码、调用邮件API。
    • schedule apscheduler : 用于实现定时循环查询(可选,也可以用简单循环加 time.sleep )。
    • smtplib email : Python内置库,用于发送邮件通知。
  • 浏览器建议 :推荐使用Chrome或Edge。它们的WebDriver支持最完善,且无头模式(Headless)运行稳定。Firefox在某些情况下对动态页面的处理略有不同。

注意:驱动版本匹配是新手最容易踩的坑。 你可以在浏览器的“关于”页面查看版本号,然后去对应的驱动官网下载相同大版本号(如Chrome 120.xxx)的驱动。不匹配会导致脚本无法启动浏览器。

3. 核心环节拆解与实战编码

接下来,我们进入具体的代码实现环节。我会分模块讲解关键代码,并附上详细的注释和避坑指南。

3.1 环境搭建与驱动配置

首先,确保你的Python环境已就绪。使用pip安装Selenium:

pip install selenium

然后,编写一个基础的浏览器启动类。这个类将封装浏览器的初始化、元素查找、等待等常用操作,提高代码复用性。

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
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import time

class TicketGrabber:
    def __init__(self, driver_path=None, headless=False):
        """
        初始化抢票器
        :param driver_path: WebDriver路径,如果已加入PATH则为None
        :param headless: 是否使用无头模式(不显示浏览器界面)
        """
        options = webdriver.ChromeOptions()
        if headless:
            options.add_argument('--headless') # 无头模式,后台运行
        options.add_argument('--disable-blink-features=AutomationControlled') # 禁用自动化控制特征
        options.add_experimental_option('excludeSwitches', ['enable-automation']) # 关闭开发者模式提示
        options.add_experimental_option('useAutomationExtension', False)

        # 初始化浏览器驱动
        if driver_path:
            self.driver = webdriver.Chrome(executable_path=driver_path, options=options)
        else:
            self.driver = webdriver.Chrome(options=options)

        # 执行CDP命令,进一步隐藏自动化特征
        self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
            'source': '''
                Object.defineProperty(navigator, 'webdriver', {
                    get: () => undefined
                })
            '''
        })
        self.wait = WebDriverWait(self.driver, 10) # 全局显式等待,超时10秒

    def find_element(self, by, value, timeout=10):
        """查找单个元素,支持显式等待"""
        try:
            return WebDriverWait(self.driver, timeout).until(
                EC.presence_of_element_located((by, value))
            )
        except TimeoutException:
            print(f"元素定位超时: {by}={value}")
            return None

    def quit(self):
        """关闭浏览器"""
        self.driver.quit()

关键点解析:

  1. 无头模式 (Headless) : --headless 参数让浏览器在后台运行,不显示GUI界面,节省资源且适合服务器部署。但在调试阶段,建议关闭此选项,以便观察脚本执行过程。
  2. 反检测策略 : 12306等现代网站会检测浏览器是否被自动化工具控制。我们通过 --disable-blink-features=AutomationControlled excludeSwitches 选项来隐藏部分特征。最核心的是通过 execute_cdp_cmd navigator.webdriver 属性设置为 undefined ,这是目前绕过检测最有效的方法之一。
  3. 显式等待 (WebDriverWait) : 这是Selenium最佳实践之一。网络有延迟,页面加载需要时间,使用 time.sleep(固定秒数) 是低效且不可靠的。显式等待会持续检查某个条件(如元素出现、元素可点击)是否成立,成立则立即继续,最多等待指定时间。这大大提高了脚本的稳定性和执行速度。

3.2 攻克第一关:自动化登录与验证码处理

登录12306最大的挑战是验证码。目前12306主要采用 滑动验证码 点选验证码

策略:半自动化处理。 完全自动识别验证码(使用OCR或深度学习)对于个人项目来说成本高、稳定性差,且可能违反网站规则。更务实的方法是 脚本负责加载出验证码图片,并暂停,等待用户手动完成验证 。验证通过后,脚本再继续执行后续操作。

def login(self, username, password):
    """登录12306"""
    self.driver.get("https://kyfw.12306.cn/otn/resources/login.html")
    time.sleep(2) # 等待页面基本加载,此处可用更智能的等待

    # 1. 点击“账号登录”标签(如果默认不是)
    account_login_tab = self.find_element(By.CLASS_NAME, "login-hd-account")
    if account_login_tab:
        account_login_tab.click()

    # 2. 输入用户名和密码
    user_input = self.find_element(By.ID, "J-userName")
    pass_input = self.find_element(By.ID, "J-password")
    if user_input and pass_input:
        user_input.clear()
        user_input.send_keys(username)
        pass_input.clear()
        pass_input.send_keys(password)
        print("用户名密码已输入,等待手动完成验证码...")
    else:
        print("未找到用户名或密码输入框")
        return False

    # 3. 关键步骤:等待用户手动完成验证码
    # 脚本在此处暂停,给出提示。用户需手动在浏览器中完成滑块或点选验证。
    input("请在浏览器中完成登录验证码操作,完成后按回车键继续...")

    # 4. 验证是否登录成功
    # 登录成功后,页面通常会跳转,或者出现用户昵称元素
    try:
        # 尝试查找代表登录成功的元素,例如“我的12306”链接或用户昵称
        WebDriverWait(self.driver, 15).until(
            EC.presence_of_element_located((By.LINK_TEXT, "我的12306"))
        )
        print("登录成功!")
        # 获取并保存登录必需的cookies,供后续查询使用(可选,但更稳定)
        self.cookies = self.driver.get_cookies()
        return True
    except TimeoutException:
        print("登录可能失败,未检测到成功跳转。")
        # 可以在这里截图,方便调试
        self.driver.save_screenshot("login_failed.png")
        return False

邮箱交互技巧初现: 想象一下,你把这个脚本部署在云服务器上,它如何通知你“该去手动验证了”?这就是我们引入邮箱交互的原因。我们可以在脚本运行到等待验证这一步时, 自动发送一封邮件到你的邮箱 ,邮件内容包含一个提醒,甚至可以直接包含一个远程桌面查看的提示。这样,即使你不在电脑前,也能通过手机邮件获知进度。

import smtplib
from email.mime.text import MIMEText
from email.header import Header

def send_email_notification(subject, content, to_addr):
    """发送邮件通知的简单函数"""
    # 配置发件人邮箱信息(以QQ邮箱为例)
    mail_host = "smtp.qq.com"
    mail_user = "your_email@qq.com" # 发件人邮箱
    mail_pass = "your_authorization_code" # 注意:不是邮箱密码,是SMTP授权码
    sender = mail_user
    receivers = [to_addr]

    message = MIMEText(content, 'plain', 'utf-8')
    message['From'] = Header("抢票机器人", 'utf-8')
    message['To'] = Header("主人", 'utf-8')
    message['Subject'] = Header(subject, 'utf-8')

    try:
        smtp_obj = smtplib.SMTP_SSL(mail_host, 465) # QQ邮箱SSL端口
        smtp_obj.login(mail_user, mail_pass)
        smtp_obj.sendmail(sender, receivers, message.as_string())
        smtp_obj.quit()
        print("邮件发送成功")
    except smtplib.SMTPException as e:
        print(f"邮件发送失败: {e}")

# 在login函数中等待验证前调用
# send_email_notification("12306登录验证提醒", "脚本已到达登录验证步骤,请尽快在浏览器中完成验证!", "your_phone@qq.com")
# input("请在浏览器中完成登录验证码操作,完成后按回车键继续...")

实操心得:关于授权码 :几乎所有邮箱服务商都要求使用授权码而非密码来连接SMTP服务。以QQ邮箱为例,需要在“设置”->“账户”中开启POP3/SMTP服务,并生成一个专属授权码。这个授权码是连接脚本和邮箱的关键。

3.3 票务查询与监控逻辑

登录成功后,就进入了核心的查票循环。12306的查询接口是 https://kyfw.12306.cn/otn/leftTicket/query ,但我们不直接调用,而是通过Selenium操作查询页面,让浏览器去发起这个请求,这样更模拟真人行为。

def query_tickets(self, from_station, to_station, train_date):
    """
    查询指定日期、区间的车票
    :param from_station: 出发站名,如“北京”
    :param to_station: 到达站名,如“上海”
    :param train_date: 出发日期,格式“2024-01-01”
    :return: 查询结果列表
    """
    # 跳转到车票查询页面
    self.driver.get("https://kyfw.12306.cn/otn/leftTicket/init")
    time.sleep(3) # 等待页面加载

    # 1. 填充出发站、到达站(这里需要处理车站编码)
    # 12306页面输入车站名时会有下拉列表选择,我们可以用JS直接设置输入框的值并触发事件
    from_station_input = self.find_element(By.ID, "fromStationText")
    to_station_input = self.find_element(By.ID, "toStationText")
    if from_station_input and to_station_input:
        # 先点击输入框清空默认内容
        from_station_input.click()
        from_station_input.clear()
        # 通过JS设置值并触发input事件,模拟真实输入
        self.driver.execute_script(f'arguments[0].value = "{from_station}"; arguments[0].dispatchEvent(new Event("input"))', from_station_input)
        time.sleep(0.5) # 等待下拉列表出现并自动选择(如果车站名唯一)

        to_station_input.click()
        to_station_input.clear()
        self.driver.execute_script(f'arguments[0].value = "{to_station}"; arguments[0].dispatchEvent(new Event("input"))', to_station_input)
        time.sleep(0.5)

    # 2. 填充出发日期
    date_input = self.find_element(By.ID, "train_date")
    if date_input:
        # 同样使用JS直接设置,因为日期输入框可能有只读属性
        self.driver.execute_script(f'arguments[0].value = "{train_date}";', date_input)

    # 3. 点击“查询”按钮
    query_btn = self.find_element(By.ID, "query_ticket")
    if query_btn:
        query_btn.click()
        print(f"已发起查询:{from_station} -> {to_station}, 日期:{train_date}")

    # 4. 等待查询结果表格加载
    try:
        WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located((By.ID, "queryLeftTable"))
        )
        print("查询结果加载完成。")
    except TimeoutException:
        print("查询结果加载超时。")
        return []

    # 5. 解析查询结果
    # 车次信息在 id 为 queryLeftTable 的 tbody 下的 tr 中,class 以 ‘bgc’ 开头或没有class
    ticket_list = []
    try:
        rows = self.driver.find_elements(By.CSS_SELECTOR, "#queryLeftTable tr[class^=bgc], #queryLeftTable tr:not([class])")
        for row in rows:
            try:
                # 提取车次号
                train_number = row.find_element(By.CLASS_NAME, "number").text.strip()
                # 提取出发/到达时间
                start_time = row.find_element(By.CLASS_NAME, "start-t").text.strip()
                # 提取历时
                duration = row.find_element(By.CLASS_NAME, "ls").text.strip()
                # 提取座位信息(这里以二等座为例,class是‘ze’)
                second_class_seat = row.find_element(By.CSS_SELECTOR, ".ze .ticket-num").text.strip()
                # “有”或数字表示有票,“无”或“--”表示无票
                has_ticket = second_class_seat not in ['无', '--', '']

                ticket_info = {
                    '车次': train_number,
                    '出发时间': start_time,
                    '历时': duration,
                    '二等座': second_class_seat,
                    '有票': has_ticket
                }
                ticket_list.append(ticket_info)
                # 打印有票的车次
                if has_ticket:
                    print(f"发现余票!车次:{train_number}, 时间:{start_time}, 二等座:{second_class_seat}")
            except NoSuchElementException:
                # 某一行可能缺少某些元素,跳过
                continue
    except Exception as e:
        print(f"解析车次信息时出错:{e}")

    return ticket_list

监控循环的设计: 有了单次查询函数,我们需要一个循环来持续监控。这个循环需要控制频率,并能在查到票时立刻中断循环,进入下单流程。

def monitor_tickets(self, from_station, to_station, train_date, target_trains=None, interval=5):
    """
    监控指定车次余票
    :param target_trains: 目标车次列表,如['G1', 'G2']。为None则监控所有有票车次。
    :param interval: 查询间隔(秒),不宜过短,建议5-10秒。
    """
    print(f"开始监控 {from_station} -> {to_station},日期:{train_date}")
    if target_trains:
        print(f"目标车次:{target_trains}")

    while True:
        try:
            tickets = self.query_tickets(from_station, to_station, train_date)
            for ticket in tickets:
                if ticket['有票']:
                    train_num = ticket['车次']
                    # 如果未指定目标车次,或者当前车次在目标列表中
                    if target_trains is None or train_num in target_trains:
                        print(f"*** 符合条件!车次 {train_num} 有余票,正在尝试下单... ***")
                        # 这里应该跳转到下单页面,我们假设有一个book_ticket函数
                        if self.book_ticket(train_num, from_station, to_station, train_date):
                            # 下单成功,发送通知并退出监控
                            send_email_notification("抢票成功!", f"恭喜!已成功抢到 {train_num} 次列车车票,请及时支付。", "your_email@example.com")
                            return True
                        else:
                            print(f"车次 {train_num} 下单失败,继续监控...")
            # 本次查询未找到符合条件的票,等待后继续
            print(f"本轮查询未找到符合条件的余票,{interval}秒后重新查询...")
            time.sleep(interval)
        except Exception as e:
            print(f"监控过程中发生异常:{e},稍后重试...")
            time.sleep(interval)

3.4 自动下单与订单确认

这是最紧张的一步。一旦监控到有票,脚本需要以最快速度完成座位选择和订单提交。

def book_ticket(self, train_number, from_station, to_station, train_date):
    """尝试预订指定车次的车票"""
    # 1. 在查询结果页,点击对应车次的“预订”按钮
    # 预订按钮的selector比较复杂,通常可以通过车次号来定位
    try:
        # 构建预订按钮的XPath:找到包含特定车次文本的td,然后找到其同级td下的预订链接
        book_btn_xpath = f"//tr[contains(@id, 'ticket_{train_number}')]//a[contains(@class, 'btn72')]"
        book_button = self.find_element(By.XPATH, book_btn_xpath, timeout=5)
        if book_button and book_button.is_enabled():
            book_button.click()
            print(f"已点击车次 {train_number} 的预订按钮。")
        else:
            print(f"车次 {train_number} 的预订按钮不可用或未找到。")
            return False
    except Exception as e:
        print(f"定位预订按钮失败:{e}")
        return False

    # 2. 等待跳转到乘客选择页面,并自动选择乘客
    try:
        # 等待页面跳转,通常会出现“乘客信息”字样
        WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located((By.XPATH, "//div[contains(text(), '乘客信息')]"))
        )
        print("已进入乘客选择页面。")

        # 3. 选择乘客(假设要选择第一个常用乘客)
        # 乘客列表的复选框
        passenger_checkbox = self.find_element(By.XPATH, "//input[@type='checkbox' and @name='passenger']", timeout=5)
        if passenger_checkbox and not passenger_checkbox.is_selected():
            # 使用JS点击,避免元素被遮挡等问题
            self.driver.execute_script("arguments[0].click();", passenger_checkbox)
            print("已选择第一位乘客。")
        else:
            print("未找到乘客复选框或已选中。")

        # 4. 选择座位类型(如二等座)
        seat_type_dropdown = self.find_element(By.ID, "seatType_1") # 示例ID,实际需根据页面调整
        if seat_type_dropdown:
            from selenium.webdriver.support.select import Select
            seat_select = Select(seat_type_dropdown)
            seat_select.select_by_visible_text("二等座") # 选择文本为“二等座”的选项
            print("已选择座位类型:二等座。")

        # 5. 提交订单
        submit_order_btn = self.find_element(By.ID, "submitOrder_id")
        if submit_order_btn:
            submit_order_btn.click()
            print("已提交订单,等待系统确认...")

        # 6. 处理订单确认对话框(通常会出现一个模态框,需要点击“确认”)
        WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located((By.ID, "qr_submit_id"))
        )
        confirm_btn = self.find_element(By.ID, "qr_submit_id")
        if confirm_btn and confirm_btn.is_displayed():
            confirm_btn.click()
            print("已确认订单!")
            # 此时订单已生成,进入支付页面。脚本可以在此暂停,或发送邮件通知用户手动支付。
            send_email_notification("订单已生成,等待支付", f"车次 {train_number} 订单已成功生成,请尽快登录12306完成支付!", "your_email@example.com")
            return True
        else:
            print("未找到订单确认按钮。")
            return False

    except TimeoutException as e:
        print(f"下单流程超时:{e}")
        self.driver.save_screenshot("order_timeout.png")
        return False
    except Exception as e:
        print(f"下单过程中发生未知错误:{e}")
        self.driver.save_screenshot("order_error.png")
        return False

4. 进阶技巧与避坑指南

将上述模块组合起来,一个基础的自动化抢票脚本就成型了。但在实际运行中,你会遇到各种各样的问题。下面分享一些进阶技巧和常见坑点。

4.1 邮箱交互的深度应用

前面我们提到了用邮件通知“需要手动验证”。其实,邮箱可以扮演更重要的角色: 远程控制中枢

  1. 状态心跳通知 :让脚本每隔一段时间(如每30分钟)发送一封“我还活着”的状态邮件,报告监控的车次、查询次数等信息。如果收不到心跳邮件,说明脚本可能已经崩溃。
  2. 指令接收 (高级):你可以搭建一个简单的邮件接收服务(如使用 imaplib 库定期检查收件箱),通过发送特定主题或内容的邮件到指定邮箱,来远程控制脚本,例如“暂停监控”、“更换监控车次为G100”、“立即停止”等。
  3. 结果详情报送 :抢票成功或失败后,不仅发送简单通知,还可以将详细的订单信息、失败原因(截图附件)一并发送,方便复盘。
# 示例:发送带简单HTML格式和附件的通知邮件(需要引入更多email模块组件)
def send_email_with_attachment(subject, content, attachment_path=None):
    from email.mime.multipart import MIMEMultipart
    from email.mime.base import MIMEBase
    from email import encoders
    import os

    msg = MIMEMultipart()
    msg['From'] = sender
    msg['To'] = receiver
    msg['Subject'] = subject

    msg.attach(MIMEText(content, 'html', 'utf-8')) # 使用HTML内容

    if attachment_path and os.path.exists(attachment_path):
        part = MIMEBase('application', 'octet-stream')
        with open(attachment_path, 'rb') as file:
            part.set_payload(file.read())
        encoders.encode_base64(part)
        part.add_header('Content-Disposition', f'attachment; filename="{os.path.basename(attachment_path)}"')
        msg.attach(part)

    # ... 发送逻辑同上

4.2 稳定性与反反爬策略

12306的反爬机制在不断升级。除了之前提到的隐藏 webdriver 属性,还需要注意:

  • 随机化等待时间 :在操作之间使用固定的 time.sleep(2) 是明显的机器特征。应该使用随机延迟,例如 time.sleep(random.uniform(1, 3))
  • 模拟人类操作轨迹 :Selenium的 ActionChains 可以模拟更真实的鼠标移动轨迹,而不是直接从A点跳到B点。
  • Cookies管理 :登录成功后,妥善保存 driver.get_cookies() ,并在下次启动时使用 driver.add_cookie(cookie) 恢复登录状态,可以避免频繁登录触发验证。
  • User-Agent轮换 :虽然Selenium会使用真实浏览器UA,但在某些场景下可以准备多个UA进行轮换(通过 options.add_argument(f'user-agent={ua}') )。
  • 使用更隐蔽的浏览器模式 :可以考虑使用 undetected-chromedriver 这类第三方库,它专门为绕过自动化检测而设计。

4.3 常见问题排查实录

  1. 报错: Message: ‘chromedriver‘ executable needs to be in PATH

    • 原因 :系统找不到ChromeDriver。
    • 解决 :确保已下载正确版本的ChromeDriver,并将其所在目录添加到系统环境变量PATH中,或者在代码中指定绝对路径: webdriver.Chrome(executable_path=‘/path/to/chromedriver‘)
  2. 元素找不到(NoSuchElementException)

    • 原因1 :页面还没加载完。 解决 :务必使用显式等待 WebDriverWait 代替硬性等待 time.sleep
    • 原因2 :页面结构变了,或者元素在iframe里。 解决 :检查页面HTML结构是否更新;如果元素在iframe内,需要使用 driver.switch_to.frame(frame_element) 切换到对应iframe后才能定位。
    • 原因3 :元素被遮挡或不可见。 解决 :尝试使用JavaScript直接点击: driver.execute_script(“arguments[0].click();“, element)
  3. 脚本被识别,出现验证码或直接失败

    • 原因 :浏览器指纹或行为被检测。
    • 解决 :启用前面提到的所有反检测选项( disable-blink-features , excludeSwitches , CDP命令)。考虑使用无头模式时添加更多参数模拟真人,如 --window-size=1920,1080 。最根本的方法是接受“半自动化”,在关键验证步骤让人工介入。
  4. 查询或下单速度慢,错过票

    • 原因 :网络延迟、代码逻辑不够优化、等待时间设置过长。
    • 解决 :优化代码,减少不必要的等待;使用更快的网络环境(如本地运行优于远程服务器);可以考虑使用多线程或异步IO来同时监控多个日期或车次,但要注意同一个账号频繁请求可能被限制。
  5. 登录成功后,查询页面提示“未登录”

    • 原因 :Cookie丢失或域名切换导致会话失效。12306查询页面和登录页面域名可能略有不同。
    • 解决 :登录成功后不要关闭浏览器,直接用同一个 driver 实例跳转到查询页。如果必须重启脚本,尝试保存和加载Cookies。

这个项目将Python的灵活性与Selenium的自动化能力结合,为你构建了一个私人订制的抢票助手。它不仅仅是一个工具,更是一个深入了解Web自动化、反爬策略和任务编排的绝佳实践。从环境搭建到反检测绕过,从邮件交互到异常处理,每一个环节都充满了值得琢磨的技术细节。记住,技术的目的是提高效率、解决重复劳动,但务必在合规合理的前提下使用。希望这篇详细的指南能帮你顺利搞定回家的车票,更重要的是,在动手实践的过程中,你的编程和问题解决能力一定会再上一个台阶。如果在实际操作中遇到新的问题,不妨多看看浏览器的开发者工具(F12),观察网络请求和元素变化,那才是解决问题的第一现场。

更多推荐