selenium综合练习-实现12306购票
1. 练习初衷
回顾所需的关于selenium的操作,集中在一个案例中练习,本博客提出的方法仍有许多不足之处,虽然可以实现(半)自主购票,但是与市面上的抢票软件相比还是差上许多,所以这里笔者仅分享自己再学习selenium过程中的一些感悟。
2. selenium实现12306购票
2.1 类基本框架
代码需求:以面向对象思路编写程序,给出类的init()方法,完成实例属性的初始化。
import csv
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
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import NoSuchElementException,ElementNotVisibleException
# 创建驱动,将其设为全局变量
driver = webdriver.Chrome()
class BuyTicket(object):
# 存储一些类属性,几个后续会用到的url
login_url = 'https://kyfw.12306.cn/otn/resources/login.html'
personal_url = 'https://kyfw.12306.cn/otn/view/index.html'
remain_ticket_url = 'https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc'
passenger_url = 'https://kyfw.12306.cn/otn/confirmPassenger/initDc'
def __init__(self, from_station, to_station, train_date):
self.from_station = from_station # 获取起始地
self.to_station = to_station # 获取目的地
self.train_date = train_date # 获取乘车日期
self.expected_trains = expected_trains # 用户想乘坐的车次
self.passengers = passengers
self.station_code = {}
self.init_station_codes() # 初始化各车站代号
def init_station_codes(self):
# 读取csv文件,获得车站代号
with open('station_code.csv', 'r', encoding='utf-8') as fobj:
reader_obj = csv.DictReader(fobj)
for cont in reader_obj:
name = cont['name']
code = cont['code']
self.station_code[name] = code
def run(self):
pass
def main():
buyer = BuyTicket('', '', '') # 参数需自己填充
buyer.run()
if __name__ == '__main__':
main()
注意点:
- driver驱动不能写在init方法中,原因在于若将驱动写成实例对象的属性,当类对象被销毁,驱动也跟着销毁;
- 初始化的时候,读取了csv文件来获取车站代号,以便后续使用;
- run()方法封装了类的所有基本功能,后续的所有操作基本靠调用该run()方法。
出于篇幅考虑,笔者这里只展示station_code.csv文件的部分内容:
name,code
北京北,VAP
北京东,BOP
北京,BJP
北京南,VNP
北京西,BXP
广州南,IZQ
重庆北,CUW
重庆,CQW
重庆南,CRW
重庆西,CXW
广州东,GGQ
上海,SHH
2.2 网站登录
代码需求:登录账号,成功后进入查询车票的页面。
想要在12306买票之前,必须先进行登陆操作。12306网站登录时,有两种方式登录:一种是扫码登录,另一种是账号密码登录。模拟登陆时输入账号、密码等操作,在笔者之前的博客均已展示,12306网站麻烦的是要点击图片验证码,在练习request第三方库的使用时是通过下载验证码图片到本地,手动获取对应图片的位置像素,并在request请求中携带query_parameters
,成功地模拟网站的登录。
这里,笔者打算用selenium先输入账号、密码,再手动点击验证码图片完成登录。等以后技术精进,再考虑更加智能化的方法。笔者测试模拟账号密码登录时,总是失败,猜想原因在于12306对selenium的监控措施比较严格,尽管手动点击图片验证码,但最后的滑块验证总是通不过。所以,笔者接下来只演示如何切换账号密码登录,并模拟输入账号密码,全当对之前所学知识点的复习。
12306登录界面的关键标签如下图:
示例代码:
def login(self):
driver.get(self.login_url) # 访问登录界面
# 添加显式等待,直到加载出二维码登录
WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.ID, 'J-qrImg'))
)
account_btn = driver.find_element_by_class_name('login-hd-account')
account_btn.click() # 点击账号密码登录
input_username = driver.find_element_by_id('J-userName')
input_username.send_keys('12345678') # 输入账号
driver.implicitly_wait(2) # 隐式等待2s
input_passwd = driver.find_element_by_id('J-password')
input_passwd.send_keys('XXXXXXX') # 输入密码
运行结果:
由于模拟账号密码登录仍有问题,现采用手动扫码登录,login()方法仍需修改:
def login(self):
driver.get(self.login_url) # 访问登录界面
# 添加显式等待,直到加载到对应的url
WebDriverWait(driver, 30).until(
EC.url_contains(self.personal_url)
)
print('登录成功!')
运行结果:
注意点:
- 账号密码登录时,需添加显式等待,直到二维码图片加载完成,才能点击切换按钮;
- 12306网站加载时间比较长,不确定什么时候登陆成功(网页加载完成),所以需要添加显式等待。
2.3 车次以及余票查询
代码需求:
- 由个人详情页面跳转到了车次查询界面;
- 将车站代号存储到字典中,通过key来找到代号value;
- 设置与起始地、目的地、出发日期对应的input标签,点击查询后获取车次信息。
笔者在上一篇博客中提到:与起始地和目的地的标签对应的input标签有两个,其一是隐藏标签,内部存储的值是城市代号,无法直接使用send_keys()方法来选定起始地或者目的地,另外一个表示城市代号具体的文本信息,如下图:
因此,在上一篇博客中,笔者先给出手动点击的方案,然后等待加载到其后紧跟的input标签的值为期望数据时(完成显式等待),继续执行下一步代码。
所以,这里处理起始地和目的地的标签时,分为如下几个步骤:
- 定位所需的input标签;
- 根据key获取对应车站的代号;
- 通过execute_scripts()方法将设置车站代号到隐藏标签;
- 点击查询,出现车次列表。
示例代码:
def query_remian_ticket(self):
# 添加显式等待
WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.ID, 'fromStation'))
)
# 设置出发地
from_station_input = driver.find_element_by_id('fromStation')
from_station_code = self.station_code[self.from_station]
# 对from_station_input传递车站代号,类似于send_keys()
driver.execute_script('arguments[0].value="%s"' % from_station_code, from_station_input)
time.sleep(2)
# 设置目的地
to_station_input = driver.find_element_by_id('toStation')
to_station_code = self.station_code[self.to_station]
driver.execute_script('arguments[0].value="%s"' % to_station_code, to_station_input)
time.sleep(2)
# 出发日期
train_data_input = driver.find_element_by_id('train_date')
driver.execute_script('arguments[0].value="%s"' % self.train_date, train_data_input)
time.sleep(2)
# 执行查询
WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.ID, 'query_ticket'))
)
query_btn = driver.find_element_by_id('query_ticket')
query_btn.click()
运行结果:
注意点:
- 代码中采用
execute_script()
方法设置arguments[0].value='station code'
,向from/toStation标签传递了车站代号,其操作类似于send_keys()
方法,具体原理需要JS相关的知识,笔者在这就不拓展了; - 因12306一直在变化,登录后需要手动点击几个确认按钮,到达选票的界面,程序才能正常运行。
2.4 解析车次列表
代码需求:
- 获取可选的车次列表;
- 选择合适的车票,点击预订。
还是老套路,查看车次信息对应的标签,每个tbody下的tr标签对应一个车次,但是含有datatran属性的tr标签不是想要的,需要想办法将其过滤:
找到合适的车票后,需要点击预订,预订按钮对应的标签:
示例代码:
def order_ticket(self):
# 解析车次数据
# 添加显式等待,直到加载到想要的tr标签
WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.XPATH, '//tbody[@id="queryLeftTable"]/tr'))
)
# 查找想要的标签,返回的是selenium对象列表
train_trs = driver.find_elements_by_xpath('//tbody[@id="queryLeftTable"]/tr[not(@datatran)]')
is_search = False
for train_tr in train_trs:
# 利用selenium对象的text属性获得车次信息字符串,将字符串中的换行符替换成空格
train_msgs = train_tr.text.replace('\n', ' ')
# 将字符串按空格分隔成列表
train_msg_list = train_msgs.split(' ')
train_number = train_msg_list[0] # 获取车次
if train_number in self.expected_trains:
# 该车次为期望搭乘车次,则继续依次判断座位情况
for seat_key in self.seat_types.keys():
# 9-seat_type表示对应座位类型的索引值
seat_state = train_msg_list[9-seat_key]
if seat_state.isdigit() or seat_state == '有':
is_search = True
break
# 找到合适的票
if is_search:
print(f'找到合适的车票, 车次:{train_number}, 座位类型:{self.seat_types[seat_key]}')
# 点击预定
order_btn = train_tr.find_element_by_xpath('.//a[@class="btn72"]')
order_btn.click()
time.sleep(10)
break
if not is_search:
print('您期望的所有车次均无余票!')
运行结果:
注意点:
- 获取车次的tr标签时,要把含有
datatran
的tr标签过滤掉,语法是[not(@datatran)]
; - 得到车次信息的字符串后,替换其中的换行符为空格,并以空格分割后放到列表中;
- init方法里面初始化了一个用户车次,获得可选车次后做判断该车次是否在期望车次列表中;
- 若车次为期望车次,进一步判断座位情况,有合适的座位则点击预订。
2.5 确认乘客信息
代码需求:
- 选择买票的乘客;
- 切换席别,练习select标签操作。
乘客对应的标签:
座位席别对应的标签:
提交订单对应的标签:
最终确认按钮对应的标签:
示例代码:
def confirm_ticket_order(self):
# 添加显式等待,直到加载到对应的url
WebDriverWait(driver, 30).until(
EC.url_contains(self.passenger_url)
)
# 确保加载出乘客信息
WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.XPATH, '//ul[@id="normal_passenger_id"]/li/label'))
)
# 选择乘客
passenger_labels = driver.find_elements_by_xpath('//ul[@id="normal_passenger_id"]/li/label')
for passenger_label in passenger_labels:
name = passenger_label.text
if name in self.passengers:
# 如果在用户设定的乘客列表中
passenger_label.click()
# 确认购买的席别
seat_select = Select(driver.find_element_by_id('seatType_1'))
seat_types = self.seat_types.values()
for seat_type in seat_types:
try:
seat_select.select_by_value(seat_type)
except NoSuchElementException:
# 如果没有对应的席别,继续选下一种
continue
else:
# 如果有当前席别,则退出
break
# 提交订单
submit_btn = driver.find_element_by_id('submitOrder_id')
submit_btn.click()
# 添加显式等待,确保加载出弹出的最终确认页面
WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.CLASS_NAME, 'dhtmlx_window_active'))
)
运行结果:
注意点:
- 第一个显式等待是为了确认加载到指定的页面;
- 第二个显式等待是为了确保加载出乘客信息;
- try语句是为了防止所选的席别刚好没有,可以继续选下一种席别;
- 笔者只是为了演示,所以最终的确认按钮就没有点击。
3. 完整代码
从网站登录到最终确认订单的完整代码如下:
import csv
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
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import NoSuchElementException,ElementNotVisibleException
# 将驱动写成全局变量
driver = webdriver.Chrome()
class BuyTrainTicket(object):
# 定义类属性
login_url = 'https://kyfw.12306.cn/otn/resources/login.html'
personal_url = 'https://kyfw.12306.cn/otn/view/index.html'
remain_ticket_url = 'https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc'
passenger_url = 'https://kyfw.12306.cn/otn/confirmPassenger/initDc'
# 定义席别
seat_types = {0: 'O', 1: 'M', 2: 'P'}
def __init__(self, from_station, to_station, train_date, expected_trains, passengers):
self.from_station = from_station # 获取起始地
self.to_station = to_station # 获取目的地
self.train_date = train_date # 获取乘车日期
self.expected_trains = expected_trains # 用户想乘坐的车次
self.passengers = passengers
self.station_code = {}
self.init_station_codes() # 初始化各车站代号
def init_station_codes(self):
# 读取csv文件,获得车站代号
with open('station_code.csv', 'r', encoding='utf-8') as fobj:
reader_obj = csv.DictReader(fobj)
for cont in reader_obj:
name = cont['name']
code = cont['code']
self.station_code[name] = code
def login(self):
driver.get(self.login_url) # 访问登录界面
# 添加显式等待,直到加载到对应的url
WebDriverWait(driver, 30).until(
EC.url_contains(self.personal_url)
)
print('登录成功!')
def query_remian_ticket(self):
# 添加显式等待
WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.ID, 'fromStation'))
)
# 设置出发地
from_station_input = driver.find_element_by_id('fromStation')
from_station_code = self.station_code[self.from_station]
# 对from_station_input传递车站代号,类似于send_keys()
driver.execute_script('arguments[0].value="%s"' % from_station_code, from_station_input)
time.sleep(2)
# 设置目的地
to_station_input = driver.find_element_by_id('toStation')
to_station_code = self.station_code[self.to_station]
driver.execute_script('arguments[0].value="%s"' % to_station_code, to_station_input)
time.sleep(2)
# 出发日期
train_data_input = driver.find_element_by_id('train_date')
driver.execute_script('arguments[0].value="%s"' % self.train_date, train_data_input)
time.sleep(2)
# 执行查询
WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.ID, 'query_ticket'))
)
query_btn = driver.find_element_by_id('query_ticket')
query_btn.click()
def order_ticket(self):
# 解析车次数据
# 添加显式等待,直到加载到想要的tr标签
WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.XPATH, '//tbody[@id="queryLeftTable"]/tr'))
)
# 查找想要的标签,返回的是selenium对象列表
train_trs = driver.find_elements_by_xpath('//tbody[@id="queryLeftTable"]/tr[not(@datatran)]')
is_search = False
for train_tr in train_trs:
# 利用selenium对象的text属性获得车次信息字符串,将字符串中的换行符替换成空格
train_msgs = train_tr.text.replace('\n', ' ')
# 将字符串按空格分隔成列表
train_msg_list = train_msgs.split(' ')
train_number = train_msg_list[0] # 获取车次
if train_number in self.expected_trains:
# 该车次为期望搭乘车次,则继续依次判断座位情况
for seat_key in self.seat_types.keys():
# 9-seat_type表示对应座位类型的索引值
seat_state = train_msg_list[9-seat_key]
if seat_state.isdigit() or seat_state == '有':
is_search = True
break
# 找到合适的票
if is_search:
print(f'找到合适的车票, 车次:{train_number}, 座位类型:{self.seat_types[seat_key]}')
# 点击预定
order_btn = train_tr.find_element_by_xpath('.//a[@class="btn72"]')
order_btn.click()
break
if not is_search:
print('您期望的所有车次均无余票!')
def confirm_ticket_order(self):
# 添加显式等待,直到加载到对应的url
WebDriverWait(driver, 30).until(
EC.url_contains(self.passenger_url)
)
# 确保加载出乘客信息
WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.XPATH, '//ul[@id="normal_passenger_id"]/li/label'))
)
# 选择乘客
passenger_labels = driver.find_elements_by_xpath('//ul[@id="normal_passenger_id"]/li/label')
for passenger_label in passenger_labels:
name = passenger_label.text
if name in self.passengers:
# 如果在用户设定的乘客列表中
passenger_label.click()
# 确认购买的席别
seat_select = Select(driver.find_element_by_id('seatType_1'))
seat_types = self.seat_types.values()
for seat_type in seat_types:
try:
seat_select.select_by_value(seat_type)
except NoSuchElementException:
# 如果没有对应的席别,继续选下一种
continue
else:
# 如果有当前席别,则退出
break
# 提交订单
submit_btn = driver.find_element_by_id('submitOrder_id')
submit_btn.click()
# 添加显式等待,确保加载出弹出的最终确认页面
WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.CLASS_NAME, 'dhtmlx_window_active'))
)
# 点击最终确定按钮
'''
btn = driver.find_element_by_id('qr_submit_id')
while btn:
try:
btn.click()
btn = driver.find_element_by_id('qr_submit_id')
except ElementNotVisibleException:
# 如果抛出异常,表面上说明点击了不存在的按钮,反向证明已经成功点击确认按钮
break
'''
def run(self):
# 登录网站
self.login()
# 查询余票
self.query_remian_ticket()
# 预订车票
self.order_ticket()
# 确认乘客信息
self.confirm_ticket_order()
def main():
# 期望车次以字典形式传入
ticket_buyer = BuyTrainTicket('xxx', 'xxx', '2021-xx-xx', ['xxx'], 'xxx')
ticket_buyer.run()
if __name__ == '__main__':
main()