【Python】利用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元’,在抢票时会与填写的内容进行字符比对,完全一致时才会点击,所以必须要注意。
三、选择观演人数和观演人
- 观演人数(buy_count)按需填写,观演人信息要提前填好。为加快抢票速度,程序中没有判断观演人信息是否对应的部分,所以抢票时只保留需要的观演人,防止选择出错;
- 票星球似乎只有在刚开票时不用勾选购票人(所以必须提前预选好!),刷回流票时是需要再次勾选的,所以只抢一个人的回流票一定程度上会加快程序运行速度,抢到的概率更大;
- 根据“刚开票”和“刷回流”两种模式不同,程序运行的顺序和逻辑也有两种方式,后续会详细说明。
四、参数设置
上述需要的"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
程序具体执行
一、连接浏览器
- 首先要引入的包,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
- 连接到浏览器(这里用的是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
二、选择场次日期和价位
- 选择日期函数如下,其中元素名称"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
- 选择价位函数如下,获取元素名称方式同上
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
- 综合上述分析,我们可以将函数汇总起来,通过循环“选择演出日期——选择演出票价——判断是否有购票数”这一过程来判断是否开抢,若未开票就刷新界面,若已开票则点击“下一步”到选择观演人。
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)
四、完整代码
完整代码如下:
- config.py
# 抢票模式,必填
ticket_model = '刷回流' # 目前只有:'刷回流'、'刚开票'(需提前预选好场次、购票人)
# 项目id,必填
show_id = '66f27787ed376600017c8029'
# 场次,必填
session = '2025-01-01 周三 19:00'
# 票价,必填
ticket_price = '1980元' # '看台 680元'
# 购票数量,一定要看购票须知,不要超过上限,必填
buy_count = 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()
五、补充信息
- 作者用程序抢到了一张张悬的回流票,但是没付款只是测试程序。建议大家拿还有票的演出运行下程序,能对各部分程序的功能有更深入的了解。文章的内容如果有表述不清晰的地方,欢迎大家讨论;
- 整体程序运行起来可能还是不太智能,需要在使用过程中慢慢优化。比如到最后的下单页面时,因为被别人抢先买走了,会出现弹窗提示,而此时程序已经默认执行结束,所以不会有反应。这时候,就需要手动终止运行,再重新运行程序的回流模式。这一块有兴趣的朋友可以自己改进一下,后面有时间的话作者会考虑优化;
- 能否抢到还是跟网速有直接关系,其次是号是否被盾;
- 使用须知:票星球有服务声明,使用自动化程序的交易订单,官方有权利取消哦= ̄ω ̄=。
总结
本篇主要实现了selenium自动抢票,做下来感觉抢票还挺有意思的,关键点在于各个元素的获取和整体逻辑要考虑好,剩下的就是无脑执行了。其他网站的爬虫方法也大同小异,主要是一些网站的反爬虫机制比较厉害,后续有机会的话可以再深入了解一下。
一些杂七杂八
- 未来可以考虑使用uiautomator2等库,用电脑连接手机直接操作APP,彻底解放双手自由;
- 探索一点监控回流的小方法。