1. 项目概述与核心价值

最近在技术社区里,看到不少朋友在讨论用Python做自动化操作,尤其是针对一些需要登录的网站。其中,12306这个“国民级”购票网站,因其复杂的验证码和登录流程,常常被当作一个颇具挑战性的实战目标。今天,我就来分享一个基于Selenium和Edge浏览器的自动化登录12306的完整方案。这不是一个简单的脚本堆砌,而是融合了我在实际项目中处理动态页面、反爬策略和浏览器驱动管理的心得。整个过程,从环境搭建到成功登录,核心代码部分确实能在5分钟左右跑通,但更重要的是理解每一步背后的逻辑和可能遇到的“坑”。

这个实战项目适合谁呢?如果你是刚接触Selenium,想找一个有代表性的综合案例来练手;或者你正在为某个需要模拟登录的爬虫或自动化测试任务发愁,那么这篇内容会非常对路。我们将使用Python语言,搭配微软的Edge浏览器,因为它与Windows系统集成度更高,驱动获取相对稳定。整个流程会覆盖:环境准备(Python、Selenium库、Edge驱动)、页面元素定位策略、验证码处理思路(重点讨论)、登录逻辑实现,以及如何让脚本运行得更稳健。我会把完整的、可运行的代码附在最后,你可以直接复制到本地进行尝试和修改。

2. 环境准备与核心工具解析

工欲善其事,必先利其器。在开始写代码之前,我们需要把“战场”布置好。这里的选择背后都有具体的考量,我会一一解释为什么这么做。

2.1 Python与Selenium库安装

首先,确保你的电脑上安装了Python。我推荐使用Python 3.7及以上版本,因为其对新的语言特性和库的支持更好。安装过程很简单,从官网下载安装包,记得勾选“Add Python to PATH”选项,这样可以在命令行中直接使用 python pip 命令。

接下来是核心——Selenium库。Selenium本质上是一个用于Web应用程序测试的工具,但它强大的浏览器操控能力使其成为了自动化操作和爬虫的利器。它通过一套WebDriver协议与真实的浏览器进行通信,从而实现点击、输入、获取数据等操作。在命令行中,使用pip安装它:

pip install selenium

这里有个注意事项:网络环境可能会导致pip安装缓慢或失败。你可以考虑使用国内的镜像源来加速,例如清华源或阿里云源。命令如下:

pip install selenium -i https://pypi.tuna.tsinghua.edu.cn/simple

安装完成后,可以在Python交互环境中输入 import selenium 来验证是否成功。

2.2 Edge浏览器与WebDriver驱动匹配

我们选择Edge浏览器而非更常见的Chrome,主要出于两点考虑:一是Edge基于Chromium内核,兼容性和性能都很好,且是Windows系统原生浏览器,稳定性有保障;二是其WebDriver由微软官方维护,下载和匹配相对清晰,避免了Chrome浏览器与驱动版本严格对应带来的麻烦。

驱动的下载与放置是关键一步,也是新手最容易出错的地方。

  1. 查看你的Edge浏览器版本 :打开Edge浏览器,点击右上角三个点(…)->“帮助和反馈”->“关于Microsoft Edge”。记下版本号,例如“版本 121.0.2277.128”。

  2. 下载对应版本的MS Edge WebDriver :访问微软官方的Edge WebDriver下载页面。你需要下载与你的Edge浏览器 主版本号 完全一致的驱动。例如,Edge是121版,就下载 edgedriver_121 开头的文件。下载时选择与你的操作系统(Windows 64位)匹配的压缩包。

  3. 驱动的放置 :下载的是一个名为 msedgedriver.exe 的可执行文件。你有三种处理方式:

    • 方式一(推荐,便于管理) :将其放置在一个你专门存放工具的目录下,例如 D:\WebDrivers\ 。然后,将这个目录的路径( D:\WebDrivers )添加到系统的环境变量 Path 中。这样,Python的Selenium就能在任何位置找到它。
    • 方式二(项目专用) :直接放在你的Python项目文件夹根目录下。在代码中指定驱动路径时,使用相对路径 ./msedgedriver.exe 即可。
    • 方式三(临时指定) :在代码中通过 executable_path 参数指定绝对路径。

注意 :驱动与浏览器版本不匹配是导致 SessionNotCreatedException 错误的常见原因。如果遇到此类错误,首先检查版本号。另外,确保下载的驱动没有被Windows Defender或其他安全软件误删,必要时需添加信任。

2.3 辅助工具与思路准备

除了核心的Selenium,我们还需要一点额外的准备来应对12306的登录页面。

  • 开发者工具(F12) :这是我们的“眼睛”。我们将频繁使用它来查看页面HTML结构,定位用户名、密码输入框以及登录按钮的元素信息(如ID、Class Name、XPath等)。熟练使用开发者工具的“检查”功能,是做好Web自动化的基础。
  • 关于验证码的思考 :12306登录时的验证码是动态生成的,通常为点击图中特定物品的类型。完全自动化解码涉及图像识别,复杂度高且可能触及合规边界。在本实战中,我们的策略是: 脚本运行后,会暂停并等待用户手动识别并点击验证码,然后再继续执行后续的自动输入和登录 。这是一种“半自动化”的务实方案,既绕开了复杂的技术难题,也保证了操作的合法性。脚本会负责打开浏览器、跳转到登录页、填充账号密码,然后等待你手动完成验证码操作。

3. 核心代码实现与分步详解

环境就绪后,我们进入核心的代码编写环节。我会将完整脚本拆解成几个逻辑模块,并详细解释每一行代码的作用和背后的考量。

3.1 脚本初始化与浏览器启动

首先,我们导入必要的模块,并设置浏览器启动选项。

from selenium import webdriver
from selenium.webdriver.edge.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

# 1. 设置Edge浏览器选项
edge_options = webdriver.EdgeOptions()
# 避免一些自动化特征被检测,可添加如下参数(非万能,但有益)
edge_options.add_argument('--disable-blink-features=AutomationControlled')
edge_options.add_experimental_option('excludeSwitches', ['enable-automation'])
edge_options.add_experimental_option('useAutomationExtension', False)

# 2. 指定WebDriver路径(如果已加入PATH,则不需要此行)
# driver_path = r'D:\WebDrivers\msedgedriver.exe'
# service = Service(executable_path=driver_path)

# 3. 启动浏览器
# 如果驱动已在PATH中,使用以下方式:
driver = webdriver.Edge(options=edge_options)
# 如果指定了路径,使用以下方式:
# driver = webdriver.Edge(service=service, options=edge_options)

# 最大化窗口,确保元素可见
driver.maximize_window()

代码解析

  • webdriver :核心模块。
  • Service :用于管理WebDriver服务,如果你将驱动放在了自定义路径,需要通过它来指定。
  • By :提供定位元素的方法(如 By.ID , By.CLASS_NAME )。
  • WebDriverWait EC :用于实现“显式等待”,这是编写健壮Selenium脚本的关键,后面会详细讲。
  • edge_options :这里我们添加了几个参数来尝试降低被网站检测为自动化的风险。 --disable-blink-features=AutomationControlled 是较新的一个设置,有助于隐藏WebDriver特征。 excludeSwitches useAutomationExtension 是传统的规避检测方法。请注意,这些方法并非绝对有效,网站的反爬策略也在升级。
  • 启动浏览器时,优先推荐将驱动加入系统PATH,这样代码更简洁,也便于跨项目共享。

3.2 页面访问与元素定位策略

接下来,我们让浏览器打开12306的登录页面,并找到我们需要操作的元素。

# 4. 访问12306登录页面
login_url = 'https://kyfw.12306.cn/otn/resources/login.html'
driver.get(login_url)
print("已打开12306登录页面,请稍候...")
time.sleep(2) # 简单等待页面加载,后续会被更优的显式等待替代

# 5. 切换到“账号登录”选项卡(12306默认可能是扫码登录)
# 首先尝试定位并点击“账号登录”标签
try:
    account_login_tab = WebDriverWait(driver, 10).until(
        EC.element_to_be_clickable((By.LINK_TEXT, "账号登录"))
    )
    account_login_tab.click()
    print("已切换到账号登录模式。")
except Exception as e:
    print(f"未找到或无法点击‘账号登录’选项卡,可能页面已变更。错误:{e}")
    # 这里可以选择继续或退出,我们假设页面已经是账号登录形式
    pass

# 6. 定位用户名、密码输入框和登录按钮
# 使用显式等待,确保元素加载完成再操作,这是避免`NoSuchElementException`的最佳实践
wait = WebDriverWait(driver, 10) # 创建等待对象,最多等10秒

try:
    # 定位用户名输入框 - 通过ID定位,通常最稳定
    username_input = wait.until(
        EC.presence_of_element_located((By.ID, "J-userName"))
    )
    # 定位密码输入框
    password_input = driver.find_element(By.ID, "J-password")
    # 定位登录按钮
    login_button = driver.find_element(By.ID, "J-login")
    
    print("成功定位登录表单元素。")
    
except Exception as e:
    print(f"定位登录表单元素失败!请检查页面结构或网络。错误:{e}")
    driver.quit()
    exit()

核心技巧与避坑指南

  1. 显式等待 vs 隐式等待 vs time.sleep
    • time.sleep(2) 是“强制等待”,简单粗暴但效率低下,不推荐在正式脚本中大量使用。
    • 隐式等待 driver.implicitly_wait(10) 设置一次,对后续所有 find_element 操作生效,但不够灵活。
    • 显式等待 WebDriverWait 是最佳实践 。它针对某个特定条件(如元素可点击、元素出现)进行等待,条件满足立即继续,最多等待指定时间。这能最大程度保证脚本的稳定性和执行速度。我们这里用 EC.presence_of_element_located 等待用户名输入框出现。
  2. 元素定位器选择优先级
    • ID > Name > Class Name > CSS Selector > XPath 。ID通常是唯一且最稳定的。12306登录框的ID(如 J-userName )比较固定,因此我们首选ID定位。
    • 使用开发者工具检查时,要确认元素的ID是唯一的,并且不是动态生成的(即每次刷新页面ID不变)。
  3. 异常处理 :使用 try...except 包裹可能失败的操作(如元素定位),并给出友好的错误提示,而不是让脚本直接崩溃。这有助于调试。

3.3 填写凭证与处理验证码

这是整个流程中最需要人工干预的部分。

# 7. 填写用户名和密码(请替换为你的测试账号)
# 警告:切勿将真实账号密码直接硬编码在脚本中!此处仅为演示。
test_username = "your_username_here"
test_password = "your_password_here"

username_input.clear() # 清空可能存在的默认值
username_input.send_keys(test_username)
print(f"已输入用户名:{test_username}")

password_input.clear()
password_input.send_keys(test_password)
print("已输入密码。")

# 8. 验证码处理 - 手动干预点
print("-" * 50)
print("【重要提示】脚本已自动填充账号密码。")
print("请现在浏览器窗口中,手动完成验证码识别并点击。")
print("完成验证码操作后,请回到此控制台窗口,按回车键继续...")
print("-" * 50)

# 脚本暂停,等待用户手动操作验证码并按回车
input("等待手动验证码完成,按回车键继续执行自动登录...")

# 9. 点击登录按钮
try:
    # 再次等待登录按钮可点击,因为填写验证码后页面状态可能变化
    login_button = wait.until(
        EC.element_to_be_clickable((By.ID, "J-login"))
    )
    login_button.click()
    print("已点击登录按钮。")
except Exception as e:
    print(f"点击登录按钮失败!错误:{e}")

实操心得

  • 安全警告 :像上面这样把账号密码明文写在代码里是极不安全的,尤其是如果你打算共享或上传代码。在实际项目中,应该通过环境变量、配置文件(如 .ini , .yaml )或密钥管理服务来存储敏感信息。例如,可以使用 os.environ.get('USERNAME') 从系统环境变量读取。
  • 验证码策略 input() 函数会让Python脚本暂停,等待用户在控制台输入。这为我们创造了手动操作浏览器的时间窗口。这是一种简单有效的“人机协作”模式。完成验证码点击后,脚本才会继续执行登录。
  • 状态刷新 :在手动操作页面后(如点击验证码),页面的DOM状态可能发生微小变化。因此,在点击最终的登录按钮前,我们再次使用显式等待来确保按钮处于可点击状态,这比直接用之前定位到的旧元素更可靠。

3.4 登录后处理与脚本收尾

点击登录后,我们需要处理登录成功或失败的情况,并优雅地关闭浏览器。

# 10. 等待登录结果并处理
print("正在等待登录结果...")
time.sleep(3) # 等待页面跳转或提示出现

# 检查登录是否成功的简单方法:查看当前URL或页面是否存在特定元素
current_url = driver.current_url
print(f"当前页面URL: {current_url}")

# 示例:检查是否跳转到登录成功后的首页(my12306)
if "otn/view/index.html" in current_url:
    print("✅ 登录成功!已跳转到12306个人中心。")
    # 这里可以继续执行登录后的操作,例如查询余票
    # ...
elif "login/init" in current_url:
    print("❌ 登录失败,可能仍在登录页面。请检查验证码或账号密码是否正确。")
else:
    print(f"⚠️  登录状态未知。当前页面标题:{driver.title}")
    # 可以尝试捕捉页面上的错误提示信息
    try:
        error_msg = driver.find_element(By.CLASS_NAME, "login-error").text
        print(f"页面错误提示:{error_msg}")
    except:
        print("未找到明确的错误提示信息。")

# 11. 保持浏览器打开一段时间供查看,然后关闭
print("登录流程演示结束。浏览器将在10秒后自动关闭...")
time.sleep(10)
driver.quit()
print("浏览器已关闭,脚本执行完毕。")

状态判断逻辑 :判断自动化操作是否成功,不能只依赖是否抛异常。一个健壮的脚本应该检查“结果状态”。这里我们通过检查跳转后的URL是否包含成功页面的特征字符串来初步判断。更严谨的做法还可以结合检查页面标题、特定欢迎语元素等。对于失败情况,尝试抓取页面上显式的错误提示信息(如 class login-error 的div),能帮助我们快速定位问题。

4. 完整代码整合与执行

将上述所有步骤整合,就得到了我们完整的自动化登录脚本。你可以将下面的代码保存为一个 .py 文件(例如 login_12306.py ),并替换其中的用户名和密码占位符。

"""
Selenium自动化登录12306实战脚本
环境要求:Python 3.7+, Selenium 4+, Edge浏览器及对应WebDriver
"""
from selenium import webdriver
from selenium.webdriver.edge.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

def login_12306(username, password):
    """执行12306登录流程"""
    # 初始化浏览器选项
    edge_options = webdriver.EdgeOptions()
    edge_options.add_argument('--disable-blink-features=AutomationControlled')
    edge_options.add_experimental_option('excludeSwitches', ['enable-automation'])
    edge_options.add_experimental_option('useAutomationExtension', False)

    # 启动浏览器(确保msedgedriver.exe在PATH中)
    driver = webdriver.Edge(options=edge_options)
    driver.maximize_window()
    wait = WebDriverWait(driver, 10)

    try:
        # 1. 访问登录页
        driver.get('https://kyfw.12306.cn/otn/resources/login.html')
        print("页面加载中...")
        time.sleep(2)

        # 2. 切换到账号登录(容错处理)
        try:
            account_tab = wait.until(EC.element_to_be_clickable((By.LINK_TEXT, "账号登录")))
            account_tab.click()
            print("已切换至账号登录。")
        except:
            print("未找到账号登录选项卡,继续执行。")

        # 3. 定位表单元素
        user_input = wait.until(EC.presence_of_element_located((By.ID, "J-userName")))
        pass_input = driver.find_element(By.ID, "J-password")
        login_btn = driver.find_element(By.ID, "J-login")

        # 4. 填写账号密码
        user_input.clear()
        user_input.send_keys(username)
        pass_input.clear()
        pass_input.send_keys(password)
        print("账号密码已自动填充。")

        # 5. 等待用户手动验证码
        print("\n" + "="*60)
        print("请手动在浏览器中完成验证码操作!")
        print("完成后,请回到此窗口按回车键继续...")
        print("="*60 + "\n")
        input("按回车键继续...")

        # 6. 点击登录
        login_btn = wait.until(EC.element_to_be_clickable((By.ID, "J-login")))
        login_btn.click()
        print("已提交登录。")

        # 7. 验证登录结果
        time.sleep(3)
        if "otn/view/index.html" in driver.current_url:
            print("✅ 登录成功!")
            return True, driver
        else:
            print("❌ 登录失败,请检查原因。")
            # 尝试捕获错误信息
            try:
                error = driver.find_element(By.CLASS_NAME, "login-error").text
                print(f"错误信息: {error}")
            except:
                pass
            return False, driver

    except Exception as e:
        print(f"脚本执行过程中发生异常: {e}")
        return False, driver

if __name__ == "__main__":
    # ==== 在这里替换你的测试账号和密码 ====
    MY_USERNAME = "替换为你的用户名"
    MY_PASSWORD = "替换为你的密码"
    # =====================================

    if MY_USERNAME == "替换为你的用户名":
        print("请先在代码中设置正确的用户名和密码!")
        exit()

    success, browser = login_12306(MY_USERNAME, MY_PASSWORD)

    if success:
        print("登录成功,可在此处继续后续操作(如查询余票)。")
        # 示例:停留20秒后退出
        time.sleep(20)
    else:
        print("登录流程未成功。")

    browser.quit()
    print("浏览器已关闭。")

执行流程

  1. 确保Python、Selenium、Edge驱动已正确安装配置。
  2. 将上述代码中的 MY_USERNAME MY_PASSWORD 替换为你的12306测试账号(强烈建议使用非重要账号)。
  3. 在命令行中运行脚本: python login_12306.py
  4. 脚本会自动打开Edge浏览器,跳转到12306登录页,填充账号密码。
  5. 脚本会在控制台暂停,提示你手动操作验证码。
  6. 你切换到浏览器窗口,完成验证码图片的识别和点击。
  7. 完成后,切换回控制台窗口,按回车键。
  8. 脚本将继续执行,点击登录按钮,并反馈登录结果。

5. 常见问题排查与进阶优化

即使按照步骤操作,你也可能会遇到一些问题。这里我整理了几个最常见的“坑”及其解决方案。

5.1 驱动相关问题

  • 问题: WebDriverException: Message: 'msedgedriver' executable needs to be in PATH.
    • 原因 :系统找不到 msedgedriver.exe
    • 解决 :确认已将 msedgedriver.exe 所在目录添加到系统环境变量 Path 中,或者按照3.1节中的“方式二”,将驱动文件直接放到项目目录下,并在代码中指定路径。
  • 问题: SessionNotCreatedException: Message: session not created: This version of MSEdgeDriver only supports MSEdge version XX
    • 原因 :Edge浏览器版本与WebDriver驱动版本不匹配。
    • 解决 :严格按照2.2节的步骤,检查浏览器版本并下载对应的驱动。更新浏览器或回退驱动版本。

5.2 元素定位失败

  • 问题: NoSuchElementException: Message: no such element: Unable to locate element
    • 原因1 :页面尚未加载完成,脚本就尝试去定位元素。
      • 解决 :用 WebDriverWait 配合 EC (如 presence_of_element_located , element_to_be_clickable )替代 time.sleep 和直接的 find_element
    • 原因2 :页面结构发生变化,元素的ID或选择器已失效。
      • 解决 :使用开发者工具重新检查元素属性。可以尝试使用更稳定的定位方式,如通过 name 属性或相对路径更复杂的 XPath 。例如,如果ID是动态的,可以尝试用 By.XPATH, "//input[@placeholder='请输入用户名']" 来定位。
    • 原因3 :元素在 iframe 框架内。
      • 解决 :12306登录页目前没有使用iframe,但如果遇到其他网站有此情况,需要先用 driver.switch_to.frame(frame_reference) 切换到对应的iframe内,才能定位其中的元素。

5.3 反爬虫检测与规避

  • 现象 :脚本运行时,网页提示“操作过快”或直接跳转到验证页面,甚至账号被临时限制。
    • 分析与解决
      1. 降低操作频率 :在关键操作(如点击登录)前后添加随机的、人性化的等待时间,可以使用 time.sleep(random.uniform(1, 3))
      2. 优化浏览器指纹 :我们代码中已经添加了 edge_options 的一些参数来隐藏自动化特征。更高级的做法可以尝试禁用WebDriver的 cdp 协议命令、设置常见的User-Agent等。但请注意,这是一场“攻防战”,没有一劳永逸的方法。
      3. 核心原则 :任何公开的自动化操作都应遵守网站的 robots.txt 协议,并尊重服务器的负载。对于12306这类公共服务,切忌高频、并发地运行脚本,这不仅是技术问题,更是法律和道德问题。本脚本仅用于个人学习和技术验证。

5.4 验证码自动化进阶思考

手动点验证码终究不够“自动化”。如果你有进一步的学习需求,可以探索以下方向(请注意合规性):

  • 图像识别库 :研究使用 pytesseract (OCR)或 ddddocr 等库进行简单的字符验证码识别。但对于12306的物体识别点选验证码,难度极大。
  • 第三方打码平台 :市面上有一些提供人工打码或高识别率API的服务平台。其原理是你的程序将验证码图片发送到平台,平台返回点击坐标。这需要付费,且涉及接口调用。
  • 深度学习 :使用TensorFlow或PyTorch训练一个专门识别12306验证码的模型。这需要大量的标注数据、计算资源和专业知识。

重要提醒 :无论采用何种技术,对12306等网站进行自动化操作必须控制在极低的频率,且仅用于个人合法的学习或购票需求。大规模、商业化的自动化抢票或爬取行为,不仅会对服务器造成压力,也可能违反相关服务条款,甚至法律法规。

这个实战项目麻雀虽小,五脏俱全。它串联起了Selenium环境搭建、浏览器控制、元素定位、等待策略、异常处理和人机交互等多个核心知识点。希望这份详细的拆解和完整的代码,能帮你打通Web自动化的“任督二脉”,并应用到更多实际的场景中去。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐