python + selenium实现12306全自动买票

news/2025/2/22 23:51:41

整个程序分了三个模块:购票模块(主体)、验证码处理模块、余票查询模块
使用方法:三个模块分别保存为三个python文件,名字分别为:book_ticket,captcha,check_ticket。

在购票模块里初始化相关参数:出发站、终点站、出发日期、自己的账号、密码,乘客姓名等,出发日期的格式:xxxx-xx-xx。

购票模块

python">from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import wait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import NoSuchElementException
import json
import time
from check_ticket import Check#余票查询模块
from captcha import Code#验证码处理模块
class Buy_Ticket():
    def __init__(self, start_station, end_station, date, username, password, purpose, names):
        self.num = 1
        self.start = start_station
        self.end = end_station
        self.date = date
        self.username = username
        self.password = password
        self.purpose = purpose
        self.all_names = names
        self.login_url = 'https://kyfw.12306.cn/otn/resources/login.html'
        self.ticket_url = 'https://kyfw.12306.cn/otn/leftTicket/init'
    #模拟登录函数,包括自动填充用户名、密码、自动点击验证、最终自动登录
    def login(self):
        browser.get(self.login_url)
        time.sleep(0.5)
        try:
            wait.WebDriverWait(browser, 5).until(EC.element_to_be_clickable((By.CLASS_NAME,'login-hd-account'))).click()
            input_name = browser.find_element_by_id('J-userName')
            input_pd = browser.find_element_by_id('J-password')
            input_name.send_keys(self.username)
            input_pd.send_keys(self.password)
            start_end, check = self.add_cookie()
            #time.sleep(5)
            c = Code(browser)       #调用验证码识别模块
            c.main()
            print('登录成功!')
            time.sleep(0.8)
            print(browser.current_url)
            #try:
                #wait.WebDriverWait(browser, 3).until(EC.element_to_be_clickable((By.CLASS_NAME,'btn.btn-primary.ok'))).click()
            #except NoSuchElementException:
                #pass
            self.check(start_end, check)
        except NoSuchElementException:
            print('没有找到元素')
            self.login()
            
    def add_cookie(self):
        check = Check(self.date, self.start, self.end, self.purpose)
        start_end = check.look_up_station()
        # cookie的添加,json.dumps把以汉字形式呈现的起始、终点站转化成unicode编码,可在审查元素里查看cookie
        browser.add_cookie({'name': '_jc_save_fromStation',
                            'value': json.dumps(self.start).strip('"').replace('\\', '%') + '%2C' + start_end[0]})
        browser.add_cookie({'name': '_jc_save_toStation',
                            'value': json.dumps(self.end).strip('"').replace('\\', '%') + '%2C' + start_end[1]})
        browser.add_cookie({'name': '_jc_save_fromDate', 'value': self.date})
        print('cookie添加完成!')
        return start_end, check
    #余票查询函数,获取预定车次信息
    def check(self, start_end, check):
        #调用余票查询模块
        browser.get(self.ticket_url)
        self.num = check.get_info(start_end, 1)
        button = wait.WebDriverWait(browser, 3).until(EC.element_to_be_clickable((By.ID,'query_ticket')))
        button.click()
        if self.purpose == '学生':
            browser.find_element_by_id('sf2').click()
            button.click()
    def check_date(self):
        #12306学生票的时间是:暑假:6月1日-9月30日,寒假:12月1日-3月31日
        date = ''.join(self.date.split('-'))
        #暑假
        if int(date[:4] + '0601') <= int(date) <= int(date[:4] + '0930'):
            return 1
        #当年寒假,也就是当年的1、2、3月
        if int(date[:4] + '0101') <= int(date) <= int(date[:4] + '0331'):
            return 1
        #这里处理的是从当年12月到第二年的3月,比如在2020-12-12买2021-01-18的学生票,那么就是在下面的处理区间
        next_year = str(int(date[:4]) + 1)
        if int(date[:4] + '1201') <= int(date) <= int(next_year + '0331'):
            return 1
        return 0
    #车票预定函数
    def book_ticket(self):
    time.sleep(1.5)
        print('开始预订车票...')
        #先查找出所有车次对应的预订按钮,再根据余票查询模块返回的车次序号,点击相应的预订按钮
        button = browser.find_elements_by_class_name('no-br')
        button[self.num-1].click()
        time.sleep(1.5)
        #选择乘车人
        #获取所有乘车人的信息
        passengers = browser.find_element_by_id('normal_passenger_id')
        names = passengers.text.split('\n')
        for name in self.all_names:
            index = names.index(name)
            browser.find_element_by_id('normalPassenger_' + str(index)).click()
            if '学生' in name:
                if self.check_date():
                    browser.find_element_by_id('dialog_xsertcj_ok').click()
                else:
                    print('当前日期不在学生票可购买时间区间!')
                    print('学生票乘车时间为暑假6月1日至9月30日、寒假12月1日至3月31日!')
                    browser.find_element_by_id('dialog_xsertcj_cancel').click()
        #browser.close()
        browser.find_element_by_id('submitOrder_id').click()
        wait.WebDriverWait(browser, 3).until(EC.element_to_be_clickable((By.ID,'qr_submit_id'))).click()
        print('车票预定成功!请在30分钟内完成付款!')
    def main(self):
        self.login()
        if self.num:
        self.book_ticket()
        else:
        browser.close()
        return
if __name__ == '__main__':
    begin = time.time()
    #隐藏浏览器
    options = webdriver.ChromeOptions()
    options.add_argument("--disable-blink-features=AutomationControlled")    #2020.03.28更新
    browser = webdriver.Chrome(options=options)
    browser.maximize_window()
    # Buy_Ticket类初始化参数,从左到右:出发站,终点站,出发日期,账号,密码,购票类型(默认购买成人票,若要购买学生票,
    # 添加乘客姓名时在后面加上(学生)),把要购买票的乘客姓名放在一个列表里
    b = Buy_Ticket('上海', '重庆', '2020-12-30', '账号', '密码', 'ADULT', ['乘客1姓名', '乘客2姓名(学生)'])
    b.main()
    end = time.time()
    print('总耗时:%d秒' % int(end-begin))
    #browser.close()

验证码处理模块

python">import time
import base64
import requests
import numpy as np
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.support import wait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
class Code():
    def __init__(self, browser):
        self.browser = browser
        self.verify_url = 'http://littlebigluo.qicp.net:47720/'     #验证码识别网址,返回识别结果
    #获取验证码图片
    def get_captcha(self):
        element = self.browser.find_element_by_class_name('imgCode')
        #time.sleep(0.5)
        img = base64.b64decode(element.get_attribute('src')[len('data:image/jpg;base64,'):])
        with open('captcha.png', 'wb') as f:
            f.write(img)
        #验证码解析
    def parse_img(self):
        pic_name = 'captcha.png'
        # 打开保存到本地的验证码图片
        files={'pic_xxfile':(pic_name,open(pic_name,'rb'),'image/png')}
        response = requests.post(self.verify_url, files=files)
        try:
            num = response.text.split('<B>')[1].split('<')[0]
        except IndexError:  #验证码没识别出来的情况
            print('验证码未能识别!重新识别验证码...')
            return
        try:
            if int(num):
                print('验证码识别成功!图片位置:%s' % num)
                return [int(num)]
        except ValueError:
            try:
                num = list(map(int,num.split()))
                print('验证码识别成功!图片位置:%s' % num)
                return num
            except ValueError:
                print('验证码未能识别')
                return
        #识别结果num都以列表形式返回,方便后续验证码的点击
        #还有可能验证码没能识别出来
        #实现验证码自动点击
    def move(self):
        num = self.parse_img()
        if num:
            try:
                element = self.browser.find_element_by_class_name('loginImg')
                for i in num:
                    if i <= 4:
                        ActionChains(self.browser).move_to_element_with_offset(element,40+72*(i-1),73).click().perform()
                    else :
                        i -= 4
                        ActionChains(self.browser).move_to_element_with_offset(element,40+72*(i-1),145).click().perform()
                self.browser.find_element_by_class_name('login-btn').click()
                self.slider()
            except:
                print('元素不可选!')
        else:
            self.browser.find_element_by_class_name('lgcode-refresh').click()  #刷新验证码
            time.sleep(1.5)
            self.main()
    def ease_out_quart(self, x):
        return 1 - pow(1 - x, 4)
    #生成滑动轨迹
    def get_tracks(self, distance, seconds, ease_func):
        tracks = [0]
        offsets = [0]
        for t in np.arange(0.0, seconds, 0.1):
            ease = ease_func
            offset = round(ease(t / seconds) * distance)
            tracks.append(offset - offsets[-1])
            offsets.append(offset)
        return tracks
    #处理滑动验证码
    def slider(self):
        print('开始处理滑动验证码...')
        #track = self.get_tracks(305, 1, self.ease_out_quart)
        track = [30, 50, 90, 140]  #滑动轨迹可随意,只要距离大于300
        try:
            slider = wait.WebDriverWait(self.browser, 5).until(
                EC.presence_of_element_located((By.CLASS_NAME, 'nc_iconfont.btn_slide')))
            ActionChains(self.browser).click_and_hold(slider).perform()
            for i in track:
                ActionChains(self.browser).move_by_offset(xoffset=i, yoffset=0).perform()
        except:
            print('验证码识别错误!等待验证码刷新,重新识别验证码...')
            time.sleep(2.1)  #验证码刷新需要2秒
            self.main()
    def main(self):
        self.get_captcha()
        self.move()

余票查询模块

python">import requests
from urllib.parse import urlencode
import time
import json
class Check():
    def __init__(self, date, start, end, purpose):
        self.base_url = 'https://kyfw.12306.cn/otn/leftTicket/query?'
        self.url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9018'
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36',
            'Cookie': 'JSESSIONID=B709F9775E72BDED99B2EEBB8CA7FBB9; BIGipServerotn=1910046986.24610.0000; RAIL_EXPIRATION=1579188884851; RAIL_DEVIC'
            }
        self.date = date
        self.start_station = start
        self.end_station = end
        self.purpose = purpose if purpose == 'ADULT' else '0X00'
        #查找出车站的英文简称,用于构造cookie、完整的余票查询链接
    def look_up_station(self):
    try:
            with open('station_code.json', 'r') as f:
                dic = json.load(f)
        except FileNotFoundError:
            response = requests.get(self.url).text
            station = response.split('@')[1:]
            dic = {}
            for each in station:
                i = each.split('|')
                dic[i[1]] = i[2]
            with open('station_code.json', 'w') as f:
                f.write(json.dumps(dic))
        return [dic[self.start_station], dic[self.end_station]]
    def get_info(self, start_end, check_count):
        #构造请求参数
        data = {
        'leftTicketDTO.train_date': self.date,
        'leftTicketDTO.from_station': start_end[0],
        'leftTicketDTO.to_station': start_end[1],
        'purpose_codes': self.purpose
        }
        url = self.base_url + urlencode(data)
        print('完整余票查询链接:', url)
        count = 0  # 用于对车次编号
        while count == 0:
            print('余票查询中...  %d次' % check_count)
            response = requests.get(url, headers=self.headers)
            #print(response.text)
            try:
            json = response.json()
            except ValueError:
            print('余票查询链接有误,请仔细检查!')
            return
            maps = json['data']['map']
            for each in json['data']['result']:
            count += 1
            s = each.split('|')[3:]
                info = {
                'train':s[0],
                'start_end':maps[s[3]] + '-' + maps[s[4]],
                'time':s[5] + '-' + s[6],
                '历时':s[7],
                '一等座':s[28],
                '二等座':s[27]
                }
                try:
                    #余票的结果有3种:有、一个具体的数字(如:18、6等)、无,判断如果余票是有或者一个具体的数字就直接输出对应的车次信息,然后返回
                    if info['二等座'] == '有' or int(info['二等座']):
                        print('预定车次信息如下:')
                        print('[%d]' % count, info)
                        return count
                except ValueError:
                    continue
            count = 0
            check_count += 1
            time.sleep(0.8)

测试结果

在这里插入图片描述

现在我邀请你进入我们的软件测试学习交流群:721945856 , 大家可以一起探讨交流软件测试,共同学习软件测试技术、面试等软件测试方方面面,还会有免费直播课,收获更多测试技巧,我们一起进阶Python自动化测试/测试开发,走向高薪之路。

喜欢软件测试的小伙伴们,如果我的博客对你有帮助、如果你喜欢我的博客内容,请 “点赞” “评论” “收藏” 一键三连哦!

软件测试工程师自学教程和源代码:

在这里插入图片描述

在这里插入图片描述


http://www.niftyadmin.cn/n/1325771.html

相关文章

一位3年经验的测试工程师水平能差到什么程度?面试后,感叹都是人才呀...

起因 老板觉得现在公司部门里都是男的&#xff0c;缺少一点阴柔之气&#xff0c;想平衡一下&#xff0c;正巧当时互金公司倒了一大批&#xff0c;大批简历投到公司&#xff0c;老板以为自己也是技术出身&#xff0c;就想着要招了一个三年工作经验的女测试员&#xff0c;要价倒…

linux硬盘挂载,linux硬盘挂载

1、首先&#xff0c;我们聊下硬盘。我们都知道硬盘是用来存储文件的&#xff0c;当然配合着文件系统(文件系统管理着对应的硬件资源)。硬盘由多个磁盘组成&#xff0c;每个磁盘有两面&#xff0c;每面有一个磁头&#xff0c;磁盘上有多个磁道。硬盘被分为多个扇区&#xff0c;当…

ping程序设计 linux,linux下ping编程

先来说说ping程序的原理吧&#xff0c;其实挺简单&#xff0c;就是一个主机系统向另外一个主机系统说&#xff1a;I love you(ICMP报文)&#xff0c;然后那个主机如果相信你或者说想和你通信&#xff0c;和你心知心&#xff0c;那它就把收到的I love you(ICMP)报文原样返回.好嘛…

面了12家软件公司测试岗位,高频面试题大盘点,刷完谁都留不住我跳槽的心

全网首发-涵盖16个技术栈 第一部分&#xff0c;测试理论&#xff08;测试基础需求分析测试模型测试计划测试策略测试案例等等&#xff09; 第二部分&#xff0c;Linux&#xff08; Linux基础Linux练习题&#xff09; 第三部分&#xff0c;MySQL&#xff08;基础知识查询练习…

kill函数的linux,linux ps命令、kill命令及kill函数概述

导致系统资源无法正常释放&#xff0c;一般不推荐使用&#xff0c;除非其他办法都无效。当使用此命令时&#xff0c;一定要通过ps -ef确认没有剩下任何僵尸进程。只能通过终止父进程来消除僵尸进程。如果僵尸进程被init收养&#xff0c;问题就比较严重了。杀死init进程意味着关…

软件测试面试怎样介绍自己的项目?会问到什么程度?

最近收到很多粉丝的私信说找不到工作&#xff0c;简历投了百十来份&#xff0c;邀约都没几个&#xff0c;更别说offer了&#xff0c;是不是软件测试要黄了&#xff1f; 说句实话&#xff0c;现在大环境确实不好&#xff0c;互联网大厂裁员这是摆在明面上的原因。时代的一粒沙&…

linux添加安全证书,Linux下Nginx安全证书ssl配置方法

分享下我是如何一步步在Nginx上配置SSL的。首先&#xff0c;确保安装了OpenSSL库&#xff0c;并且安装Nginx时使用了–with-http_ssl_module参数。初学者或者菜鸟建议使用LNMP进行一键安装。生成证书&#xff1a;进入要生成证书的目录cd /usr/local/nginx/conf使用openssl创建创…

软件开发毕业4年后,靠自学自动化测试月入2W,本人亲身经历供大家参考

大专毕业4年了&#xff0c;靠自学自动化测试月入2万&#xff0c;本人亲身经历供大家参考。毕业院校&#xff1a;湖南科技职业学院软件开发与应用专业。学校课程很杂&#xff0c;都只教点皮毛&#xff0c;是真正的师傅领进门&#xff0c;修行在个人&#xff0c;在学校我也不是优…