一、背景与技术选型
12306 作为全球最大的实时票务系统,日均处理数千万次请求,其反爬机制也在不断升级。有这还怕平时回家买不到票?本文将介绍如何通过 Python 和 Selenium 实现自动化购票,重点突破以下技术难点:
- 反检测配置(隐藏自动化特征)
- 动态元素处理(显式等待与动态定位)
- 多场景交互(登录、查询、预订)
二、核心代码解析
1. 反检测配置(关键反爬策略)
options = Options()
# 移除自动化标识
options.add_experimental_option('excludeSwitches', ['enable-automation'])
options.add_argument('--disable-blink-features=AutomationsControlled')
# 关闭证书验证
options.add_argument('ignore-certificate-errors')
driver = webdriver.Chrome(options=options)
- 技术原理:通过修改浏览器启动参数,移除
navigator.webdriver
属性等自动化特征,降低被 12306 识别的概率。
2. 登录模块(用户输入与二次验证)
def Longin():
driver.get('https://kyfw.12306.cn/otn/resources/login.html')
user = input("请输入用户名: ")
pwd = getpass.getpass("请输入密码: ") # 密码输入隐藏
# 定位并输入用户名/密码
nameuser = driver.find_element(By.XPATH, '//div/input[@id="J-userName"]')
password = driver.find_element(By.XPATH, '//div/input[@id="J-password"]')
nameuser.send_keys(user)
password.send_keys(pwd)
driver.find_element(By.XPATH, '//*[@id="J-login"]').click()
# 二次验证(身份证后四位+短信验证码)
code_id = input("请输入身份证后四位:")
driver.find_element(By.XPATH, '//*[@id="verification_code"]').click()
code = input("请输入验证码:")
driver.find_element(By.XPATH, '//*[@id="code"]').send_keys(code)
# 检查登录结果
try:
error_element = driver.find_element(By.XPATH, '//*[@id="message"]/p')
if error_element.text == "用户名或密码错误":
print("登录失败:请检查信息")
else:
print("登录成功")
driver.find_element(By.XPATH, '//*[@id="link_for_ticket"]').click()
Inquirer()
except Exception as e:
print(f"未找到错误提示:{e}")
- 安全设计:使用
getpass
模块隐藏密码输入,避免敏感信息泄露。 - 验证流程:支持身份证后四位 + 短信验证码的二次验证,符合 12306 最新登录逻辑。
- 运行结果展示
-
请输入用户名: 13800138000 请输入密码: ········ # 输入时密码不可见 请输入身份证后四位:1234 验证码正在发送中,请耐心等待 请输入验证码:56789 登录成功
3. 查询模块(日期处理与车次提取)
def Inquirer():
chufadi = input("请输入出发地:")
mudidi = input("请输入目的地:")
# 日期处理(默认当天,支持格式验证)
current_date = datetime.now().strftime("%Y-%m-%d")
user_input = input("输入出发日期(格式: YYYY-MM-DD,留空默认今日):")
departure_date = current_date if not user_input else datetime.strptime(user_input, "%Y-%m-%d").strftime("%Y-%m-%d")
# 更新日期输入框
if departure_date != current_date:
date_input = driver.find_element(By.XPATH, '//*[@id="train_date"]')
date_input.clear()
date_input.send_keys(departure_date)
# 点击查询
driver.find_element(By.XPATH, '//*[@id="query_ticket"]').click()
# 提取车次信息(存在索引越界风险,需优化)
list_Train = []
for trin in range(20):
try:
dizhi = driver.find_element(By.XPATH, f'//*[@id="train_num_{trin}"]/div/strong')
list_Train.append(dizhi.text)
print(f"车次: {list_Train[trin]} | 出发地: {list_Train[trin+1]} | 到达时间: {list_Train[trin+2]}")
except:
break
- 日期验证:使用
datetime.strptime
进行格式校验,确保输入合法。 - 车次提取:通过循环遍历车次列表,动态获取车次信息(需注意索引越界问题)
- 运行结果
-
请输入出发地:北京 请输入目的地:上海 输入出发日期(格式: YYYY-MM-DD,留空默认今日):2025-05-20 正在查询2025-05-20的车票,请稍后... 出发地 目的地 到达时间 G1 北京南 上海虹桥 08:00 出发地 目的地 到达时间 G3 北京南 上海虹桥 09:00 出发地 目的地 到达时间 G5 北京南 上海虹桥 10:00
4. 预订模块(时间匹配与订单提交)
def Get_ticket():
# 提取所有车次的出发时间
time_list = [driver.find_element(By.XPATH, f'//*[@id="train_num_{i}"]/div[3]/strong[1]').text
for i in range(int(driver.find_element(By.XPATH, '//*[@id="trainum"]').text))]
move_time = input("选择出发时间段(1/2/3):")
wait = WebDriverWait(driver, 10)
if move_time == '1':
driver.find_element(By.XPATH, '//*[@id="cc_start_time"]/option[3]').click()
accurate_time = input("输入目标时间(如12:00):")
closest = get_closest_time(accurate_time, time_list)
# 显式等待确保预订按钮可见
wait.until(EC.visibility_of_element_located((By.XPATH, f'//a[@class="btn72" and contains(text(), "{closest}")]')))
driver.find_element(By.XPATH, f'//a[@class="btn72" and contains(@onclick, "{closest}")]').click()
# 提交订单
driver.find_element(By.XPATH, '//*[@id="normalPassenger_0"]').click() # 选择乘客
driver.find_element(By.XPATH, '//*[@id="submitOrder_id"]').click() # 提交订单
- 时间匹配:使用
get_closest_time
函数计算最接近用户指定时间的车次,提升购票精准度。 - 显式等待:通过
WebDriverWait
确保预订按钮加载完成,避免元素未找到错误。 - 运行结果展示
-
请输入出发时间段, 1: 06:00--12:00 2: 12:00--18:00 3: 18:00--24:001 输入准确时间,将会自动购买靠近该时间的车票(12:00)09:30 最接近 09:30 的时间是: 09:00 所有时间为 ['08:00', '09:00', '10:00']
三、代码优化与最佳实践
1. 反检测增强(应对 12306 最新策略)
# 通过CDP命令修改navigator.webdriver属性
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})
"""
})
- 技术原理:直接修改浏览器环境,隐藏
navigator.webdriver
属性,绕过 12306 的自动化检测。
2. 动态元素处理(避免固定索引)
# 优化车次信息提取(使用动态定位)
trains = driver.find_elements(By.XPATH, '//div[contains(@id, "train_num_")]')
for train in trains:
try:
info = [elem.text for elem in train.find_elements(By.XPATH, './div/strong')]
print(f"车次: {info[0]} | 出发地: {info[1]} | 到达时间: {info[2]}")
except IndexError:
continue
- 优化点:使用动态元素定位,避免固定索引导致的越界错误。
3. 异常处理(提升稳定性)
# 添加异常捕获与重试机制
from selenium.common.exceptions import NoSuchElementException
def safe_click(by, value, timeout=10):
try:
wait = WebDriverWait(driver, timeout)
element = wait.until(EC.element_to_be_clickable((by, value)))
element.click()
except NoSuchElementException:
print(f"元素 {value} 未找到")
except TimeoutException:
print(f"等待元素 {value} 超时")
- 作用:封装点击操作,处理元素未找到或超时异常,提高脚本鲁棒性。
四、风险提示与合规建议
-
法律风险:
- 自动化抢票可能违反 12306 用户协议,甚至涉嫌非法经营罪。
- 建议仅用于学习用途,避免用于实际购票。
-
账号安全:
- 频繁自动化操作可能导致账号被封禁。
- 避免在代码中存储敏感信息,如账号密码。
-
技术对抗:
- 12306 已推出防抢票专利,如 SVG 验证码、行为验证等。
- 脚本需持续更新以应对反爬策略变化。
五、总结与扩展
本文介绍了基于 Selenium 的 12306 自动化购票实现,涵盖反检测配置、登录验证、车次查询、时间匹配等核心功能。建议结合以下方向进一步优化:
- 多线程与分布式:使用多线程提高查询效率,结合代理 IP 避免 IP 封禁。
- 图像识别:集成 OCR 技术自动识别验证码,降低人工干预。
- 候补购票:模拟 12306 官方候补逻辑,提升购票成功率。
六、总体代码展示(Al优化后)
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time
from datetime import datetime
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import getpass
# ----------------------
# 浏览器反检测配置
# ----------------------
def create_chrome_options():
options = Options()
# 隐藏自动化特征
options.add_experimental_option('excludeSwitches', ['enable-automation'])
options.add_experimental_option('useAutomationExtension', False)
options.add_argument('--disable-blink-features=AutomationsControlled')
# 伪装用户代理(可替换为真实浏览器UA)
options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36')
# 其他实用配置
options.add_argument('ignore-certificate-errors')
options.add_argument('--disable-gpu')
options.add_argument('--no-sandbox')
options.add_argument('--headless') #无头模式 注释这句 可以展示出购票 过程
#不注释,大大缩短,购票时间
return options
# ----------------------
# 登录模块
# ----------------------
def login(driver):
try:
driver.get('https://kyfw.12306.cn/otn/resources/login.html')
wait = WebDriverWait(driver, 10)
# 输入用户名和密码
user = input("请输入用户名: ")
pwd = getpass.getpass("请输入密码: ")
# 定位并输入用户名
username_input = wait.until(EC.presence_of_element_located((By.XPATH, '//div/input[@id="J-userName"]')))
username_input.send_keys(user)
# 定位并输入密码
password_input = wait.until(EC.presence_of_element_located((By.XPATH, '//div/input[@id="J-password"]')))
password_input.send_keys(pwd)
# 点击登录按钮
login_button = wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="J-login"]')))
login_button.click()
# 二次验证:身份证后四位
code_id = input("请输入身份证后四位: ")
id_input = wait.until(EC.presence_of_element_located((By.XPATH, '/html/body/div[1]/div[4]/div[2]/div[1]/div/div[1]/input')))
id_input.send_keys(code_id)
# 点击获取短信验证码
get_code_button = wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="verification_code"]')))
get_code_button.click()
time.sleep(2)
# 输入短信验证码
code = input("请输入短信验证码: ")
code_input = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="code"]')))
code_input.send_keys(code)
# 检查登录结果
try:
error_msg = wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@id="message"]/p'))).text
if "用户名或密码错误" in error_msg:
print("登录失败:用户名、密码或验证码错误")
return False
except:
print("登录成功!")
# 跳转到车票查询页面
ticket_link = wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="link_for_ticket"]')))
ticket_link.click()
return True
except Exception as e:
print(f"登录过程中出现错误:{e}")
return False
# ----------------------
# 车次查询模块
# ----------------------
def search_tickets(driver):
try:
wait = WebDriverWait(driver, 10)
# 输入出发地和目的地
from_station = input("请输入出发地: ")
to_station = input("请输入目的地: ")
# 定位出发地输入框
from_input = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="fromStationText"]')))
from_input.clear()
from_input.send_keys(from_station)
# 定位目的地输入框
to_input = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="toStationText"]')))
to_input.clear()
to_input.send_keys(to_station)
# 处理出发日期
current_date = datetime.now().strftime("%Y-%m-%d")
user_date = input("输入出发日期(格式: YYYY-MM-DD,留空默认今日): ")
departure_date = current_date if not user_date else user_date
# 更新日期输入框
if departure_date != current_date:
date_input = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="train_date"]')))
date_input.clear()
date_input.send_keys(departure_date)
# 点击查询按钮
search_button = wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="query_ticket"]')))
search_button.click()
print(f"正在查询{departure_date}从{from_station}到{to_station}的车票...")
time.sleep(3) # 等待查询结果加载
return departure_date
except Exception as e:
print(f"查询车次时出现错误:{e}")
return None
# ----------------------
# 车票预订模块
# ----------------------
def book_ticket(driver, departure_date):
try:
wait = WebDriverWait(driver, 10)
# 提取所有车次时间
time_list = []
train_elements = wait.until(EC.presence_of_all_elements_located((By.XPATH, '//div[contains(@id, "train_num_")]')))
for i, train in enumerate(train_elements):
try:
time_text = train.find_element(By.XPATH, './div[3]/strong[1]').text
time_list.append(time_text)
except:
continue
if not time_list:
print("未找到可用车次!")
return
# 用户选择时间段
move_time = input("请选择出发时间段(1:06:00-12:00,2:12:00-18:00,3:18:00-24:00): ")
time_ranges = {
'1': ('06:00--12:00', 3),
'2': ('12:00--18:00', 4),
'3': ('18:00--24:00', 5)
}
if move_time not in time_ranges:
print("无效选择!")
return
# 选择时间段
range_name, option_index = time_ranges[move_time]
time_option = wait.until(EC.element_to_be_clickable((By.XPATH, f'//*[@id="cc_start_time"]/option[{option_index}]')))
time_option.click()
print(f"已筛选{range_name}的车次")
# 输入目标时间并匹配最接近车次
target_time = input("请输入目标时间(格式: HH:MM): ")
closest_time = get_closest_time(target_time, time_list)
print(f"最接近的车次时间:{closest_time}")
# 等待预订按钮加载并点击
book_button = wait.until(EC.element_to_be_clickable((By.XPATH, f'//a[@class="btn72" and contains(text(), "{closest_time}")]')))
book_button.click()
time.sleep(2)
# 选择乘客(默认第一个乘客,需根据实际情况调整)
passenger = wait.until(EC.element_to_be_clickable((By.XPATH, '//ul[@id="normal_passenger_id"]/li[1]/input')))
passenger.click()
# 提交订单(需手动处理可能的验证码或确认弹窗)
submit_button = wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="submitOrder_id"]')))
submit_button.click()
print("已提交订单,请尽快完成支付!")
except Exception as e:
print(f"预订车票时出现错误:{e}")
# ----------------------
# 时间匹配函数
# ----------------------
def get_closest_time(user_time, time_list):
user_min = int(user_time[:2]) * 60 + int(user_time[3:])
time_diffs = [(abs(int(t[:2])*60 + int(t[3:]) - user_min), t) for t in time_list]
return min(time_diffs, key=lambda x: x[0])[1]
# ----------------------
# 主程序入口
# ----------------------
if __name__ == "__main__":
options = create_chrome_options()
driver = webdriver.Chrome(options=options)
if login(driver):
departure_date = search_tickets(driver)
if departure_date:
book_ticket(driver, departure_date)
input("按回车键退出程序...")
driver.quit()
作者原创
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options # 导入Options操作类
import time
from datetime import datetime
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import getpass
# 获取当前日期(格式:YYYY-MM-DD)
current_date = datetime.now().strftime("%Y-%m-%d")
options = Options()
options.add_argument('--headless') #无头模式
# 设置自动化特性扩展的关闭,防止被服务器检测到是由selenium驱动的
options.add_experimental_option('excludeSwitches', ['enable-automation'])
# 设置关闭自动化特性,防止被服务器检测到是由selenium驱动的
options.add_argument('--disable-blink-features=AutomationsControlled')
# 关闭证书报错
options.add_argument('ignore-certificate-errors')
# 启动 WebDriver(假设你使用的是 Chrome) 如果这里不进行传参数 就是有界面模式
driver = webdriver.Chrome(options = options)
# driver = webdriver.Chrome()
time.sleep(1)
def Longin():
try:
# 打开目标网页
driver.get('https://kyfw.12306.cn/otn/resources/login.html')
# 获取用户输入的账号和密码
user = input("请输入用户名: ")
# pwd = input("请输入密码: ")
pwd = getpass.getpass("请输入密码: ") # 输入时密码不会显示在屏幕上
# 使用 XPath 定位用户名输入框并输入
nameuser = driver.find_element(By.XPATH, '//div/input[@id="J-userName"]')
time.sleep(1)
nameuser.send_keys(user)
# 定位密码输入框并输入
password = driver.find_element(By.XPATH, '//div/input[@id="J-password"]')
time.sleep(1)
password.send_keys(pwd)
# 登录按钮点击
driver.find_element(By.XPATH, '//*[@id="J-login"]').click()
# 后续代码保持不变
code_id = input("请输入身份证后四位:")
id_cord = driver.find_element(By.XPATH, '/html/body/div[1]/div[4]/div[2]/div[1]/div/div[1]/input')
id_cord.send_keys(code_id)
print("验证码正在发送中,请耐心等待")
time.sleep(1)
driver.find_element(By.XPATH, '//*[@id="verification_code"]').click()
time.sleep(1)
id_code = driver.find_element(By.XPATH, '//*[@id="code"]')
code = input("请输入验证码:")
id_code.send_keys(f"{code}")
try:
error_element = driver.find_element(By.XPATH, '//*[@id="message"]/p')
if error_element.text == "用户名或密码错误":
print("密码,用户名,验证码错误,请检查")
else:
print("登录成功")
time.sleep(2)
driver.find_element(By.XPATH, '//*[@id="link_for_ticket"]').click()
time.sleep(2)
driver.find_element(By.XPATH, '//*[@id="sureClick"]')
Inquirer()
except Exception as e:
print(f"未找到错误提示元素: {e}")
except Exception as e:
print("程序运行发生错误", e)
def Inquirer():
try:
chufadi = input("请输入出发地:")
# 获取出发地输入框
from_station_input = driver.find_element(By.XPATH, '//*[@id="fromStationText"]')
# 清空输入框
from_station_input.clear()
# 输入出发地
from_station_input.send_keys(chufadi)
mudidi = input("请输入目的地:")
end_station_input = driver.find_element(By.XPATH, '//*[@id="toStationText"]')
end_station_input.clear()
end_station_input.send_keys(mudidi)
from datetime import datetime, timedelta
try:
# 获取当前日期
current_date = datetime.now().strftime("%Y-%m-%d")
# 获取用户输入
user_input = input("不输入默认获取当日,回车跳过输入,输入出发时间格式(2025-5-20):")
# 处理用户输入
if not user_input.strip(): # 输入为空
departure_date = current_date
print(f"未输入日期,将查询今日({departure_date})车票")
else:
try:
# 验证并格式化日期
parsed_date = datetime.strptime(user_input, "%Y-%m-%d")
departure_date = parsed_date.strftime("%Y-%m-%d")
except ValueError:
print("警告:输入日期格式不正确,将使用默认日期")
departure_date = current_date
# 如果不是查询当日,需要更新日期输入框
if departure_date != current_date:
date_input = driver.find_element(By.XPATH, '//*[@id="train_date"]')
date_input.clear()
date_input.send_keys(departure_date)
# 点击查询按钮(合并重复逻辑)
query_button = driver.find_element(By.XPATH, '//*[@id="query_ticket"]')
query_button.click()
print(f"正在查询{departure_date}的车票,请稍后...")
list_Train = []
for trin in range(0, 20):
dizhi = driver.find_element(By.XPATH, f'//*[@id="train_num_{trin}"]/div/strong')
time.sleep(1)
list_Train.append(dizhi.text)
print(f"""
出发地 目的地 到达时间
{list_Train[trin]} {list_Train[trin+1]} {list_Train[trin+2]}
""")
except Exception as e:
print(f"程序运行发生错误,在输入日期 {e}")
except Exception as e:
print("程序运行发生错误,在输入地点", e)
#下面这个会确定元素加载完才会进行下一步
def Get_ticket():
time_list = []
chechi_number = driver.find_element(By.XPATH, '//*[@id="trainum"]')
chechi_number = int(chechi_number.text)
for i in range(chechi_number):
time_list.append(driver.find_element(By.XPATH, f'//*[@id="train_num_{i}"]/div[3]/strong[1]').text)
move_time = input("请输入出发时间段,\n1: 06:00--12:00\n2: 12:00--18:00\n3: 18:00--24:00")
# 创建显式等待对象
wait = WebDriverWait(driver, 10)
if move_time == '1':
"""
//*[@id="cc_start_time"]/option[*] 选着时间段
#这个是通过固定时间选定这个预订标签 onclick 这个下面的时间 选着这个标签
//a[@class="btn72" and contains(@onclick, '21:02')]
"""
driver.find_element(By.XPATH, '//*[@id="cc_start_time"]/option[3]').click()
accurate_time = input("输入准确时间,将会自动购买靠近该时间的车票(12:00)")
closest = get_closest_time(accurate_time, time_list)
print(f"最接近 {accurate_time} 的时间是: {closest}")
print('所有时间为', str(time_list))
# 添加显式等待
wait.until(
EC.visibility_of_element_located((By.XPATH, f'//a[@class="btn72" and contains(text(), "{closest}")]')))
# 点击预定按钮
driver.find_element(By.XPATH, f'//a[@class="btn72" and contains(@onclick, "{closest}")]').click()
time.sleep(1)
driver.find_element(By.XPATH, '//*[@id="normalPassenger_0"]').click() # 点击坐车人按钮
driver.find_element(By.XPATH, '//*[@id="dialog_xsertcj_cancel"]').click() # 点击取消按钮
driver.find_element(By.XPATH, '//*[@id="submitOrder_id"]').click() # 点击提交订单按钮
if move_time == '2':
driver.find_element(By.XPATH, '//*[@id="cc_start_time"]/option[4]').click()
accurate_time = input("输入准确时间,将会自动购买靠近该时间的车票(12:00)")
closest = get_closest_time(accurate_time, time_list)
print(f"最接近 {accurate_time} 的时间是: {closest}")
print('所有时间为', str(time_list))
# 添加显式等待
wait.until(
EC.visibility_of_element_located((By.XPATH, f'//a[@class="btn72" and contains(text(), "{closest}")]')))
# 点击预定按钮
driver.find_element(By.XPATH, f'//a[@class="btn72" and contains(@onclick, "{closest}")]').click()
time.sleep(1)
driver.find_element(By.XPATH, '//*[@id="normalPassenger_0"]').click() # 点击坐车人按钮
driver.find_element(By.XPATH, '//*[@id="dialog_xsertcj_cancel"]').click() # 点击取消按钮
driver.find_element(By.XPATH, '//*[@id="submitOrder_id"]').click() # 点击提交订单按钮
if move_time == '3':
driver.find_element(By.XPATH, '//*[@id="cc_start_time"]/option[5]').click()
accurate_time = input("输入准确时间,将会自动购买靠近该时间的车票(12:00)")
closest = get_closest_time(accurate_time, time_list)
print(f"最接近 {accurate_time} 的时间是: {closest}")
print('所有时间为', str(time_list))
# 添加显式等待
wait.until(
EC.visibility_of_element_located((By.XPATH, f'//a[@class="btn72" and contains(text(), "{closest}")]')))
# 点击预定按钮
driver.find_element(By.XPATH, f'//a[@class="btn72" and contains(@onclick, "{closest}")]').click()
time.sleep(1)
driver.find_element(By.XPATH, '//*[@id="normalPassenger_0"]').click() # 点击坐车人按钮
driver.find_element(By.XPATH, '//*[@id="dialog_xsertcj_cancel"]').click() # 点击取消按钮
driver.find_element(By.XPATH, '//*[@id="submitOrder_id"]').click() # 点击提交订单按钮
def get_closest_time(user_time, time_list):
user_minutes = int(user_time[:2]) * 60 + int(user_time[3:])
time_diffs = [(abs(int(t[:2]) * 60 + int(t[3:]) - user_minutes), t) for t in time_list]
return min(time_diffs)[1]
if __name__ == '__main__':
Longin()
# 等待一段时间以观察结果
# time.sleep()
input("回车结束")
# 关闭浏览器
driver.quit()