Python爬虫学习笔记-第十二+十三课(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()

注意点:

  1. driver驱动不能写在init方法中,原因在于若将驱动写成实例对象的属性,当类对象被销毁,驱动也跟着销毁;
  2. 初始化的时候,读取了csv文件来获取车站代号,以便后续使用;
  3. 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('登录成功!')

运行结果:
在这里插入图片描述

注意点:

  1. 账号密码登录时,需添加显式等待,直到二维码图片加载完成,才能点击切换按钮;
  2. 12306网站加载时间比较长,不确定什么时候登陆成功(网页加载完成),所以需要添加显式等待。

2.3 车次以及余票查询

代码需求:

  1. 由个人详情页面跳转到了车次查询界面;
  2. 将车站代号存储到字典中,通过key来找到代号value;
  3. 设置与起始地、目的地、出发日期对应的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()

运行结果:
在这里插入图片描述

注意点:

  1. 代码中采用execute_script()方法设置arguments[0].value='station code',向from/toStation标签传递了车站代号,其操作类似于send_keys()方法,具体原理需要JS相关的知识,笔者在这就不拓展了;
  2. 因12306一直在变化,登录后需要手动点击几个确认按钮,到达选票的界面,程序才能正常运行。

2.4 解析车次列表

代码需求:

  1. 获取可选的车次列表;
  2. 选择合适的车票,点击预订。

还是老套路,查看车次信息对应的标签,每个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('您期望的所有车次均无余票!')

运行结果:
在这里插入图片描述
注意点:

  1. 获取车次的tr标签时,要把含有datatran的tr标签过滤掉,语法是[not(@datatran)]
  2. 得到车次信息的字符串后,替换其中的换行符为空格,并以空格分割后放到列表中;
  3. init方法里面初始化了一个用户车次,获得可选车次后做判断该车次是否在期望车次列表中;
  4. 若车次为期望车次,进一步判断座位情况,有合适的座位则点击预订。

2.5 确认乘客信息

代码需求:

  1. 选择买票的乘客;
  2. 切换席别,练习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'))
  )

运行结果:
在这里插入图片描述
在这里插入图片描述

注意点:

  1. 第一个显式等待是为了确认加载到指定的页面;
  2. 第二个显式等待是为了确保加载出乘客信息;
  3. try语句是为了防止所选的席别刚好没有,可以继续选下一种席别;
  4. 笔者只是为了演示,所以最终的确认按钮就没有点击。

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()
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值