# coding: utf-8
import logging
import random
from json import loads
from logging.handlers import RotatingFileHandler
from time import sleep, time
from pickle import dump, load
from os.path import exists
from selenium import webdriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.common import exceptions
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.chrome.service import Service
class Concert(object):
def __init__(self, config): # 保持接收config字典参数
self._init_logger()
self.date = config['date']
self.session = config['sess']
self.price = config['price']
self.real_name = config['real_name']
self.status = 0
self.status = 0
self.time_start = 0
self.time_end = 0
self.num = 0
self.ticket_num = config['ticket_num']
self.viewer_person = config['viewer_person']
self.nick_name = config['nick_name']
self.damai_url = config['damai_url']
self.target_url = config['target_url']
self.driver_path = config['driver_path']
self.logger.info(f'初始化抢票账号: {self.nick_name}')
def _init_logger(self):
self.logger = logging.getLogger(self.__class__.__name__)
handler = RotatingFileHandler('concert.log', maxBytes=1e6, backupCount=5)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.logger.setLevel(logging.INFO)
def _init_driver(self): # 正确:缩进在类内部
try:
options = webdriver.ChromeOptions()
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)
return webdriver.Chrome(service=Service(self.driver_path), options=options)
except Exception as e: # 现在有对应的try了
self.logger.error(f'获取cookie失败: {str(e)}')
def get_cookie(self):
self.logger.info('开始获取cookie')
try:
self.driver = self._init_driver()
self.driver.get(self.damai_url)
WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.CLASS_NAME, 'login-user'))).click()
while "大麦登录" in self.driver.title:
sleep(1)
dump(self.driver.get_cookies(), open("cookies.pkl", "wb"))
self.logger.info('Cookie获取成功')
except Exception as e:
self.logger.error(f'获取cookie失败: {str(e)}')
raise
finally:
if self.driver:
self.driver.quit()
def set_cookie(self):
try:
cookies = load(open("cookies.pkl", "rb"))
for cookie in cookies:
self.driver.add_cookie({
'domain': '.damai.cn',
'name': cookie.get('name'),
'value': cookie.get('value'),
'path': '/',
'httpOnly': False,
'Secure': False
})
self.logger.info('Cookie载入成功')
except Exception as e:
self.logger.error(f'载入Cookie失败: {str(e)}')
raise
def login(self):
self.logger.info('开始登录流程')
try:
self.driver.get(self.target_url)
WebDriverWait(self.driver, 10).until(
EC.title_contains('商品详情')
)
self.set_cookie()
self.driver.refresh()
# 检查验证码
if "验证码" in self.driver.page_source:
self.logger.warning("需要人工处理验证码")
input("请在浏览器中完成验证码验证后按回车继续...")
except Exception as e:
self.logger.error(f"登录失败: {str(e)}")
raise
def enter_concert(self):
self.logger.info('启动抢票流程')
try:
if not exists('cookies.pkl'):
self.get_cookie()
self.driver = self._init_driver()
if not self.driver:
raise Exception("浏览器驱动初始化失败")
self.login()
self.time_start = time()
self.logger.info('进入抢票页面成功')
except Exception as e:
self.logger.error(f'抢票流程异常: {str(e)}')
if hasattr(self, 'driver') and self.driver:
self.driver.quit()
raise
def choose_ticket(self):
self.logger.info('进入抢票界面')
while self.driver.title.find('订单确认') == -1:
self.num += 1
try:
box = WebDriverWait(self.driver, 3).until(
EC.presence_of_element_located((By.ID, 'app')))
# 处理实名制弹窗
try:
realname_popup = box.find_elements(By.XPATH, "//div[@class='realname-popup']")
if realname_popup:
realname_popup[0].find_element(By.XPATH, ".//div[contains(@class,'button')]").click()
except:
pass
# 获取购买按钮状态
buybutton = box.find_element(By.CLASS_NAME, 'buy__button')
buybutton_text = buybutton.text
if "即将开抢" in buybutton_text:
self.status = 2
raise Exception("演出尚未开售")
if "缺货" in buybutton_text:
raise Exception("票已售罄")
buybutton.click()
self._select_ticket_options()
except Exception as e:
self.logger.warning(f'抢票尝试失败: {str(e)}')
self.driver.refresh()
sleep(random.uniform(0.5, 1.5))
continue
def select_ticket_options(self):
box = WebDriverWait(self.driver, 2).until(
EC.presence_of_element_located((By.CSS_SELECTOR, '.sku-pop-wrapper')))
# 选择日期
try:
date = WebDriverWait(self.driver, 1).until(
EC.presence_of_element_located((By.CLASS_NAME, 'bui-dm-sku-calendar')))
date_list = date.find_elements(By.CLASS_NAME, 'bui-calendar-day-box')
for i in self.date:
if i <= len(date_list):
date_list[i-1].click()
break
except:
pass
# 选择场次
session = WebDriverWait(self.driver, 1).until(
EC.presence_of_element_located((By.CLASS_NAME, 'sku-times-card')))
session_list = session.find_elements(By.CLASS_NAME, 'bui-dm-sku-card-item')
for i in self.session:
if i <= len(session_list):
item = session_list[i-1]
if not self._check_item_availability(item):
continue
item.click()
break
# 选择票价
price = WebDriverWait(self.driver, 1).until(
EC.presence_of_element_located((By.CLASS_NAME, 'sku-tickets-card')))
price_list = price.find_elements(By.CLASS_NAME, 'bui-dm-sku-card-item')
for i in self.price:
if i <= len(price_list):
item = price_list[i-1]
if not self._check_item_availability(item):
continue
item.click()
break
# 提交选择
buybutton = box.find_element(By.CLASS_NAME, 'sku-footer-buy-button')
buybutton_text = buybutton.text
if buybutton_text == "选座购买":
buybutton.click()
self.status = 5
return
# 增加票数
try:
ticket_num_up = box.find_element(By.CLASS_NAME, 'plus-enable')
for _ in range(self.ticket_num - 1):
ticket_num_up.click()
except:
pass
buybutton.click()
self.status = 4
WebDriverWait(self.driver, 3).until(EC.title_contains("确认"))
def _check_item_availability(self, element):
try:
tag = element.find_element(By.CLASS_NAME, 'item-tag')
return tag.text not in ['无票', '缺货']
except:
return True
def check_order(self):
if self.status in [3, 4, 5]:
try:
# 选择观影人
WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH, '//*[@id=\"dmViewerBlock_DmViewerBlock\"]/div[2]/div/div')))
people = self.driver.find_elements(By.XPATH, '//*[@id=\"dmViewerBlock_DmViewerBlock\"]/div[2]/div/div')
for i in self.viewer_person:
if i <= len(people):
people[i-1].click()
sleep(0.1)
# 提交订单
WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH, '//*[@id=\"dmOrderSubmitBlock_DmOrderSubmitBlock\"]/div[2]/div/div[2]/div[3]/div[2]')))
self.driver.find_element(By.XPATH, '//*[@id=\"dmOrderSubmitBlock_DmOrderSubmitBlock\"]/div[2]/div/div[2]/div[3]/div[2]').click()
# 等待跳转支付
WebDriverWait(self.driver, 10).until(EC.title_contains('支付宝'))
self.status = 6
self.time_end = time()
self.logger.info('抢票成功,请完成支付')
except Exception as e:
self.logger.error(f'支付流程异常: {str(e)}')
raise
def __del__(self):
if hasattr(self, 'driver') and self.driver:
self.driver.quit()
self.logger.info('浏览器已关闭')
def click_util(self, btn, locator):
while True:
btn.click()
try:
return WebDriverWait(self.driver, 1, 0.1).until(EC.presence_of_element_located(locator))
except:
continue
# 实现购买函数
def choose_ticket(self):
print(u"###进入抢票界面###")
# 如果跳转到了确认界面就算这步成功了,否则继续执行此步
while self.driver.title.find('订单确认') == -1:
self.num += 1 # 尝试次数加1
if con.driver.current_url.find("buy.damai.cn") != -1:
break
# 确认页面刷新成功
try:
box = WebDriverWait(self.driver, 3, 0.1).until(
EC.presence_of_element_located((By.ID, 'app')))
except:
raise Exception(u"***Error: 页面刷新出错***")
try:
realname_popup = box.find_elements(
by=By.XPATH, value="//div[@class='realname-popup']") # 寻找实名身份遮罩
if len(realname_popup) != 0:
known_button = realname_popup[0].find_element(
by=By.XPATH, value="//div[@class='operate']//div[@class='button']")
known_button[0].click()
except:
raise Exception(u"***Error: 实名制遮罩关闭失败***")
try:
buybutton = box.find_element(by=By.CLASS_NAME, value='buy__button')
sleep(0.5)
buybutton_text: str = buybutton.text
except Exception as e:
raise Exception(f"***Error: buybutton 位置找不到***: {e}")
if "即将开抢" in buybutton_text:
self.status = 2
raise Exception(u"---尚未开售,刷新等待---")
if "缺货" in buybutton_text:
raise Exception("---已经缺货了---")
sleep(0.1)
buybutton.click()
box = WebDriverWait(self.driver, 2, 0.1).until(
EC.presence_of_element_located((By.CSS_SELECTOR, '.sku-pop-wrapper')))
try:
# 日期选择
toBeClicks = []
try:
date = WebDriverWait(self.driver, 2, 0.1).until(
EC.presence_of_element_located((By.CLASS_NAME, 'bui-dm-sku-calendar')))
except Exception as e:
date = None
if date is not None:
date_list = date.find_elements(
by=By.CLASS_NAME, value='bui-calendar-day-box')
for i in self.date:
j: WebElement = date_list[i-1]
toBeClicks.append(j)
break
for i in toBeClicks:
i.click()
sleep(0.05)
# 选定场次
session = WebDriverWait(self.driver, 2, 0.1).until(
EC.presence_of_element_located((By.CLASS_NAME, 'sku-times-card'))) # 日期、场次和票档进行定位
session_list = session.find_elements(
by=By.CLASS_NAME, value='bui-dm-sku-card-item')
toBeClicks = []
for i in self.session: # 根据优先级选择一个可行场次
if i > len(session_list):
i = len(session_list)
j: WebElement = session_list[i-1]
# TODO 不确定已满的场次带的是什么Tag
k = self.isClassPresent(j, 'item-tag', True)
if k: # 如果找到了带presell的类
if k.text == '无票':
continue
elif k.text == '预售':
toBeClicks.append(j)
break
elif k.text == '惠':
toBeClicks.append(j)
break
else:
toBeClicks.append(j)
break
# 多场次的场要先选择场次才会出现票档
for i in toBeClicks:
i.click()
sleep(0.05)
# 选定票档
toBeClicks = []
price = WebDriverWait(self.driver, 2, 0.1).until(
EC.presence_of_element_located((By.CLASS_NAME, 'sku-tickets-card'))) # 日期、场次和票档进行定位
price_list = price.find_elements(
by=By.CLASS_NAME, value='bui-dm-sku-card-item') # 选定票档
# print('可选票档数量为:{}'.format(len(price_list)))
for i in self.price:
if i > len(price_list):
i = len(price_list)
j = price_list[i-1]
# k = j.find_element(by=By.CLASS_NAME, value='item-tag')
k = self.isClassPresent(j, 'item-tag', True)
if k: # 存在notticket代表存在缺货登记,跳过
continue
else:
toBeClicks.append(j)
break
for i in toBeClicks:
i.click()
sleep(0.1)
buybutton = box.find_element(
by=By.CLASS_NAME, value='sku-footer-buy-button')
sleep(1.0)
buybutton_text = buybutton.text
if buybutton_text == "":
raise Exception(u"***Error: 提交票档按钮文字获取为空,适当调整 sleep 时间***")
try:
WebDriverWait(self.driver, 2, 0.1).until(
EC.presence_of_element_located((By.CLASS_NAME, 'bui-dm-sku-counter')))
except:
raise Exception(u"***购票按钮未开始***")
except Exception as e:
raise Exception(f"***Error: 选择日期or场次or票档不成功***: {e}")
try:
ticket_num_up = box.find_element(
by=By.CLASS_NAME, value='plus-enable')
except:
if buybutton_text == "选座购买": # 选座购买没有增减票数键
buybutton.click()
self.status = 5
print(u"###请自行选择位置和票价###")
break
elif buybutton_text == "提交缺货登记":
raise Exception(u'###票已被抢完,持续捡漏中...或请关闭程序并手动提交缺货登记###')
else:
raise Exception(u"***Error: ticket_num_up 位置找不到***")
if buybutton_text == "立即预订" or buybutton_text == "立即购买" or buybutton_text == '确定':
for i in range(self.ticket_num-1): # 设置增加票数
ticket_num_up.click()
buybutton.click()
self.status = 4
WebDriverWait(self.driver, 3, 0.1).until(
EC.title_contains("确认"))
break
else:
raise Exception(f"未定义按钮:{buybutton_text}")
def check_order(self):
if self.status in [3, 4, 5]:
# 选择观影人
toBeClicks = []
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, '//*[@id="dmViewerBlock_DmViewerBlock"]')))
people = self.driver.find_elements(
By.XPATH, '//*[@id="dmViewerBlock_DmViewerBlock"]/div[2]/div/div')
sleep(0.2)
for i in self.viewer_person:
if i > len(people):
break
j = people[i-1]
j.click()
sleep(0.05)
WebDriverWait(self.driver, 15).until(
lambda d: 'alipay' in d.current_url.lower() or '支付宝' in d.title)
comfirmBtn = self.driver.find_element(
By.XPATH, '//*[@id="dmOrderSubmitBlock_DmOrderSubmitBlock"]/div[2]/div/div[2]/div[3]/div[2]')
sleep(0.5)
comfirmBtn.click()
# 判断title是不是支付宝
print(u"###等待跳转到--付款界面--,可自行刷新,若长期不跳转可选择-- CRTL+C --重新抢票###")
while True:
try:
WebDriverWait(self.driver, 4, 0.1).until(
EC.title_contains('支付宝'))
except:
# 通过人工判断是否继续等待支付宝跳转界面
c ="""
等待输入指示:
1.抢票成功
2.抢票失败,未知原因没跳转到支付宝界面,进入下一轮抢票
"""
step = input('等待跳转到支付宝页面,请输入:')
if step == '1':
# 成功
break
# elif step == '2':
# # 有遮罩弹窗,包括各种错误提示
# try:
# confirm = WebDriverWait(self.driver, 3, 0.1).until(
# EC.presence_of_element_located((By.ID, 'confirm')))
# except:
# raise Exception(u"***Error: 页面刷新出错***")
else:
raise Exception(u'***Error: 长期跳转不到付款界面***')
break
self.status = 6
print(u'###成功提交订单,请手动支付###')
self.time_end = time()
if __name__ == '__main__':
try:
with open('./config.json', 'r', encoding='utf-8') as f:
config = loads(f.read())
# params: 场次优先级,票价优先级,实名者序号, 用户昵称, 购买票数, 官网网址, 目标网址, 浏览器驱动地址
con = Concert(config) # 直接传入整个config字典
con.enter_concert() # 进入到具体抢购页面
except Exception as e:
print(e)
exit(1)
while True:
try:
con.choose_ticket()
con.check_order()
except Exception as e:
con.driver.get(con.target_url)
print(e)
continue
if con.status == 6:
print(u"###经过%d轮奋斗,共耗时%.1f秒,抢票成功!请确认订单信息###" %
(con.num, round(con.time_end-con.time_start, 3)))
break
帮我修正更新增加捡漏模式