12306案例
学习目标
通过案例复习selenium的知识点,通过selenium抓取Ajax数据,使用Ajax技术,打开页面的时候不会完全显示内容,通过按钮操作后网页不会全部更新,实现部分界面的增量数据更新。只更新数据不刷新整个界面。
需求
人工操作12306的购票流程,让程序按照人操作的流程去模拟操作,这里登录界面需要扫码,先不处理验证框的问题。
步骤
第一步,登录;第二步,车次及余票的查询(点击查询按钮),进入购票界面;第三步,解析车次列表数据,人为的选择是通过肉眼判断的,要通过程序实现对车次列表的解析;第四步,确认乘客信息和席别;第五步,核对信息。
第一步登录
1.用类来实现12306的操作,用面向对象的方式搭建好整体的框架,通过实例化对象的方式把需要初始化的变量传递进去,实现部分函数的功能,登录成功之后,用显示等待去判断。
2.创建run函数用来调用使用的函数,先创建实现登录的函数,这里需要扫码进入(手动),之后判断登录成功后是否显示个人信息的界面,如果显示就是登录成功。
第二步车次以及余票的查询
实现车次列表的查询
创建查票的函数,用驱动加载网站,用隐式等待暂停2秒,点击确定按钮,关闭弹窗。人为的操作是输入出发地、目的地和出发日,再点击查询,现在要浏览器模仿人的操作执行代码。在出发站输入框点右键,检查,光标定位到第二个input标签,但是在第一个input标签的value值中有出发地的代号,目的地同样存放的是城市的代号。我们选择出发地和目的地的时候需要把input里面的value值改成相应站台的代号。
需要把获得的代号文件导入进去,通过站台获得相应的代号。
得到站台相对应代号的字典
import csv
with open('stations.csv', 'r',encoding='utf-8') as f:
read = csv.DictReader(f) # 读出是两对值
stations_dict = {} # 组成一对字典
for line in read:
# print(line)
name = line['name']
code = line['code']
stations_dict[name] = code
print(stations_dict)
得到的字典在后续的函数中需要使用的,要把传入的站台转换为代号进行程序的执行,需要字典查询对应的代号。要在初始化的函数中定义字典,运行获得站台字典的函数,需要提前定义字典的变量,self.stations_dict = {},这样的话就可以在任何函数中都能使用,跨函数进行调用数据。
通过站台得到代号之后,要传入到value中。在查票函数中进行操作,先定位input标签,把输入的站台转换为代号,在12306程序内部只接收代号。这时运行会发现出发地和目的地输入框里没有内容,但是在网页空白处点右键,用元素选择器查看,每个框里是有相应的代码的。但是用右键进行检查发现代码中的value值又没了,那是因为右键进行检查的时候,对数据进行了刷新,数据又消失了,这是中途做的测试,不影响。
日期的value默认值永远都是那一天,不用管。把传入的日期放入value中。
出发地、目的地、日期输入完成之后,点击查询,进行车次的检索。
总结:
1 进入了查票的网页,需要等待页面加载几秒,然后处理弹窗
2 填入出发地、目的地和日期三个数据
- 定位到输入框
- 拿到站台的代号(日期是没有的 但是日期需要注意格式)
- 把站台代号填入标签的value中
3 点击查询按钮
这个步骤通过代码完成了车次及余票的查询
第三步解析车次列表的数据
页面分析
用selenium爬取就不用到网页源码中查看了,直接右键检查就可以了。找到任意一个车次,点右键,检查,定位到<a标签,里面的文本是车次,依次往上折叠标签,会发现需要的数据在<tr标签内,一个tr包含了一趟车次的所有数据,所有的tr都放到tbody标签里,每两个tr中都有一个无效的tr(光标放进去没有数据的指向)。
通过分析一个tr标签中存放了一趟车次的数据,所有的tr都存放于tbody标签中,其中夹杂了无效的tr标签,需要把无效的标签进行过滤。点击查询按钮会加载出来车次列表的数据,但是不知道是否完全加载出来。可以用显示等待,如果满足设定的条件,就说明车次列表数据是加载出来的。
无效的tr标签里有datatran属性,遍历取出的tr标签的时候可以根据这个属性过滤到无用的tr,对得到的tr进行遍历取值的时候,会发现车票数据里有的车次里多了“复”字,我们可以看到车次的信息是用"\n"进行分割,而有无票的数据是用空格分割的,我们可以先把"\n"换成空格,再用split(""),用空格进行切割,最后判断如果第二个字符为复,删掉"复"字。
车次是在分割后列表的第一个元素,用索引0取出,然后判断遍历取出的车次是否在传入的车次中,如果在的,就用字典把车次对应的坐席类型取出。先判断二等座,二等座在分割后列表的第十个,索引为9。如果取出的坐席是二等座,代码为O,就要判断车次和二等座交叉的位置是“有”或者数字就代表有票,否则是无票,如果有票的话进行点击操作;否则去判断一等座的情况。
如果一等座,二等座都有票的情况下,会出现先去选中二等座,点击预定按钮;之后再判断一等座的情况,再去点击预定。此时二等座点击完后页面会跳转,一等座再去点击预定的时候会找不到按钮,此时会报错。这时可以在解析完座位后用try语句。
代码实现
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
import csv
"""用面向对象的方式搭建好整体的框架,通过实例化对象的方式把需要初始化的变量传递进去,实现部分函数的功能
登录成功之后,用显示等待去判断"""
class TrainSpider():
"""定义初始化方法 用类初始化对象,在实例化对象的时候把三个参数传进去,否则创建不成功"""
def __init__(self, from_station, to_station, train_date, ticket_info):
"""
:param from_station: 出发地
:param to_station: 目的地
:param train_date: 出发日期
:param ticket_info: 车次以及坐席,9是特等坐,O是二等座,M是一等座
{"G534":['O','M']},需要传到main里的参数里
"""
self.from_station = from_station
self.to_station = to_station
self.train_date = train_date
self.ticket_info = ticket_info
self.driver = webdriver.Edge()
# 登录的url
self.logic_url = 'https://kyfw.12306.cn/otn/resources/login.html'
# 个人界面的url
self.person_url = 'https://kyfw.12306.cn/otn/view/index.html'
# 查票的url
self.query_url = 'https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc'
# 存放字典和代号,可以跨函数调用,相当于全局,有站台名称可以随时调用
self.stations_dict = {}
# 初始化stations_dict,调用stations_code函数
self.stations_code()
# 得到站台以及对应代号的字典
def stations_code(self):
with open('stations.csv', 'r', encoding='utf-8') as f:
read = csv.DictReader(f) # 读出是两对值
# stations_dict = {} # 组成一对字典
for line in read:
# print(line)
name = line['name']
code = line['code']
self.stations_dict[name] = code
# self.stations_dict[line['name']] = line['code']
# print(self.stations_dict)
# 实现登录
def logic(self):
self.driver.get(self.logic_url)
"""用显示等待判断是否登录成功,传入驱动和最大等待时间,
until()里设置等待的条件,跳转的界面里是否包含self.person_url里的地址 """
WebDriverWait(self.driver, 1000).until(
EC.url_contains(self.person_url)
) # 如果包含就显示登录成功。
print("登录成功")
# 查票
def query_ticket(self):
self.driver.get(self.query_url)
# 用隐式等待2秒
self.driver.implicitly_wait(2)
# 关闭弹窗,点击弹窗的确定
self.driver.find_element_by_id('qd_closeDefaultWarningWindowDialog_id').click()
# 定位出发地输入框,要对value值进行改变,定位到有value值的input标签
from_station_input = self.driver.find_element_by_id('fromStation')
# 把输入的站台名转换成代号
from_station_code = self.stations_dict[self.from_station]
# 用js把代号传入value中,定位到input标签,然后往里输入转换后的代码值
# self.driver.execute_script('arguments[0].value="%s"' % from_station_code, from_station_input)
# 输入目的地,定位到目的地的输入框
to_station_input = self.driver.find_element_by_id('toStation')
# 输入的目的地的站台名转换为代号
to_station_code = self.stations_dict[self.to_station]
# 用js代码传入目的地的代号
# self.driver.execute_script('arguments[0].value="%s"' % to_station_code, to_station_input)
# 这时运行代码,出发地和目的地输入框里没有内容,但是在网页空白处点右键,用元素选择器查看,每个框里是有相应的代码的
# 用id定位日期的输入标签,
date_input = self.driver.find_element_by_id('train_date')
# 用js代码传入日期
# self.driver.execute_script('arguments[0].value="%s"' % self.train_date, date_input)
# 以上三个框的输入也可以用一行代码实现,把每一个js传入的代码都注释掉,
self.driver.execute_script('arguments[0].value="{}"; arguments[1].value="{}"; arguments[2].value="{}"'.format(from_station_code, to_station_code, self.train_date),from_station_input, to_station_input, date_input)
# 定位到查询按钮,并进行点击
self.driver.find_element_by_id('query_ticket').click()
# 第三步 解析车次列表数据
# 显示等待,等待车次列表数据加载完成
WebDriverWait(self.driver, 1000).until(
EC.presence_of_element_located((By.XPATH, './/tbody[@id="queryLeftTable"]/tr'))
)
# 获取车次列表
# 过滤掉无效的tr标签
train_trs = self.driver.find_elements_by_xpath('.//tbody[@id="queryLeftTable"]/tr[not(@datatran)]')
try:
for train_tr in train_trs:
# print(train_tr.text)
# print('*'*50)
train_infos = train_tr.text.replace('\n', " ").split(" ")
# 把“复”字从列表中删除
if train_infos[1] == "复":
# remove把指定元素从列表中删除
train_infos.remove("复")
train = train_infos[0] # 从网页中爬取下来的车次,在init中实例化车次
# 判断一下爬取下来的车次是否在传入的初始化车次中
# train 是取到的车次,判断一下取到的一个个车次是否在传入的车次中
if train in self.ticket_info:
# 如果在,就用传入字典格式的车次,去取后面的坐席
# 传入的{"G345": ['O', 'M']},先取O,再取M。
seat_types = self.ticket_info[train]
# 从seat_types里遍历出列出的车票类型,O,M
for seat_type in seat_types:
# 如果seat_type等于O,判断二等座是否有票
# 检查二等座对应的位置是否有余票
if seat_type == 'O':
# 在相应车次,二等奖交叉点处点右键,发现有票的话是“有”或者数字
# 从train_infos 的列表中可以看到,需要的信息在列表中第10个,索引是9
o_count = train_infos[9]
# 判断二等票的位置的内容是 “有”或者是数字
if o_count == "有" or o_count.isdigit():
# 如果满足,就点击预定按钮
# 光标在预定处点击右键,发现定位到tr标签下的a标签
train_tr.find_element_by_xpath('.//a[@class="btn72"]').click()
# 点击完预定后要退出,否则程序还会对一等座进行判断,如果一等座也有,会再次进行点击操作
# 二等座已经点击了,一等座在点击的时候页面已经跳转就找不到点击按钮,就会报错
break
elif seat_type == 'M':
# 如果要买一等座,就查看一等座的情况
# 找到下标索引为8的,为一等座
m_count = train_infos[8]
# 判断一等票的位置的内容是 “有”或者是数字
if m_count == "有" or m_count.isdigit():
# 如果满足,就点击预定按钮
# 光标在预定处点击右键,发现定位到tr标签下的a标签
train_tr.find_element_by_xpath('.//a[@class="btn72"]').click()
except:
pass
# 实现函数的调用
def run(self):
# 第一步 登录
self.logic() # 调用函数
# 第二步 车次及余票的查询
self.query_ticket()
if __name__ == '__main__':
# 通过创建的类去实例化对象,需要传递三个参数到init里
# 注意日期的格式 yyyy-xx-xx
spider = TrainSpider('长沙', '北京', '2021-09-15', {"G534": ['O', 'M']})
spider.run()
有时会出现程序运行完成之后,网页自动关闭的情况。这涉及到垃圾回收机制,是因为用驱动打开浏览器是在类里面进行的操作,类运行结束后,相应的驱动也会结束,驱动随着类的消亡也消亡,这时候可以把驱动放到外面就可以了。
第四步确认乘客信息和席别
程序跳转到确认乘客信息和席别的url:https://kyfw.12306.cn/otn/confirmPassenger/initDc
这时我们是在同一个窗口进行的操作,会覆盖之前的浏览器地址。
这时的列车信息就不需要我们去操作了,是按照之前选择的内容进行选择的,乘客信息里面的内容需要进行点击操作,在这里点击姓名或者姓名前面的小框都可以选中乘客。需要进行的操作为:
1,确认乘客信息。可能会有多个人,以列表的形式在实例化对象的时候传进去。
用显示等待,看是否出现确认信息的网页,如果出现就等待成功。光标在乘客姓名处点右键,发现乘客信息是在li标签下的label里,如果有多个乘客会分别出现在多个li标签里面。li标签都在ul标签内,我们要先定位到ul标签,然后在里面找到li标签的文本值。这里找到的乘车人有多个,所以要在element后加个s,返回的是所有乘车人的列表。遍历取出每一个乘车人,看是否在实例化对象时候传入的乘车人姓名列表中,如果在,就点击乘车人的标签进行选中。
2,确认坐席。需要用selenium操作下拉菜单。在席别下拉框中点击,进行选中操作,点右键检查,定位到select标签,操作之前需要导入select类。
利用id定位到席别选择框,导入select类,把定位到的选择框传入select类中进行操作,这里可以看到O代表二等座,M代表一等座,9代表商务座,这时只需要把每个座位的value值传入到select中进行选择即可。
3,点击提交订单按钮。
第五步 核对信息
用显示等待进行判断,如果出现核对信息框,就进行点击。
完整的代码实现如下:
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 # 第四步确认坐席时使用
import time
import csv
"""用面向对象的方式搭建好整体的框架,通过实例化对象的方式把需要初始化的变量传递进去,实现部分函数的功能
登录成功之后,用显示等待去判断"""
class TrainSpider():
"""定义初始化方法 用类初始化对象,在实例化对象的时候把三个参数传进去,否则创建不成功"""
def __init__(self, from_station, to_station, train_date, ticket_info, passengers):
"""
:param from_station: 出发地
:param to_station: 目的地
:param train_date: 出发日期
:param ticket_info: 车次以及坐席,9是特等坐,O是二等座,M是一等座
{"G534":['O','M']},需要传到main里的参数里
:param passengers: 第四步添加的,需要乘车的人
"""
self.from_station = from_station
self.to_station = to_station
self.train_date = train_date
self.ticket_info = ticket_info
self.passengers = passengers
self.driver = webdriver.Edge()
# 第一步登录的url
self.logic_url = 'https://kyfw.12306.cn/otn/resources/login.html'
# 第一步个人界面的url
self.person_url = 'https://kyfw.12306.cn/otn/view/index.html'
# 第二步查票的url
self.query_url = 'https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc'
# 第四步确认信息的url
self.confirm_url = 'https://kyfw.12306.cn/otn/confirmPassenger/initDc'
# 存放字典和代号,可以跨函数调用,相当于全局,有站台名称可以随时调用
self.stations_dict = {}
# 初始化stations_dict,调用stations_code函数
self.stations_code()
# 定义选择信息为空,在第四步席别选择框中使用,先定义为空值,把第三步得到的seat_type赋值给它
self.select_info = None
# 得到站台以及对应代号的字典
def stations_code(self):
with open('stations.csv', 'r', encoding='utf-8') as f:
read = csv.DictReader(f) # 读出是两对值
# stations_dict = {} # 组成一对字典
for line in read:
# print(line)
name = line['name']
code = line['code']
self.stations_dict[name] = code
# self.stations_dict[line['name']] = line['code']
# print(self.stations_dict)
# 实现登录
def logic(self):
self.driver.get(self.logic_url)
"""用显示等待判断是否登录成功,传入驱动和最大等待时间,
until()里设置等待的条件,跳转的界面里是否包含self.person_url里的地址 """
WebDriverWait(self.driver, 1000).until(
EC.url_contains(self.person_url)
) # 如果包含就显示登录成功。
print("登录成功")
# 查票
def query_ticket(self):
self.driver.get(self.query_url)
# 用隐式等待2秒
self.driver.implicitly_wait(2)
# 关闭弹窗,点击弹窗的确定
self.driver.find_element_by_id('qd_closeDefaultWarningWindowDialog_id').click()
# 定位出发地输入框,要对value值进行改变,定位到有value值的input标签
from_station_input = self.driver.find_element_by_id('fromStation')
# 把输入的站台名转换成代号
from_station_code = self.stations_dict[self.from_station]
# 用js把代号传入value中,定位到input标签,然后往里输入转换后的代码值
# self.driver.execute_script('arguments[0].value="%s"' % from_station_code, from_station_input)
# 输入目的地,定位到目的地的输入框
to_station_input = self.driver.find_element_by_id('toStation')
# 输入的目的地的站台名转换为代号
to_station_code = self.stations_dict[self.to_station]
# 用js代码传入目的地的代号
# self.driver.execute_script('arguments[0].value="%s"' % to_station_code, to_station_input)
# 这时运行代码,出发地和目的地输入框里没有内容,但是在网页空白处点右键,用元素选择器查看,每个框里是有相应的代码的
# 用id定位日期的输入标签,
date_input = self.driver.find_element_by_id('train_date')
# 用js代码传入日期
# self.driver.execute_script('arguments[0].value="%s"' % self.train_date, date_input)
# 以上三个框的输入也可以用一行代码实现,把每一个js传入的代码都注释掉,
self.driver.execute_script('arguments[0].value="{}"; arguments[1].value="{}"; arguments[2].value="{}"'.format(from_station_code, to_station_code, self.train_date),from_station_input, to_station_input, date_input)
# 定位到查询按钮,并进行点击
self.driver.find_element_by_id('query_ticket').click()
# 第三步 解析车次列表数据
# 显示等待,等待车次列表数据加载完成
WebDriverWait(self.driver, 1000).until(
EC.presence_of_element_located((By.XPATH, './/tbody[@id="queryLeftTable"]/tr'))
)
# 获取车次列表
# 过滤掉无效的tr标签
train_trs = self.driver.find_elements_by_xpath('.//tbody[@id="queryLeftTable"]/tr[not(@datatran)]')
# 定义标志位,用来判断是否找到用户需要购买的车次和坐席,如果找到就改为True
# False的默认值为0
flag = False
for train_tr in train_trs:
# print(train_tr.text)
# print('*'*50)
train_infos = train_tr.text.replace('\n', " ").split(" ")
# 把“复”字从列表中删除
if train_infos[1] == "复":
# remove把指定元素从列表中删除
train_infos.remove("复")
train = train_infos[0] # 从网页中爬取下来的车次,在init中实例化车次
# 判断一下爬取下来的车次是否在传入的初始化车次中
# train 是取到的车次,判断一下取到的一个个车次是否在传入的车次中
if train in self.ticket_info:
# 如果在,就用传入字典格式的车次,去取后面的坐席
# 传入的{"G345": ['O', 'M']},先取O,再取M。
seat_types = self.ticket_info[train]
# 从seat_types里遍历出列出的车票类型,O,M
for seat_type in seat_types:
# 如果seat_type等于O,判断二等座是否有票
# 检查二等座对应的位置是否有余票
if seat_type == 'O':
# 在相应车次,二等奖交叉点处点右键,发现有票的话是“有”或者数字
# 从train_infos 的列表中可以看到,需要的信息在列表中第10个,索引是9
o_count = train_infos[9]
# 判断二等票的位置的内容是 “有”或者是数字
if o_count == "有" or o_count.isdigit():
# 如果满足,就点击预定按钮
# 光标在预定处点击右键,发现定位到tr标签下的a标签
# train_tr.find_element_by_xpath('.//a[@class="btn72"]').click()
# # 点击完预定后要退出,否则程序还会对一等座进行判断,如果一等座也有,会再次进行点击操作
# # 二等座已经点击了,一等座在点击的时候页面已经跳转就找不到点击按钮,就会报错
self.select_info = seat_type
flag = True # 如果找到了,就把标志位改为True
break # 退出判断坐席有无的操作
elif seat_type == 'M':
# 如果要买一等座,就查看一等座的情况
# 找到下标索引为8的,为一等座
m_count = train_infos[8]
# 判断一等票的位置的内容是 “有”或者是数字
if m_count == "有" or m_count.isdigit():
# 如果满足,就点击预定按钮
# 光标在预定处点击右键,发现定位到tr标签下的a标签
# train_tr.find_element_by_xpath('.//a[@class="btn72"]').click()
self.select_info = seat_type
flag = True # 如果找到了,就把标志位改为True
break
if flag: # 如果找到了就进行点击
train_tr.find_element_by_xpath('.//a[@class="btn72"]').click()
break # 退出选坐席的操作
# 确认乘客信息
def confirm_passengers(self):
# 显示等待,判断是否成功跳转到确认乘客信息界面
# 如果跳转到confirm_url,则说明页面跳转成功。
WebDriverWait(self.driver, 1000).until(
EC.url_contains(self.confirm_url)
)
# 确认乘客信息,乘客信息是在li标签下的label里,所有的li都在ul标签里
# 返回多个内容,改为elements,返回的是个列表
passenger_labels = self.driver.find_elements_by_xpath('.//ul[@id="normal_passenger_id"]/li/label')
# 这里的乘车人要跟传进来的乘车人进行对比,需要在实例化对象的时候传入乘车人,在初始化的时候接收参数
for passenger in passenger_labels:
name = passenger.text # 定位到乘车人标签之后,遍历取出乘车人的文本
if name in self.passengers:
passenger.click() # 如果找到的乘车人在传入的参数中,进行点击
# 确认坐席
# 定位到席别选项框
seat_tag = self.driver.find_element_by_id('seatType_1')
# 把定位到的选择框座位参数传入到Select类里才能进行操作
seat_select = Select(seat_tag)
# 可以利用上个函数里 选中二等座或者一等座的结果放到select里传参进行选择
# 这时就需要定义个全局的变量select_info,把找到的seat_type赋值给select_info
seat_select.select_by_value(self.select_info)
# 第一种方法
# 点击提交订单按钮,通过id定位
self.driver.find_element_by_id('submitOrder_id').click()
# 第二种方法:
# 在119和121行的seat_types,改为self.select_info,135和147行注释掉
# 下面的语句中,比如找到了二等座
# for seat in self.select_info: # 遍历出列表中的坐席
# try:
# seat_select.select_by_value(seat) # 对每一个坐席进行选择
# # 如果没问题就进行选择,如果找不到就继续遍历下一个坐席
# except:
# continue
# else:
# break
# # 如果没问题就执行try和else里的语句,如果找的二等座有异常,就去寻找一等座,找到合适的坐席就退出
# # 相当于又去寻找了一次实例化传入列表的坐席
# 第五步 核对信息
# 用显示等待 判断核对信息框是否出现,传入核对框的ID。
WebDriverWait(self.driver, 1000).until(
EC.presence_of_element_located((By.ID, 'content_checkticketinfo_id'))
)
# 如果消息框出现,点击确认按钮
# time.sleep(2)
self.driver.find_element_by_id('qr_submit_id').click()
"""功能模块的使用,封装基本的功能,如果把登录,查询,确认的内容都写在一个函数里,
就会特别臃肿,可以封装不同的函数实现不同的功能,在一个函数中进行调用"""
# 实现函数的调用
def run(self):
# 第一步 登录
self.logic() # 调用函数
# 第二步 车次及余票的查询,第三步 车票数据解析
self.query_ticket()
# 第四步 确认乘客信息和席别
self.confirm_passengers()
if __name__ == '__main__':
# 通过创建的类去实例化对象,需要传递三个参数到init里
# 注意日期的格式 yyyy-xx-xx
spider = TrainSpider('长沙', '北京', '2021-09-15', {"G534": ['O', 'M']}, ['乘客姓名1', '乘客姓名2'])
spider.run()