【Web自动化测试】Python + Selenium实现携程网火车票订购的自动化测试

整体流程图

在这里插入图片描述

代码展示

import json
import sys
import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from time import sleep, time
from datetime import datetime

# 预输入
departure = ""     # 出发站[如:北京]
destination = ""   # 到达站[如:上海]
date = ""   	   # 日期[格式:YYYY-MM-DD]
departTime = datetime.strptime("15:00", "%H:%M").time()  # 预期出发时间,最早
arrivalTime = datetime.strptime("18:00", "%H:%M").time() # 预期抵达时间,最晚
name = ""    			# 姓名
passengerID = ""    	# 身份证号
tel = "" 				# 11位自然数手机号
passengerType = ""      # 成人、学生、儿童

# 初始化浏览器驱动并访问火车票查询页面
driver = webdriver.Chrome()
url = "https://trains.ctrip.com/"
driver.get(url)
# 最大化页面
driver.maximize_window()

# 捕获异常方法【用于判断某元素是否存在】
def is_element_present(driver, xpath):
    try:
        driver.find_element(By.XPATH, xpath)
        return True
    except Exception:
        return False

# 判断cookies.txt是否存在并检测cookies.txt内是否有内容
cookiesFilePath = "./cookies.txt"
if os.path.exists(cookiesFilePath) and os.path.getsize(cookiesFilePath) > 0:
    print("将使用Cookies实现自动登录!")
    # 注入cookies
    with open(cookiesFilePath, 'r') as file:
        cookies = json.load(file)
    for cookie in cookies:
        # 添加 cookie 之前,删除 'expiry' 属性,防止不兼容问题
        if 'expiry' in cookie:
            del cookie['expiry']
        driver.add_cookie(cookie)
else:
    print("请手动登录!")
    # 点击登录按钮
    loginBtn = driver.find_element(By.XPATH, "//button[@class='tl_nfes_home_header_login_not_frb4a']")
    loginBtn.click()
    # 隐式等待
    driver.implicitly_wait(10)
    # 切换登录方式为:扫码登录
    switchLoginWayToQR = driver.find_element(By.PARTIAL_LINK_TEXT, "扫码登录")
    switchLoginWayToQR.click()
    # 循环等待扫码完成
    while True:
        print("===========开始循环===========")
        if is_element_present(driver, "//p[text()='二维码已失效']"):
            print("=========二维码已过期=========")
            sleep(3)
            refreshBtn = driver.find_element(By.XPATH, "//a[text()='刷新']")
            refreshBtn.click()
            sleep(3)
            print("=========二维码已刷新=========")
        elif is_element_present(driver, "//*[@id='label-departStation']"):
            print("===========登录成功===========")
            break
        else:
            print("=========等待扫描QR码=========")
            sleep(10)
    cookies = driver.get_cookies()
    # 保存cookies到本地
    with open(cookiesFilePath, 'w') as file:
        json.dump(cookies, file)
        print("cookies写入完成")

sleep(3)
# 浏览器刷新
driver.refresh()
sleep(3)

# 定位元素【出发站、到达站、日期】
departureStation = driver.find_element(By.XPATH, "//*[@id='label-departStation']")
arrivalStation = driver.find_element(By.XPATH, "//*[@id='label-arriveStation']")
datePicker = driver.find_element(By.XPATH, "//*[@id='label-departDate']")
searchBtn = driver.find_element(By.XPATH, "//*[text()='搜索']")

# 输入出发站、到达站并选择日期
departureStation.send_keys(Keys.CONTROL, 'a')
departureStation.send_keys(Keys.DELETE)
departureStation.send_keys(f"{departure}")
arrivalStation.send_keys(Keys.CONTROL, 'a')
arrivalStation.send_keys(Keys.DELETE)
arrivalStation.send_keys(f"{destination}")

# 使用 JavaScript 添加样式规则
js_code = """
var styles = '.assist-block-dom, .assist-flex-dom, .assist-ib-dom { display: block !important; }';
var styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
"""
driver.execute_script(js_code)
datePicker.send_keys(Keys.CONTROL, 'a')
datePicker.send_keys(Keys.DELETE)
datePicker.send_keys(f"{date}")

'''
不适用click()的原因在于在调整出日期的输入框后,会导致页面出现一层遮蔽罩
这就导致无法直接点击按钮
所以直接使用 JavaScript 点击
'''
driver.execute_script("arguments[0].click();", searchBtn)

# 隐式等待
driver.implicitly_wait(5)
sleep(2)

# 滚动加载数据
last_count = 0

while True:
    # 获取当前卡片数量
    cards = driver.find_elements(By.XPATH, "//div[@class='card-white list-item']")
    current_count = len(cards)
    # 如果卡片数量没有变化,停止滚动
    if current_count == last_count:
        break
    # 滚动到底部
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    last_count = current_count
    # 等待新数据加载
    sleep(2)  # 根据加载速度调整等待时间

# 将页面滚动到顶部,方便观察
driver.execute_script("window.scrollTo(0, 0);")

# 打开筛选界面
driver.find_element(By.XPATH, "//div[text()='展开']").click()
sleep(1)

# 筛选元素定位
# 仅显示有票车次
availableTickets = driver.find_element(By.XPATH, "//*[text()='仅显示有票车次']")

# 车型
gaotie = driver.find_element(By.XPATH, "//*[text()='高铁(G/C)']")
dongche = driver.find_element(By.XPATH, "//*[text()='动车(D)']")
putong = driver.find_element(By.XPATH, "//*[text()='普通(Z/T/K)']")
qita = driver.find_element(By.XPATH, "//*[text()='其他(L/Y)']")

# 出发时间
selectDeTime0 = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[3]/ul/li[1]''')
selectDeTime6 = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[3]/ul/li[2]''')
selectDeTime12 = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[3]/ul/li[3]''')
selectDeTime18 = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[3]/ul/li[4]''')

# 到达时间
selectArrTime0 = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[4]/ul/li[1]''')
selectArrTime6 = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[4]/ul/li[2]''')
selectArrTime12 = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[4]/ul/li[3]''')
selectArrTime18 = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[4]/ul/li[4]''')

# 坐席
yingzuo = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[5]/ul/li[1]''')
yingwo = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[5]/ul/li[2]''')
ruanwo = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[5]/ul/li[3]''')
wuzuo = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[5]/ul/li[4]''')
gaojiruanwo = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[5]/ul/li[5]''')
erdengzuo = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[5]/ul/li[6]''')
erdengwo = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[5]/ul/li[7]''')
yidengwo = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[5]/ul/li[8]''')
yidengzuo = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[5]/ul/li[9]''')
shangwuzuo = driver.find_element(By.XPATH, '''//*[@id="__next"]/div/div[3]/div[2]/div[1]/div[2]/div[5]/ul/li[10]''')

# 进行筛选
availableTickets.click()
dongche.click()
putong.click()
qita.click()
selectDeTime12.click()
selectArrTime12.click()
yingzuo.click()

# 隐式等待
driver.implicitly_wait(5)

# 根据预期时间进行再筛选
cards = driver.find_elements(By.XPATH, "//section[@role='product']/div[@class='card-white list-item']")
if len(cards) == 0:
    print("没有预期车票,关闭程序~")
    sleep(2)
    driver.quit()
    sys.exit()
isValidTicket = False
for card in cards:
    deTime = card.find_element(By.XPATH, ".//div/div[@class='from']/div[@class='time']").text
    arrTime = card.find_element(By.XPATH, ".//div/div[@class='to']/div[@class='time']").text
    if departTime <= datetime.strptime(deTime, "%H:%M").time():
        if arrivalTime >= datetime.strptime(arrTime, "%H:%M").time():
            print(f"此次列车出发时间为:{deTime}, 预计到达时间为:{arrTime}")
            isValidTicket = True
            cardId = card.get_attribute('id')
            print(cardId)
            break

if isValidTicket == False:
    print("没有预期车票,关闭程序~")
    sleep(2)
    driver.quit()
    sys.exit()

# 定位到预期车次card并展开
unfoldBtn = driver.find_element(By.XPATH, f"//*[@id='{cardId}']/div[@class='list-bd']/button[@aria-label='展开或收起详情']")
unfoldBtn.click()
bookBtn = driver.find_element(By.XPATH, f"//*[@id='{cardId}']/following-sibling::div/ul/li/strong[text()='硬座']/following-sibling::button")
driver.execute_script("arguments[0].click();", bookBtn)

# 显式等待
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.XPATH, f"//li[.//div[@class='name' and text()='{name}']]")))
sleep(1)

def selectPassenger(name):
    driver.find_element(By.XPATH, "//button[contains(@class, 'btn-blue') and contains(@class, 'btn-add')]").click()
    # 显式等待iframe加载完成
    WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.XPATH, "//iframe[@id='picker-iframe']")))
    # 切换frame
    iframe = driver.find_element(By.XPATH, "//iframe[@id='picker-iframe']")
    driver.switch_to.frame(iframe)
    # 定位到待添加乘客信息
    passengerCard = driver.find_element(By.XPATH, f"//span[text()='{name}']/ancestor::div[2][@class='PassengerCell_innerContainer__PPhbr']")
    passengerCard.click()
    # 点击确认按钮
    confirmBtn = driver.find_element(By.XPATH, "//div[contains(@class, 'passengerList_bottomButton__VF11U')]")
    confirmBtn.click()
    # 切换回上一个页面
    driver.switch_to.parent_frame()

# 寻找指定名字的乘客信息
if is_element_present(driver, f"//li[.//div[@class='name' and text()='{name}']]"):
    selectPassenger(name)
else:
	# 点击新增游客
    driver.find_element(By.XPATH, "//button[text()='新增乘客']").click()
    # 选择乘客类型
    driver.find_element(By.XPATH, f"//div[p[text()='{passengerType}']]").click()    
    # 输入姓名
    driver.find_element(By.XPATH, "//input[contains(@class, 'input-txt') and contains(@class, 'focus-cNName')]").send_keys(f"{name}")
    # 输入身份证号
    driver.find_element(By.XPATH, "//input[contains(@class, 'input-txt') and contains(@class, 'focus-identityNo')]").send_keys(f"{passengerID}")
    # 输入手机号
    driver.find_element(By.XPATH, "//input[contains(@class, 'input-txt') and contains(@class, 'focus-mobilePhone')]").send_keys(f"{tel}")
    # 点击添加按钮
    driver.find_element(By.XPATH, "//button[text()='确认添加']").click()
    # 选择乘客
    selectPassenger(name)

# 预订
buyBtn = driver.find_element(By.XPATH, "//button[text()='立即预订']")
buyBtn.click()

# 进入待支付页面
sleep(1800)

# 所有操作结束后等待10秒并释放资源
sleep(10)
driver.quit()

代码重构

代码重构篇

技术点总结

在该项目中运用到了:

  1. Python函数
  2. Python异常处理
  3. Python文件操作
  4. Python模块
  5. Selenium元素定位(XPATH、超链接文本)
  6. Selenium元素操作
  7. Selenium浏览器操作
  8. Selenium键盘操作
  9. Selenium页面滚动
  10. Selenium页面等待
  11. Selenium切换frame
  12. Selenium处理cookies

项目难点

1. 登录实现

在尝试通过Selenium实现登录的过程中,我尝试了3种方式:

首先,我尝试了通过账号+密码的方式登录,但是发现无法获取验证码输入框中的内容,导致无法判定是否输入有效的验证码,最终放弃了该登录方式。

try:
    # 显式等待验证码界面加载
    WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.XPATH, "//*[@id='sc_mobilevalidate']")))
    if is_element_present(driver, "//*[@id='sc_mobilevalidate']"):
        print("验证码界面已加载")
        # 验证码
        captchaInput = driver.find_element(By.XPATH, "//input[@data-testid='verifyCodeInput']")
        sendCaptcha = driver.find_element(By.XPATH, "//a[text()='发送验证码']")
        verificationCode = input("请输入手机获取的验证码:")
        driver.execute_script("arguments[0].value = arguments[1]; arguments[0].dispatchEvent(new Event('input'));", captchaInput, verificationCode)
        # sendCaptcha.click()
        # 尝试获取验证码输入框内容
        captchaInput.send_keys("123456")
        print(captchaInput.get_attribute('value'))
        # 循环等待验证码
        
    else:
        print("验证码界面不存在")

except TimeoutException:
    print("等待验证码界面超时")

接着,我又尝试了手机号+验证码的登录方式,但是在获取验证码的过程中需要通过两种不同的验证方式,最终放弃了该登录方式。

# 切换登录方式为:验证码登录
switchLoginWay = driver.find_element(By.XPATH, "//*[text()='验证码登录']")
switchLoginWay.click()
driver.implicitly_wait(5)

# 登录
account = driver.find_element(By.XPATH, "//input[@type='tel']")
protocol = driver.find_element(By.XPATH, "//*[@id='checkboxAgreementInput']")
sendCaptcha = driver.find_element(By.PARTIAL_LINK_TEXT, "验证码")
loginBtn = driver.find_element(By.XPATH, "//input[@value='登   录']")
account.send_keys(accountText)
driver.execute_script("arguments[0].click();", protocol)

# 等待输入验证码
js_str = 'return document.querySelector("input[data-testid=\'verifyCodeInput\']").value;'
print("等待输入验证码~")
timeout = 60
while True:
    start_time = time()  # 记录每次循环的开始时间
    # 点击发送验证码按钮【实际是超链接】
    sendCaptcha.click()
    while True:
        captchaCode = driver.execute_script(js_str)
        if re.fullmatch(r'\d{6}', captchaCode):
            print("验证码输入完毕!")
            break
        elif time() - start_time > timeout:  # 如果超过60秒
            print("等待超时,重新发送验证码...")
            break  # 结束当前循环,重新开始
        else:
            sleep(1)
    if re.fullmatch(r'\d{6}', captchaCode):  # 如果验证码正确,跳出外层循环
        break

# 点击登录按钮
driver.execute_script("arguments[0].click();", loginBtn)

最终,我选择了手动扫码登录+本地Cookies注入的登录方式,可以实现较为便捷的登录。

# 捕获异常方法【用于判断某元素是否存在】
def is_element_present(driver, xpath):
    try:
        driver.find_element(By.XPATH, xpath)
        return True
    except Exception:
        return False

# 判断cookies.txt是否存在及检测cookies.txt内是否有内容
cookiesFilePath = "./cookies.txt"
if os.path.exists(cookiesFilePath) and os.path.getsize(cookiesFilePath) > 0:
    print("将使用Cookies实现自动登录!")
    # 注入cookies
    with open(cookiesFilePath, 'r') as file:
        cookies = json.load(file)
    for cookie in cookies:
        # 添加 cookie 之前,删除 'expiry' 属性,防止不兼容问题
        if 'expiry' in cookie:
            del cookie['expiry']
        driver.add_cookie(cookie)
else:
    print("请手动登录!")
    # 点击登录按钮
    loginBtn = driver.find_element(By.XPATH, "//button[@class='tl_nfes_home_header_login_not_frb4a']")
    loginBtn.click()

    # 隐式等待
    driver.implicitly_wait(10)

    # 切换登录方式为:扫码登录
    switchLoginWayToQR = driver.find_element(By.PARTIAL_LINK_TEXT, "扫码登录")
    switchLoginWayToQR.click()

    # 循环等待扫码完成
    while True:
        print("===========开始循环===========")
        if is_element_present(driver, "//p[text()='二维码已失效']"):
            print("=========二维码已过期=========")
            sleep(3)
            refreshBtn = driver.find_element(By.XPATH, "//a[text()='刷新']")
            refreshBtn.click()
            sleep(3)
            print("=========二维码已刷新=========")
        elif is_element_present(driver, "//*[@id='label-departStation']"):
            print("===========登录成功===========")
            break
        else:
            print("=========等待扫描QR码=========")
            sleep(10)
    cookies = driver.get_cookies()
    # 保存cookies到本地
    with open(cookiesFilePath, 'w') as file:
        json.dump(cookies, file)
        print("cookies写入完成")

sleep(3)
# 浏览器刷新
driver.refresh()
sleep(3)
2. 火车票查询-日期控件

这个问题的关键在于日期控件的display 如何从none调整至block。该问题的发现、解决尝试和最终方法都在我的另外一篇文章Selenium处理携程日期组件的方法中,具体代码如下:

# 使用 JavaScript 添加样式规则
js_code = """
var styles = '.assist-block-dom, .assist-flex-dom, .assist-ib-dom { display: block !important; }';
var styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
"""
driver.execute_script(js_code)
datePicker.send_keys(Keys.CONTROL, 'a')
datePicker.send_keys(Keys.DELETE)
datePicker.send_keys(f"{date}")

总结

整体流程对于像我这样的初学者确实有一定的难度,但是在整体流程中可以复习到之前学到的生硬的知识并将其转换为实际应用。
在流程上还需要提取测试点、设计测试用例,并根据测试用例设计设计具体程序,在这里就不展示测试点提取以及测试用例了。

侵权必删声明
本资料部分内容来源于互联网及公开渠道,仅供学习和交流使用,版权归原作者所有。若涉及版权问题,敬请原作者联系我,我将立即处理或删除相关内容。感谢您的理解与支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值