├── .github └── workflows │ └── codeql.yml ├── O1CN01QtSzD62GdSE1msrJp_!!2251059038.jpg ├── README.md ├── config.json └── damai_ticket.py /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '42 4 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v2 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v2 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /O1CN01QtSzD62GdSE1msrJp_!!2251059038.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kemomi/daimai/d07238dcad3d3229f0ce59e682fc47bfa8e53327/O1CN01QtSzD62GdSE1msrJp_!!2251059038.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### 更新 3 | 4 |
5 | logo
6 |
7 | 8 | 9 | ### 提前准备 10 | * Python 3.6.3 11 | * Chromedriver.exe 12 | * Chrome 浏览器安装好后需将chromedriver.exe放置于Chrome浏览器目录下 13 | * `pip install selenium requests lxml` 14 | 15 | ### 参数设置 16 | 17 | 在`config.json`中输入相应配置信息,具体说明如下: 18 | 19 | * `date`: 日期选择 20 | * `sess`: 场次优先级列表 21 | * `price`: 票价优先级,如本例中共有三档票价,根据下表,则优先选择1,再选择3;也可以仅设置1个。 22 | * `real_name`: [1,2], 实名者序号,根据序号共选择两位实名者,根据序号,也可仅选择一位 23 | * #选择一位或是多位根据购票需知要求:若无需实名制信息则不需要填写;若一个订单仅需提供一位购票人信息则选择一位;若一张门票对应一位购票人信息则选择多位。 24 | * `driver_path`:浏览器驱动地址 25 | * `nick_name`: 用户在大麦网的昵称,用于验证登录是否成功 26 | * `ticket_num`: 购买票数 27 | * `damai_url`: https://www.damai.cn, 大麦网官网网址 28 | * `target_url`: https://detail.damai.cn/item.htm?id=607865020360 目标,例如:[周杰伦2023嘉年华世界巡回演唱会--海口站](https://detail.damai.cn/item.htm?id=607865020360) 29 | 30 | 31 | * 部分门票需要选择城市,只需选择相应城市后将其网址复制到config.json文件的`target_url`参数。 32 | 33 | * 根据需要选择的场次和票价分别修改config.json文件中的`sess`和`price`参数。 34 | 35 | * 查看购票须知中实名制一栏,若无需实名制则config.json文件中的`real_name`参数不需要填写(即为[]);若每笔订单只需一个证件号则`real_name`参数只需选择一个;若每张门票需要一个证件号,则real_name参数根据需购票数量进行相应添加。 36 | 37 | 38 | * 若是首次登录,根据终端输出的提示,依次点击登录、扫码登录,代码将自动保存cookie文件(cookie.pkl) 39 | 40 | * 使用前请将待抢票者的姓名、手机、地址设为默认。 41 | 42 | * 配置完成后执行`python damai_ticket.py`即可,注意观察控制台输出。 43 | 44 | * 本代码为保证抢票顺利,设置循环直到抢票成功才退出循环,若中途需要退出程序请直接终止程序。 45 | 46 | 47 | 48 | 49 | 50 | 51 | ### 热门演唱会信息 52 | * [周杰伦演唱会-天津](https://detail.damai.cn/item.htm?spm=a2oeg.search_category.searchtxt.ditem_0.f4294d15c5tGAZ&id=611160757855) 53 | * [周杰伦演唱会-海口](https://detail.damai.cn/item.htm?id=607865020360) 54 | * [周杰伦演唱会-呼和浩特](https://detail.damai.cn/item.htm?id=704967827554) 55 | * [周杰伦演唱会-太原](https://detail.damai.cn/item.htm?id=704762591363) 56 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "date": [2023/3/1], 3 | "sess": [1], 4 | "price": [2, 3, 1], 5 | "real_name": [1, 2], 6 | "nick_name": "麦子1Akli", 7 | "ticket_num": 1, 8 | "driver_path": "C:\Program Files (x86)\Microsoft\Edge\Application", 9 | "damai_url": "https://www.damai.cn/", 10 | "target_url": "https://search.damai.cn/search.htm?spm=a2oeg.home.category.ditem_0.591b23e1SM3Gnc&ctl=%E6%BC%94%E5%94%B1%E4%BC%9A&order=1&cty=%E6%9D%AD%E5%B7%9E" 11 | } -------------------------------------------------------------------------------- /damai_ticket.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from json import loads 3 | from time import sleep, time 4 | from pickle import dump, load 5 | from os.path import exists 6 | from selenium import webdriver 7 | from selenium.webdriver.common.by import By 8 | from selenium.webdriver.support.ui import WebDriverWait 9 | from selenium.webdriver.support import expected_conditions as EC 10 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 11 | 12 | class Concert(object): 13 | def __init__(self, date, session, price, real_name, nick_name, ticket_num, damai_url, target_url,driver_path): 14 | self.date = date # 日期序号 15 | self.session = session # 场次序号优先级 16 | self.price = price # 票价序号优先级 17 | self.real_name = real_name # 实名者序号 18 | self.status = 0 # 状态标记 19 | self.time_start = 0 # 开始时间 20 | self.time_end = 0 # 结束时间 21 | self.num = 0 # 尝试次数 22 | self.ticket_num = ticket_num # 购买票数 23 | self.nick_name = nick_name # 用户昵称 24 | self.damai_url = damai_url # 大麦网官网网址 25 | self.target_url = target_url # 目标购票网址 26 | self.driver_path = driver_path # 浏览器驱动地址 27 | self.driver = None 28 | 29 | def isClassPresent(self, item, name, ret=False): 30 | try: 31 | result = item.find_element_by_class_name(name) 32 | if ret: 33 | return result 34 | else: 35 | return True 36 | except: 37 | return False 38 | 39 | # 获取账号的cookie信息 40 | def get_cookie(self): 41 | self.driver.get(self.damai_url) 42 | print(u"###请点击登录###") 43 | self.driver.find_element_by_class_name('login-user').click() 44 | while self.driver.title.find('大麦网-全球演出赛事官方购票平台') != -1: # 等待网页加载完成 45 | sleep(1) 46 | print(u"###请扫码登录###") 47 | while self.driver.title == '大麦登录': # 等待扫码完成 48 | sleep(1) 49 | dump(self.driver.get_cookies(), open("cookies.pkl", "wb")) 50 | print(u"###Cookie保存成功###") 51 | 52 | def set_cookie(self): 53 | try: 54 | cookies = load(open("cookies.pkl", "rb")) # 载入cookie 55 | for cookie in cookies: 56 | cookie_dict = { 57 | 'domain':'.damai.cn', # 必须有,不然就是假登录 58 | 'name': cookie.get('name'), 59 | 'value': cookie.get('value'), 60 | "expires": "", 61 | 'path': '/', 62 | 'httpOnly': False, 63 | 'HostOnly': False, 64 | 'Secure': False} 65 | self.driver.add_cookie(cookie_dict) 66 | print(u'###载入Cookie###') 67 | except Exception as e: 68 | print(e) 69 | 70 | def login(self): 71 | print(u'###开始登录###') 72 | self.driver.get(self.target_url) 73 | WebDriverWait(self.driver, 10, 0.1).until(EC.title_contains('大麦网')) 74 | self.set_cookie() 75 | 76 | def enter_concert(self): 77 | print(u'###打开浏览器,进入大麦网###') 78 | if not exists('cookies.pkl'): # 如果不存在cookie.pkl,就获取一下 79 | self.driver = webdriver.Chrome(executable_path=self.driver_path) 80 | self.get_cookie() 81 | print(u'###成功获取Cookie,重启浏览器###') 82 | self.driver.quit() 83 | 84 | options = webdriver.ChromeOptions() 85 | # 禁止图片、js、css加载 86 | prefs = {"profile.managed_default_content_settings.images": 2, 87 | "profile.managed_default_content_settings.javascript": 1, 88 | 'permissions.default.stylesheet': 2} 89 | options.add_experimental_option("prefs", prefs) 90 | 91 | # 更换等待策略为不等待浏览器加载完全就进行下一步操作 92 | capa = DesiredCapabilities.CHROME 93 | capa["pageLoadStrategy"] = "none" 94 | self.driver = webdriver.Chrome(executable_path=self.driver_path, options=options, desired_capabilities=capa) 95 | # 登录到具体抢购页面 96 | self.login() 97 | self.driver.refresh() 98 | try: 99 | # 等待nickname出现 100 | locator = (By.XPATH, "/html/body/div[1]/div/div[3]/div[1]/a[2]/div") 101 | WebDriverWait(self.driver, 5, 0.3).until(EC.text_to_be_present_in_element(locator, self.nick_name)) 102 | self.status = 1 103 | print(u"###登录成功###") 104 | self.time_start = time() 105 | except: 106 | self.status = 0 107 | self.driver.quit() 108 | raise Exception(u"***错误:登录失败,请删除cookie后重试***") 109 | 110 | # 实现购买函数 111 | def choose_ticket(self): 112 | print(u"###进入抢票界面###") 113 | while self.driver.title.find('确认订单') == -1: # 如果跳转到了确认界面就算这步成功了,否则继续执行此步 114 | self.num += 1 #尝试次数加1 115 | 116 | if con.driver.current_url.find("buy.damai.cn") != -1: 117 | break 118 | 119 | # 确认页面刷新成功 120 | try: 121 | box = WebDriverWait(self.driver, 1, 0.1).until(EC.presence_of_element_located((By.CLASS_NAME, 'perform__order__box'))) 122 | except: 123 | raise Exception(u"***Error: 页面刷新出错***") 124 | 125 | try: 126 | realname_popup = box.find_elements_by_xpath("//div[@class='realname-popup']") # 寻找实名身份遮罩 127 | if len(realname_popup) != 0: 128 | known_button = realname_popup[0].find_elements_by_xpath("//div[@class='operate']//div[@class='button']") 129 | known_button[0].click() 130 | except: 131 | raise Exception(u"***Error: 实名制遮罩关闭失败***") 132 | 133 | try: 134 | buybutton = box.find_element_by_class_name('buybtn') # 寻找立即购买标签 135 | buybutton_text = buybutton.text 136 | except: 137 | raise Exception(u"***Error: buybutton 位置找不到***") 138 | 139 | if buybutton_text == "即将开抢" or buybutton_text == "即将开售": 140 | self.status = 2 141 | raise Exception(u"---尚未开售,刷新等待---") 142 | 143 | try: 144 | selects = box.find_elements_by_class_name('perform__order__select') # 日期、场次和票档进行定位 145 | date = None # 有的演出没有日期的选项 146 | for item in selects: 147 | if item.find_element_by_class_name('select_left').text == '日期': 148 | date = item 149 | # print('\t日期定位成功') 150 | elif item.find_element_by_class_name('select_left').text == '场次': 151 | session = item 152 | # print('\t场次定位成功') 153 | elif item.find_element_by_class_name('select_left').text == '票档': 154 | price = item 155 | # print('\t票档定位成功') 156 | 157 | if date is not None: 158 | date_list = date.find_elements_by_xpath("//div[@class='wh_content_item']//div[starts-with(@class,'wh_item_date')]") #选定日期 159 | # print('可选日期数量为:{}'.format(len(date_list))) 160 | for i in self.date: 161 | j = date_list[i-1] 162 | j.click() 163 | break 164 | 165 | session_list = session.find_elements_by_class_name('select_right_list_item')#选定场次 166 | # print('可选场次数量为:{}'.format(len(session_list))) 167 | for i in self.session: # 根据优先级选择一个可行场次 168 | j = session_list[i-1] 169 | k = self.isClassPresent(j, 'presell', True) 170 | if k: # 如果找到了带presell的类 171 | if k.text == '无票': 172 | continue 173 | elif k.text == '预售': 174 | j.click() 175 | break 176 | elif k.text == '惠': 177 | j.click() 178 | break 179 | else: 180 | j.click()# 选定好场次点击按钮确定 181 | break 182 | 183 | price_list = price.find_elements_by_class_name('select_right_list_item')#选定票档 184 | # print('可选票档数量为:{}'.format(len(price_list))) 185 | for i in self.price: 186 | j = price_list[i-1] 187 | k = self.isClassPresent(j, 'notticket') 188 | if k: # 存在notticket代表存在缺货登记,跳过 189 | continue 190 | else: 191 | j.click()#选定好票档点击确定 192 | break 193 | except: 194 | raise Exception(u"***Error: 选择日期or场次or票档不成功***") 195 | 196 | try: 197 | ticket_num_up = box.find_element_by_class_name('cafe-c-input-number-handler-up') 198 | except: 199 | if buybutton_text == "选座购买": # 选座购买没有增减票数键 200 | buybutton.click() 201 | self.status = 5 202 | print(u"###请自行选择位置和票价###") 203 | break 204 | elif buybutton_text == "提交缺货登记": 205 | raise Exception(u'###票已被抢完,持续捡漏中...或请关闭程序并手动提交缺货登记###') 206 | else: 207 | raise Exception(u"***Error: ticket_num_up 位置找不到***") 208 | 209 | if buybutton_text == "立即预订": 210 | for i in range(self.ticket_num-1): # 设置增加票数 211 | ticket_num_up.click() 212 | buybutton.click() 213 | self.status = 3 214 | 215 | elif buybutton_text == "立即购买": 216 | for i in range(self.ticket_num-1): # 设置增加票数 217 | ticket_num_up.click() 218 | buybutton.click() 219 | self.status = 4 220 | 221 | def check_order(self): 222 | if self.status in [3, 4, 5]: 223 | if self.real_name is not None: 224 | print(u"###等待--确认订单--页面出现,可自行刷新,若长期不跳转可选择-- CRTL+C --重新抢票###") 225 | try: 226 | tb = WebDriverWait(self.driver, 1, 0.1).until(EC.presence_of_element_located((By.XPATH, '/html/body/div[3]/div[2]/div'))) 227 | except: 228 | raise Exception(u"***Error:实名信息选择框没有显示***") 229 | 230 | print(u'###开始确认订单###') 231 | print(u'###选择购票人信息,可手动帮助点击###') 232 | init_sleeptime = 0.0 233 | Labels = tb.find_elements_by_tag_name('label') 234 | 235 | # 防止点击过快导致没有选择多个人 236 | while True: 237 | init_sleeptime += 0.1 238 | true_num = 0 239 | for num_people in self.real_name: 240 | tag_input = Labels[num_people-1].find_element_by_tag_name('input') 241 | if tag_input.get_attribute('aria-checked') == 'false': 242 | sleep(init_sleeptime) 243 | tag_input.click() 244 | else: 245 | true_num += 1 246 | if true_num == len(self.real_name): 247 | break 248 | print("本次抢票时间:", time()-self.time_start) 249 | self.driver.find_element_by_xpath('/html/body/div[3]/div[2]/div/div[9]/button').click() # 同意以上协议并提交订单 250 | 251 | else: 252 | self.driver.find_element_by_xpath('/html/body/div[2]/div[2]/div/div[8]/button').click() 253 | 254 | # 判断title是不是支付宝 255 | print(u"###等待跳转到--付款界面--,可自行刷新,若长期不跳转可选择-- CRTL+C --重新抢票###") 256 | try: 257 | WebDriverWait(self.driver, 3600, 0.1).until(EC.title_contains('支付宝')) 258 | except: 259 | raise Exception(u'***Error: 长期跳转不到付款界面***') 260 | 261 | self.status = 6 262 | print(u'###成功提交订单,请手动支付###') 263 | self.time_end = time() 264 | 265 | 266 | if __name__ == '__main__': 267 | try: 268 | with open('./config.json', 'r', encoding='utf-8') as f: 269 | config = loads(f.read()) 270 | # params: 场次优先级,票价优先级,实名者序号, 用户昵称, 购买票数, 官网网址, 目标网址, 浏览器驱动地址 271 | con = Concert(config['date'], config['sess'], config['price'], config['real_name'], config['nick_name'], config['ticket_num'], config['damai_url'], config['target_url'], config['driver_path']) 272 | con.enter_concert() #进入到具体抢购页面 273 | except Exception as e: 274 | print(e) 275 | exit(1) 276 | while True: 277 | try: 278 | con.choose_ticket() 279 | con.check_order() 280 | except Exception as e: 281 | con.driver.get(con.target_url) 282 | print(e) 283 | continue 284 | 285 | if con.status == 6: 286 | print(u"###经过%d轮奋斗,共耗时%.1f秒,抢票成功!请确认订单信息###" % (con.num, round(con.time_end-con.time_start, 3))) 287 | break 288 | --------------------------------------------------------------------------------