【Python】利用selenium实现自动抢票(ticket星球网页版)

【Python】利用selenium实现自动抢票(票星球网页版)

  • 前言
  • selenium简单介绍
  • 抢票流程分析
  • 一、进入网页
  • 二、 选择观演日期和票价
  • 三、选择观演人数和观演人
  • 四、参数设置
  • 程序具体执行
  • 一、连接浏览器
  • 二、选择场次日期和价位
  • 三、选择观演人及确认下单
  • 四、完整代码
  • 五、补充信息
  • 总结
  • 一些杂七杂八
  • 前言

    最近在学习python里selenium库的使用,正好当前各种演唱会纷飞,可以用来测试。目前开发的程序只适用于票星球网页非选座版,app和要选座版暂未开发,所以网页上能买票的才能运行该程序。
    申明:程序仅供学习,请勿用于违法活动,如作他用所承受的法律责任一概与作者无关。

    selenium简单介绍

    概括来说,selenium能根据编程指令自动在浏览器中执行,解放双手,一般用于自动化测试等。整体实现方法很简单:提前规划好执行逻辑——连接到浏览器——确定要点击/判断的网页元素名称——执行。作者测试的版本是python 3.9,selenium 4.26.1,其他python版本可以直接在网上搜索到对应的版本号。

    抢票流程分析

    一、进入网页

    直接进入购票界面,如图所示:
    查看红框可知,抢票界面的网址是:

    "https://m.piaoxingqiu.com/booking/"+show_id+"?saleShowSessionId=&seatPickType=SUPPORT_NONE&selectedDate=&showId="+show_id+"&stdShowId="+stdShowId
    

    其中,"show_id"是该演出的id,可以直接在网址上找到,每个项目的id是唯一的;"stdShowId"经过验证,其实就是show_id(十六进制)-1,所以只要知道show_id,就可以计算出stdShowId。

    二、 选择观演日期和票价

    选择界面如图所示,观演日期(session)和票价(ticket_price)填写时必须与网站显示的完全相同,例如图上显示的’2025-02-21 周五 19:30’和’480元’,在抢票时会与填写的内容进行字符比对,完全一致时才会点击,所以必须要注意。

    三、选择观演人数和观演人

    1. 观演人数(buy_count)按需填写,观演人信息要提前填好。为加快抢票速度,程序中没有判断观演人信息是否对应的部分,所以抢票时只保留需要的观演人,防止选择出错;
    2. 票星球似乎只有在刚开票时不用勾选购票人(所以必须提前预选好!),刷回流票时是需要再次勾选的,所以只抢一个人的回流票一定程度上会加快程序运行速度,抢到的概率更大;
    3. 根据“刚开票”和“刷回流”两种模式不同,程序运行的顺序和逻辑也有两种方式,后续会详细说明。

    四、参数设置

    上述需要的"show_id"、“session”、“ticket_price”、"buy_count"都可配置在config.py文件中,方便直接修改运行,示例如图。

    # 抢票模式,必填
    ticket_model = '刚开票'  # 目前只有:'刷回流'、'刚开票'(需提前预选好场次、购票人)
    # 项目id,必填
    show_id = '66f27787ed376600017c8029'
    # 场次,必填
    session = '2025-01-01 周三 19:00'
    # 票价,必填
    ticket_price = '1980元' # '看台 680元'
    # 购票数量,一定要看购票须知,不要超过上限,必填
    buy_count = 1
    

    程序具体执行

    一、连接浏览器

    1. 首先要引入的包,config是“参数设置”里的配置文件
    from selenium import webdriver
    from selenium.webdriver.edge.service import Service
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    from selenium.common.exceptions import TimeoutException
    from selenium.webdriver.common.by import By
    import time
    import os
    
    import config
    
    ticket_model = config.ticket_model
    show_id = config.show_id
    session = config.session
    ticket_price = config.ticket_price
    buy_count = config.buy_count
    
    1. 连接到浏览器(这里用的是Edge浏览器)并查询网址,用户配置路径需修改
    print('当前模式:', ticket_model)
    edge_options = webdriver.EdgeOptions()
    edge_options.add_argument(r"user-data-dir=D:\csdn\Mint_V\scoped_dir18252_1702533799") # 用户配置路径,直接在edg浏览器输入edge://version/可以查看
    browser = webdriver.Edge(options=edge_options)
    
    stdShowId = hex_str_subtract_one(show_id)
    url = "https://m.piaoxingqiu.com/booking/"+show_id+"?saleShowSessionId=&seatPickType=SUPPORT_NONE&selectedDate=&showId="+show_id+"&stdShowId="+stdShowId
    print(url)
    browser.get(url) # 访问网址
    time.sleep(1) # 等待1s反应
    

    使用到的计算stdShowId的函数:

    def hex_str_subtract_one(hex_str):
    	# 将十六进制字符串转换为整数
        num = int(hex_str, 16)
        # 对整数进行减 1 操作
        num -= 1
        # 将结果转换回十六进制字符串
        # 使用 format 函数确保结果的长度与原始字符串一致
        result_hex_str = format(num, 'x').zfill(len(hex_str))
    
        return result_hex_str
    

    二、选择场次日期和价位

    1. 选择日期函数如下,其中元素名称"session-name"的获取方式为:进入开发者模式(F12或Fn+F12)——点击检查元素按钮——点击需要的场次名——查看元素名称,可以看到class=“session-name”,即为需要的元素。
    def choose_date(browser):
        while True:
         	# 判断是否存在元素,不存在就刷新页面
            try:
                elements = WebDriverWait(browser, 4).until(
                    EC.presence_of_all_elements_located((By.XPATH, '//uni-text[contains(@class,"session-name")]'))
                )
                break
            except Exception as e:
                browser.refresh()
                time.sleep(0.5)
        for element in elements:
            if element.text.strip() == session: # 选择的场次与想要的相同
                element.click()  # 单击选择
                time.sleep(0.1)  # 等待0.1s缓冲
                break
        return
    

    1. 选择价位函数如下,获取元素名称方式同上
    def choose_price(browser):
        elements = WebDriverWait(browser, 3).until(
            EC.presence_of_all_elements_located((By.XPATH, '//uni-text[contains(@class,"ticket")]'))
        )
        # elements = browser.find_elements(By.XPATH, '//uni-text[contains(@class,"ticket")]')
        for element in elements:
            print(element.text.strip())
            if element.text.strip() == ticket_price:
                element.click()
                time.sleep(0.1)
                break
        return
    
    1. 判断是否已经开票。之前的大多数抢票代码是设定开票时间,通过判断当前时间与开票时间的误差,确定是否抢票,其实是有一些问题的。在此提供一个新的思路,下图分别为能购票和还未开票的界面,可以看到它们之间的区别在于能否选择购票数量,也就是购票数量元素是否存在。因此,选择购票人数的相关代码如下。
    def choose_ticket_number(buy_count, browser):
        for i in range(buy_count):
            element = browser.find_element(By.XPATH, '//uni-view[contains(@class,"number")]')
            print('当前购票数:', int(element.text.strip()))
            if int(element.text.strip()) == buy_count:
                break
            else:
                element_add = browser.find_element(By.XPATH,
                                                   "//uni-view[contains(concat(' ', normalize-space(@class), ' '), ' plus iconfont icon-jia ')]")
                element_add.click()
                time.sleep(0.1)
        return
    
    1. 综合上述分析,我们可以将函数汇总起来,通过循环“选择演出日期——选择演出票价——判断是否有购票数”这一过程来判断是否开抢,若未开票就刷新界面,若已开票则点击“下一步”到选择观演人。
    def ready_to_buy(buy_count, browser):
        while True:
            try:
                choose_date(browser)
                choose_price(browser)
                choose_ticket_number(buy_count, browser)
                break
            except Exception as e:
                print('未开卖')
                browser.refresh()
                time.sleep(0.5)
        return
    
    ready_to_buy(buy_count, browser)
    # 点击下一步
    element = browser.find_element(By.XPATH, '//uni-view[contains(@class,"one-btn bottom-btn-item")]')
    element.click()
    time.sleep(0.5)
    

    三、选择观演人及确认下单

    上文提到过,票星球在选择观演人这一步有两种模式:刚开票和刷回流。在刚开票模式下,只要提前预选好观演人,选择观演人这一步是不需要再进行操作的,所以直接点击下单即可;在刷回流模式下,无论是否预选过观演人,都需要再次选择观演人,因此这里会选择与购票数量相同的观演人数。为了简化程序,这里并没有写判断勾选的观演人是否与期望的相同,所以大家在使用时,只保留要购票人的信息即可,避免勾选到其他人。两种模式

        if ticket_model == '刚开票':
            while True:
                element = WebDriverWait(browser, 3).until(
                    EC.element_to_be_clickable(
                        (By.XPATH, '//uni-view[contains(@class,"btn-pay")]'))
                )
                # element = browser.find_element(By.XPATH, '//uni-view[contains(@class,"btn-pay")]')
                print('可以点击')
                # 循环点击
                time.sleep(0.1)
                element.click()
                try:
                    element = WebDriverWait(browser, 3).until(
                        EC.element_to_be_clickable(
                            (By.XPATH, '//uni-view[contains(@class,"payment-type-title")]'))
                    )
                    if element.text.strip() == "选择支付方式":
                        print('已买到,请立刻付款')
                        time.sleep(5)
                        break
                except Exception as e:
                    browser.refresh()
                    time.sleep(0.5)
    
        elif ticket_model == '刷回流':
            # 选购票人
            while True:
                element_xpath = "//uni-view[@class='iconfont icon-choose icon-xuanzhong']"
                try:
                    elements = browser.find_elements(By.XPATH, element_xpath)
                    break
                except Exception as e:
                    browser.refresh()
                    time.sleep(0.5)
    
            if len(elements) > 0:  # 购票人已勾选
                print("已勾选购票人.")
                while True:
                    element = WebDriverWait(browser, 3).until(
                        EC.element_to_be_clickable(
                            (By.XPATH, '//uni-view[contains(@class,"btn-pay")]'))
                    )
                    # element = browser.find_element(By.XPATH, '//uni-view[contains(@class,"btn-pay")]')
                    if element.text.strip() == "去支付":
                        print('可以点击')
                        # 循环点击
                        element.click()
                        try:
                            element = WebDriverWait(browser, 3).until(
                                EC.element_to_be_clickable(
                                    (By.XPATH, '//uni-view[contains(@class,"payment-type-title")]'))
                            )
                            if element.text.strip() == "选择支付方式":
                                print('已买到,请立刻付款')
                                time.sleep(5)
                                break
                        except Exception as e:
                            browser.refresh()
                            time.sleep(0.5)
    
            else: # 勾选所有存在的购票人
                print("购票人未勾选,自动选择预购票数相同的前几位购票人.")
                elements = WebDriverWait(browser, 3).until(
                    EC.presence_of_all_elements_located((By.XPATH, "//uni-view[@class='iconfont icon-choose icon-weigouxuan']"))
                )
                # elements = browser.find_elements(By.XPATH, "//uni-view[@class='iconfont icon-choose icon-weigouxuan']")
                for element in elements:
                    element.click()
                    time.sleep(0.1)
    
                while True:
                    element = WebDriverWait(browser, 3).until(
                        EC.element_to_be_clickable(
                            (By.XPATH, '//uni-view[contains(@class,"btn-pay")]'))
                    )
                    # element = browser.find_element(By.XPATH, '//uni-view[contains(@class,"btn-pay")]')
                    if element.text.strip() == "去支付":
                        print('可以点击')
                        # 循环点击
                        element.click()
                        time.sleep(0.8)
                        try:
                            element = WebDriverWait(browser, 3).until(
                                EC.element_to_be_clickable(
                                    (By.XPATH, '//uni-view[contains(@class,"payment-type-title")]'))
                            )
                            if element.text.strip() == "选择支付方式":
                                print('已买到,请立刻付款')
                                time.sleep(5)
                                break
                        except Exception as e:
                            browser.refresh()
                            time.sleep(0.5)
    

    四、完整代码

    完整代码如下:

    1. config.py
    # 抢票模式,必填
    ticket_model = '刷回流'  # 目前只有:'刷回流'、'刚开票'(需提前预选好场次、购票人)
    # 项目id,必填
    show_id = '66f27787ed376600017c8029'
    # 场次,必填
    session = '2025-01-01 周三 19:00'
    # 票价,必填
    ticket_price = '1980元' # '看台 680元'
    # 购票数量,一定要看购票须知,不要超过上限,必填
    buy_count = 1
    
    1. main.py
    from selenium import webdriver
    from selenium.webdriver.edge.service import Service
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    from selenium.common.exceptions import TimeoutException
    from selenium.webdriver.common.by import By
    import time
    import os
    
    import config
    
    
    def hex_str_subtract_one(hex_str):
        # 将十六进制字符串转换为整数
        num = int(hex_str, 16)
        # 对整数进行减 1 操作
        num -= 1
        # 将结果转换回十六进制字符串
        # 使用 format 函数确保结果的长度与原始字符串一致
        result_hex_str = format(num, 'x').zfill(len(hex_str))
    
        return result_hex_str
        
    
    def ready_to_buy(buy_count, browser):
        while True:
            try:
                choose_date(browser)
                choose_price(browser)
                choose_ticket_number(buy_count, browser)
                break
            except Exception as e:
                print('未开卖')
                browser.refresh()
                time.sleep(0.5)
        return
    
    
    def choose_date(browser):
        while True:
            try:
                elements = WebDriverWait(browser, 4).until(
                    EC.presence_of_all_elements_located((By.XPATH, '//uni-text[contains(@class,"session-name")]'))
                )
                break
            except Exception as e:
                browser.refresh()
                time.sleep(0.5)
        for element in elements:
            if element.text.strip() == session:
                element.click()
                time.sleep(0.1)
                break
        return
    
    
    def choose_price(browser):
        elements = WebDriverWait(browser, 3).until(
            EC.presence_of_all_elements_located((By.XPATH, '//uni-text[contains(@class,"ticket")]'))
        )
        # elements = browser.find_elements(By.XPATH, '//uni-text[contains(@class,"ticket")]')
        for element in elements:
            print(element.text.strip())
            if element.text.strip() == ticket_price:
                element.click()
                time.sleep(0.1)
                break
        return
    
    def choose_ticket_number(buy_count, browser):
        for i in range(buy_count):
            element = browser.find_element(By.XPATH, '//uni-view[contains(@class,"number")]')
            print('当前购票数:', int(element.text.strip()))
            if int(element.text.strip()) == buy_count:
                break
            else:
                element_add = browser.find_element(By.XPATH,
                                                   "//uni-view[contains(concat(' ', normalize-space(@class), ' '), ' plus iconfont icon-jia ')]")
                element_add.click()
                time.sleep(0.1)
        return
    
    
    ticket_model = config.ticket_model
    show_id = config.show_id
    session = config.session
    ticket_price = config.ticket_price
    buy_count = config.buy_count
    
    
    if __name__ == '__main__':
        print('当前模式:', ticket_model)
        edge_options = webdriver.EdgeOptions()
        edge_options.add_argument(r"user-data-dir=D:\csdn\Mint_V\scoped_dir18252_1702533799")
        browser = webdriver.Edge(options=edge_options)
    
        stdShowId = hex_str_subtract_one(show_id)
        url = "https://m.piaoxingqiu.com/booking/"+show_id+"?saleShowSessionId=&seatPickType=SUPPORT_NONE&selectedDate=&showId="+show_id+"&stdShowId="+stdShowId
        print(url)
        browser.get(url)
        time.sleep(1)
    
        # 刷新页面直到可以买票
        ready_to_buy(buy_count, browser)
        # 点击下一步
        element = browser.find_element(By.XPATH, '//uni-view[contains(@class,"one-btn bottom-btn-item")]')
        element.click()
        time.sleep(0.5)
    
        # =======页面跳转========
        # 下单
        if ticket_model == '刚开票':
            while True:
                element = WebDriverWait(browser, 3).until(
                    EC.element_to_be_clickable(
                        (By.XPATH, '//uni-view[contains(@class,"btn-pay")]'))
                )
                print('可以点击')
                # 循环点击
                time.sleep(0.1)
                element.click()
                try:
                    element = WebDriverWait(browser, 3).until(
                        EC.element_to_be_clickable(
                            (By.XPATH, '//uni-view[contains(@class,"payment-type-title")]'))
                    )
                    if element.text.strip() == "选择支付方式":
                        print('已买到,请立刻付款')
                        time.sleep(5)
                        break
                except Exception as e:
                    browser.refresh()
                    time.sleep(0.5)
    
        elif ticket_model == '刷回流':
            # 选购票人
            while True:
                element_xpath = "//uni-view[@class='iconfont icon-choose icon-xuanzhong']"
                try:
                    elements = browser.find_elements(By.XPATH, element_xpath)
                    break
                except Exception as e:
                    browser.refresh()
                    time.sleep(0.5)
    
            if len(elements) > 0:  # 购票人已勾选
                print("已勾选购票人.")
                while True:
                    element = WebDriverWait(browser, 3).until(
                        EC.element_to_be_clickable(
                            (By.XPATH, '//uni-view[contains(@class,"btn-pay")]'))
                    )
                    if element.text.strip() == "去支付":
                        print('可以点击')
                        # 循环点击
                        element.click()
                        try:
                            element = WebDriverWait(browser, 3).until(
                                EC.element_to_be_clickable(
                                    (By.XPATH, '//uni-view[contains(@class,"payment-type-title")]'))
                            )
                            if element.text.strip() == "选择支付方式":
                                print('已买到,请立刻付款')
                                time.sleep(5)
                                break
                        except Exception as e:
                            browser.refresh()
                            time.sleep(0.5)
    
            else:
                print("购票人未勾选,自动选择预购票数相同的前几位购票人.")
                elements = WebDriverWait(browser, 3).until(
                    EC.presence_of_all_elements_located((By.XPATH, "//uni-view[@class='iconfont icon-choose icon-weigouxuan']"))
                )
                for element in elements:
                    element.click()
                    time.sleep(0.1)
    
                while True:
                    element = WebDriverWait(browser, 3).until(
                        EC.element_to_be_clickable(
                            (By.XPATH, '//uni-view[contains(@class,"btn-pay")]'))
                    )
                    # element = browser.find_element(By.XPATH, '//uni-view[contains(@class,"btn-pay")]')
                    if element.text.strip() == "去支付":
                        print('可以点击')
                        # 循环点击
                        element.click()
                        time.sleep(0.8)
                        try:
                            element = WebDriverWait(browser, 3).until(
                                EC.element_to_be_clickable(
                                    (By.XPATH, '//uni-view[contains(@class,"payment-type-title")]'))
                            )
                            if element.text.strip() == "选择支付方式":
                                print('已买到,请立刻付款')
                                time.sleep(5)
                                break
                        except Exception as e:
                            browser.refresh()
                            time.sleep(0.5)
    
        input()
    

    五、补充信息

    1. 作者用程序抢到了一张张悬的回流票,但是没付款只是测试程序。建议大家拿还有票的演出运行下程序,能对各部分程序的功能有更深入的了解。文章的内容如果有表述不清晰的地方,欢迎大家讨论;
    2. 整体程序运行起来可能还是不太智能,需要在使用过程中慢慢优化。比如到最后的下单页面时,因为被别人抢先买走了,会出现弹窗提示,而此时程序已经默认执行结束,所以不会有反应。这时候,就需要手动终止运行,再重新运行程序的回流模式。这一块有兴趣的朋友可以自己改进一下,后面有时间的话作者会考虑优化;
    3. 能否抢到还是跟网速有直接关系,其次是号是否被盾;
    4. 使用须知:票星球有服务声明,使用自动化程序的交易订单,官方有权利取消哦= ̄ω ̄=。

    总结

    本篇主要实现了selenium自动抢票,做下来感觉抢票还挺有意思的,关键点在于各个元素的获取和整体逻辑要考虑好,剩下的就是无脑执行了。其他网站的爬虫方法也大同小异,主要是一些网站的反爬虫机制比较厉害,后续有机会的话可以再深入了解一下。

    一些杂七杂八

    1. 未来可以考虑使用uiautomator2等库,用电脑连接手机直接操作APP,彻底解放双手自由;
    2. 探索一点监控回流的小方法。

    作者:Mint-V

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【Python】利用selenium实现自动抢票(ticket星球网页版)

    发表回复