├── .DS_Store ├── .gitignore ├── README.md ├── auto_run.py ├── config.py ├── cookies.py ├── html_parser.py ├── img ├── .DS_Store ├── 1.png ├── 2.png ├── 3.png ├── line_notification.jpeg ├── line_token_screenshots │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ └── 5.png ├── op.png ├── unzip.png ├── url_example.png ├── v2_1-interactive_interface.gif └── v2_2-interactive_interface.gif ├── main.py ├── requests_operations.py └── requirements.txt /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | venv 3 | __pycache__ 4 | dist 5 | build 6 | *.spec 7 | *.json 8 | *.zip 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ticket Refresh 自動查看餘票 2 | 3 | ## 說明 4 | > 監控拓元售票系統的餘券狀況,可以依照需求選擇跳窗提醒或是連動LINE Notify 5 | 6 | ### 使用source code 執行(建議使用虛擬環境) 7 | 1. 8 | ``` 9 | conda create -n py311 python=3.11 #conda建立虛擬環境 10 | conda activate py311 11 | ``` 12 | 2. 13 | ``` 14 | pip3 install -r requirements.txt 15 | ``` 16 | 17 | 3. 18 | ``` 19 | python3 main.py 20 | ``` 21 | 22 | 4. 依據終端機的詢問完成設定 23 | 5. 產生config.json檔並開始監控 24 | 25 | ### 執行 26 | > 執行檔已經停止維護,若要使用最新版本請用source code執行 27 | 28 | 1. 請先至 [Releases](https://github.com/showaykerker/ticket_refresh/releases/tag/v2.1) 下載對應的壓縮檔並解壓縮 29 | * mac M1/M2 請選擇 “TicketMonitor-v2-arm.zip” 30 | * mac intel請選擇 “TicketMonitor-v2_2-x86_64.zip 31 | 2. 解壓縮後按著鍵盤control點擊TicketMonitor開啟,否則系統會因不信任而無法執行 32 | 33 | 34 | 35 | #### 設定通知方式、監看活動、監看範圍 36 | * 找好要監看的網址,e.g. https://tixcraft.com/ticket/area/23_reneliu/14411 37 | 38 | 39 | * 跟著終端機中的提示輸入通知方式、監看的活動和範圍 40 | * 若需要重新設定可以直接更改config.json或是刪除後重新跑執行檔生成 41 | * 若需要使用Line通知,請參考[#設定line通知](#設定line通知) 42 | * 若沒票會顯示XX區域No tickets available,幾秒過後會再重新載入一次,直到刷到票會依據設定跳出剩餘票券的視窗或是傳送Line通知 43 | 44 | 45 | #### 設定Line通知 46 | 1. 進入[Line Notify](https://notify-bot.line.me/my/) 47 | 48 | 3. 點選「發行權杖」 49 | 50 | 4. 權杖名稱輸入「餘票通知」 51 | 52 | 5. 聊天室選擇「透過1對1聊天接收Line Notify的通知」 53 | 54 | 6. 記下權杖 55 | 56 | 57 | 58 | 59 | ### Note 60 | * 開啟後需要一點時間載入! 61 | * 這支程式只能監控有分區域的票種,可以看下面範例圖,若是單一票種沒有分區域則不適用 62 | * 若執行檔可以成功執行,可將TicketMonitor和config.json以外的檔案都清除 63 | 64 | ### 進階使用 65 | 66 | #### 以config檔設定網址和監聽範圍 67 | 在 `config.json` 裡的"target"欄位分別填入 68 | 1. "url": 查詢到的網址,須包含雙引號 69 | 2. "group_ids": 需要監看的票區,格式為 `["group_#", "group_#"]` 70 | 71 | #### Build for x86_64 with config file using pyinstaller 72 | 1. 73 | ``` 74 | virtualenv ticket_env #建立虛擬環境 75 | win: ticket_env\Scripts\activate #進入虛擬環境 76 | macOS: source ./ticket_env/bin/activate 77 | ``` 78 | 2. 79 | ``` 80 | pip3 install -r requirements.txt 81 | ``` 82 | 3. 83 | ``` 84 | pyinstaller --clean --onefile main.py 85 | ``` 86 | 87 | ### TODO 88 | - [ ] AWS distribution guide. 89 | - [ ] 其他售票網站 90 | - [ ] 其他通知方式 91 | - [x] 詢問是否使用舊的config,若否詢問是否沿用權杖 -------------------------------------------------------------------------------- /auto_run.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import requests 3 | from typing import Dict 4 | from bs4 import BeautifulSoup 5 | from PyQt6 import QtWidgets, QtGui 6 | from PyQt6.QtCore import * 7 | 8 | import html_parser as parser 9 | from requests_operations import send_line_msg 10 | try: 11 | from fake_useragent import UserAgent 12 | except: 13 | pass 14 | 15 | class AutoRun: 16 | 17 | def __init__( 18 | self, 19 | config: dict) -> None: 20 | self.header = self.read_header() 21 | self.target = config.get("target") 22 | self.url = self.target.get("url") 23 | self.group_ids = self.target.get("group_ids") 24 | self.title = None 25 | self.notification_type = config.get("notification_type") or {} 26 | self.token = config.get("token") or {} 27 | 28 | def read_header(self) -> str: 29 | try: 30 | ua = UserAgent(use_external_data=True) 31 | user_agent = ua.random 32 | return user_agent 33 | except: 34 | pass 35 | 36 | def get_header(self) -> dict: 37 | try: 38 | return {'User-Agent': self.header} 39 | except: pass 40 | 41 | def __del__(self) -> None: 42 | if self.notification_type.get("line"): 43 | send_line_msg( 44 | self.token["line"], 45 | self.title, 46 | "結束監看餘票。\n", 47 | self.url) 48 | 49 | def has_ticket_alarm(self, ts:list) -> None: 50 | if self.notification_type.get("line"): 51 | self._has_ticket_alarm_line(ts) 52 | if self.notification_type.get("window"): 53 | self._has_ticket_alarm_qt(ts) 54 | 55 | def _has_ticket_alarm_line(self, ts:list=[], msg_overwrite="") -> None: 56 | remain_ticket = [] 57 | for t in ts: 58 | if 'remain' in t: 59 | remain_ticket.append(f''.join(t)) 60 | msg_body = "".join(remain_ticket) 61 | send_line_msg(self.token["line"], self.title, msg_body, self.url) 62 | 63 | def _has_ticket_alarm_qt(self, ts: list) -> None: 64 | app = QtWidgets.QApplication(sys.argv) 65 | Form = QtWidgets.QWidget() 66 | Form.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint) 67 | Form.setWindowTitle('ticket') 68 | 69 | # 設置窗口大小 70 | Form.resize(500, 400) 71 | scroll_area = QtWidgets.QScrollArea(Form) 72 | scroll_area.setGeometry(0, 0, Form.width(), Form.height()) # 設置滾動區域的大小 73 | scroll_area.setWidgetResizable(True) 74 | remain_ticket = [] 75 | label = QtWidgets.QLabel(scroll_area) 76 | for t in ts: 77 | if 'remain' in t or 'Available' in t or '剩餘' in t or '熱賣中' in t or '席残り' in t or '大ヒット発売中' in t: 78 | remain_ticket.append( 79 | ''+f''.join(t)+'
') 80 | 81 | label.setText(f''.join(remain_ticket)) 82 | font = QtGui.QFont() 83 | font.setPointSize(20) 84 | font.setBold(True) 85 | label.setFont(font) 86 | 87 | scroll_area.setWidget(label) 88 | scroll_area.show() 89 | Form.show() 90 | sys.exit(app.exec()) 91 | 92 | def check_ticket_status(self, soup: BeautifulSoup): 93 | if self.title is None: 94 | self.title = parser.parse_title(soup) 95 | send_line_msg( 96 | self.token["line"], 97 | self.title, 98 | "開始監看餘票:)\n", 99 | self.url) 100 | ticket_status = soup.find('div', class_='zone area-list') 101 | ticket_status_list = [] 102 | find_flag = False 103 | # print(ticket_status) 104 | for group_id in self.group_ids: 105 | try: 106 | group_ul = ticket_status.find('ul', id=group_id) 107 | lis = group_ul.find_all('li') 108 | for li in lis: 109 | # print(li.text) 110 | ticket_status_list.append(li.text + '\n') 111 | if 'out' not in li.text and '已售完' not in li.text and '完売した' not in li.text: 112 | find_flag = True 113 | except: 114 | return True, ticket_status_list, True # 當找不到tag返回out_of_range==True 115 | 116 | if find_flag: 117 | self.has_ticket_alarm(ticket_status_list) 118 | return False, ticket_status_list, False 119 | else: 120 | return True, ticket_status_list, False 121 | 122 | if __name__ == '__main__': 123 | # url = "https://tixcraft.com/ticket/area/23_more2come/14445" 124 | url = "https://tixcraft.com/ticket/area/23_reneliu/14411" 125 | ar = AutoRun(url, {"target": {}, "notification_type": {}, "token": {}}) 126 | response = requests.get( 127 | url, headers=ar.get_header()) 128 | ar.parse_zone_info(BeautifulSoup(response.text, "html.parser")) 129 | """ 130 | # 模擬假資料 131 | fake_data = ["remain_ticket_1", "remain_ticket_2", "out_of_range_ticket"] 132 | # 初始化 AutoRun 物件 133 | ar = AutoRun({"target": {}, "notification_type": {"window": True}, "token": {}}) 134 | # 觸發 _has_ticket_alarm_qt 方法 135 | ar._has_ticket_alarm_qt(fake_data) 136 | # 或者,您也可以直接觸發 has_ticket_alarm 方法,它會選擇性地呼叫 _has_ticket_alarm_qt 137 | ar.has_ticket_alarm(fake_data) 138 | """ -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Dict, List, Tuple 3 | 4 | import questionary 5 | 6 | from requests_operations import validate_line_token, validate_url 7 | from requests_operations import request 8 | import html_parser as parser 9 | 10 | def pretty_print_config(config): 11 | config = validate_config(config) 12 | notification_type = "通知方式: " 13 | if config["notification_type"]["window"]: 14 | notification_type += "跳出視窗通知 " 15 | if config["notification_type"]["line"]: 16 | notification_type += "傳送Line通知 (權杖已驗證)" 17 | questionary.print(notification_type, style="bold fg:lightgreen") 18 | 19 | title = config["target"].get("title") 20 | if title: 21 | questionary.print(f"監控活動:{title}", style="bold fg:lightgreen") 22 | zone_verboses = config["target"].get("zone_verboses") 23 | if zone_verboses: 24 | questionary.print(f"監控票區:{zone_verboses}", style="bold fg:lightgreen") 25 | 26 | def validate_config(config): 27 | """ 28 | { 29 | "notification_type": { 30 | "window": false, 31 | "line": true 32 | }, 33 | "token": { 34 | "line": "" 35 | }, 36 | "target": { 37 | "url": "", 38 | "group_ids": [] 39 | } 40 | } 41 | """ 42 | # Existance Check 43 | if "notification_type" not in config.keys(): 44 | config["notification_type"] = ask_for_notification_type() 45 | if "token" not in config.keys(): 46 | config["token"] = {} 47 | if "target" not in config.keys(): 48 | config["target"] = ask_for_target() 49 | if "url" not in config["target"]: 50 | config["target"] = ask_for_target() 51 | if "group_ids" not in config["target"]: 52 | config["target"] = ask_for_target(config["target"]["url"]) 53 | 54 | # Value Check 55 | if config["notification_type"]["line"] and not config["token"].get("line"): 56 | notification_type, line_token = ask_for_line_token(config["notification_type"], True) 57 | config["notification_type"] = notification_type 58 | config["token"]["line"] = line_token 59 | if not config["target"].get("url"): 60 | config["target"] = ask_for_target() 61 | if not len(config["target"].get("group_ids")): 62 | config["target"] = ask_for_target(config["target"]["url"]) 63 | 64 | # Validate 65 | if config["notification_type"]["line"]: 66 | valid, reason = validate_line_token(config["token"]["line"]) 67 | if not valid: 68 | questionary.print( 69 | "設定的權杖無效,請從 https://notify-bot.line.me/my/ 申請或是稍後修改config.json檔", 70 | style="bold italic fg:red") 71 | notification_type, line_token = ask_for_line_token(config["notification_type"], True) 72 | config["notification_type"] = notification_type 73 | config["token"]["line"] = line_token 74 | valid, reason = validate_url(config["target"]["url"]) 75 | if not valid: 76 | questionary.print( 77 | "設定的網址無效,請重新輸入", 78 | style="bold italic fg:red") 79 | config["target"] = ask_for_target() 80 | return config 81 | 82 | def ask_for_notification_type() -> dict: 83 | answer = questionary.checkbox( 84 | '選擇通知方式 (上下鍵移動|按空白鍵勾選(可複選)|Enter確認)', 85 | choices = [ 86 | "跳出視窗通知", 87 | "傳送Line通知" 88 | ] 89 | ).ask() 90 | return { 91 | "window": "跳出視窗通知" in answer, 92 | "line": "傳送Line通知" in answer 93 | } 94 | 95 | def ask_for_line_token(notification_type, required=False) -> Tuple[dict, str]: 96 | 97 | while True: 98 | line_token = questionary.password( 99 | "輸入Line權杖(可從 https://notify-bot.line.me/my/ 申請或是按Enter跳過):" 100 | ).skip_if(not notification_type.get("line")).ask() or "" 101 | 102 | line_token = line_token.strip() 103 | line_token_valid, reason = validate_line_token(line_token) 104 | 105 | if line_token_valid: 106 | questionary.print(">>> 權杖驗證成功!", style="bold fg:lightyellow") 107 | break 108 | 109 | if required: 110 | cancel = questionary.confirm("權杖無效,是否不使用Line通知?").ask() 111 | 112 | if not required or cancel: 113 | if line_token: 114 | questionary.print( 115 | "權杖無效,請從 https://notify-bot.line.me/my/ 申請或是稍後修改config.json檔", 116 | style="bold italic fg:red") 117 | line_token = "" 118 | notification_type["line"] = False 119 | break 120 | 121 | questionary.print( 122 | "權杖無效。請重新輸入", 123 | style="bold italic fg:red") 124 | 125 | return notification_type, line_token 126 | 127 | 128 | def ask_for_target(url="") -> dict: 129 | while True: 130 | if not url: 131 | url = questionary.text( 132 | "輸入網址:" 133 | ).ask() 134 | url = url.strip() 135 | url_valid, reason = validate_url(url) 136 | if not url_valid: 137 | questionary.print( 138 | f"網址無效 ({url}, {reason}),請重新輸入", 139 | style="bold italic fg:red") 140 | url = "" 141 | else: 142 | soup = request(url) 143 | title = parser.parse_title(soup) 144 | if questionary.confirm(f"確認監控{title}嗎?").ask(): 145 | break 146 | url = "" 147 | 148 | zone_info_dict = parser.parse_zone_info(soup) 149 | group_ids, zone_verboses = ask_for_region(zone_info_dict) 150 | return { 151 | "url": url, 152 | "group_ids": group_ids, 153 | "title": title, 154 | "zone_verboses": zone_verboses 155 | } 156 | 157 | def ask_for_region(regions: Dict[str, str]) -> Tuple[List[str], List[str]]: 158 | """ 159 | regions format 160 | verbose_region: group_# 161 | e.g. "一樓站區 已售完": group_22542 162 | """ 163 | keys = questionary.checkbox( 164 | "選擇要監控的範圍 (上下鍵移動|按空白鍵勾選(可複選)|Enter確認)", 165 | regions.keys()).ask() 166 | return [regions[k] for k in keys], keys 167 | 168 | def create_config_content() -> dict: 169 | questionary.print( 170 | "現在開始創建設定檔", 171 | style="bold fg:lightgreen") 172 | 173 | notification_type = ask_for_notification_type() 174 | 175 | notification_type, line_token = ask_for_line_token(notification_type) 176 | 177 | target = {"url": "", "group_ids": []} 178 | if questionary.confirm("是否預先設定監控網址及區間?").ask(): 179 | target = ask_for_target() 180 | 181 | config = { 182 | "notification_type": notification_type, 183 | "token": 184 | { 185 | "line": line_token or "" 186 | }, 187 | "target": target 188 | } 189 | 190 | # questionary.print(f"\n設定檔:\n{json.dumps(config, indent=3)}\n", style="bold fg:lightblue") 191 | return config 192 | 193 | # 判斷是否沿用舊的設定 194 | def ask_for_reset(config): 195 | with open(config, "r") as f: 196 | config_data = json.load(f) 197 | title = config_data["target"].get("title") 198 | if title: 199 | questionary.print(f"監控:{title}中", style="bold fg:lightgreen") 200 | zone_verboses = config_data["target"].get("zone_verboses") 201 | if zone_verboses: 202 | questionary.print(f"監控票區:{zone_verboses}", style="bold fg:lightgreen") 203 | 204 | reset_config = 0 # 預設值為 0 205 | if not questionary.confirm("請問是否繼續使用此設定檔?").ask(): 206 | questionary.print("建立新的設定檔...", style="bold fg:lightgreen") 207 | reset_config = 1 208 | 209 | return reset_config 210 | 211 | 212 | 213 | if __name__ == "__main__": 214 | create_config_content() -------------------------------------------------------------------------------- /cookies.py: -------------------------------------------------------------------------------- 1 | # Ref: https://stackoverflow.com/questions/14742899/using-cookies-txt-file-with-python-requests 2 | # Use this extension: https://chromewebstore.google.com/detail/open-cookiestxt/gdocmgbfkjnnpapoeobnolbbkoibbcif 3 | # to extract your cookies into a file called tixcraft.com_cookies.txt 4 | # It's ok to disable or uninstall the extension once you extract the cookies. 5 | 6 | import re 7 | import os.path 8 | 9 | def parse_cookie_file(cookie_file): 10 | """ 11 | Parse a cookies.txt file and return a dictionary of key value pairs 12 | compatible with requests. 13 | """ 14 | 15 | cookies = {} 16 | with open (cookie_file, 'r') as fp: 17 | for line in fp: 18 | if not re.match(r'^\#', line): 19 | line_fields = re.findall(r'[^\s]+', line) #capturing anything but empty space 20 | try: 21 | cookies[line_fields[5]] = line_fields[6] 22 | except Exception as e: 23 | # print (e) 24 | # Some fields are not long enough but it is ok to ignore them 25 | pass 26 | 27 | return cookies 28 | 29 | def get_cookies(): 30 | cookies = parse_cookie_file('tixcraft.com_cookies.txt') if os.path.isfile('tixcraft.com_cookies.txt') else dict() 31 | return cookies -------------------------------------------------------------------------------- /html_parser.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from bs4 import BeautifulSoup 3 | 4 | def parse_title(soup:BeautifulSoup) -> None: 5 | h2s = soup.find_all('h2', {"class": "activityT title"}) 6 | if h2s: 7 | return h2s[0].text 8 | return None 9 | 10 | def parse_zone_info(soup:BeautifulSoup) -> Dict[str, str]: 11 | """ 12 | return zone_list/area_list format 13 | verbose_area_list: group_# 14 | e.g. "一樓站區 已售完": group_22542 15 | """ 16 | zone_area_list = soup.find("div", {"class": "zone area-list"}) 17 | zones = zone_area_list.find_all("div", {"class": "zone-label"}) 18 | if zones: 19 | # Multiple zones 20 | zone_group_dict = {} 21 | for zone in zones: 22 | group_id = zone.attrs["data-id"] 23 | zone_label = zone.find("b").contents[0] 24 | zone_group_dict[zone_label] = group_id 25 | return zone_group_dict 26 | else: 27 | # 1 zone 28 | area_list = zone_area_list.find_all("ul", {"class": "area-list"}) 29 | # each ul is an area 30 | area_group_dict = {} 31 | for area in area_list: 32 | group_id = area.attrs["id"] 33 | area_label = area.find("font").contents[0] 34 | area_group_dict[area_label] = group_id 35 | return area_group_dict 36 | 37 | -------------------------------------------------------------------------------- /img/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/.DS_Store -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/1.png -------------------------------------------------------------------------------- /img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/2.png -------------------------------------------------------------------------------- /img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/3.png -------------------------------------------------------------------------------- /img/line_notification.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/line_notification.jpeg -------------------------------------------------------------------------------- /img/line_token_screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/line_token_screenshots/1.png -------------------------------------------------------------------------------- /img/line_token_screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/line_token_screenshots/2.png -------------------------------------------------------------------------------- /img/line_token_screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/line_token_screenshots/3.png -------------------------------------------------------------------------------- /img/line_token_screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/line_token_screenshots/4.png -------------------------------------------------------------------------------- /img/line_token_screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/line_token_screenshots/5.png -------------------------------------------------------------------------------- /img/op.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/op.png -------------------------------------------------------------------------------- /img/unzip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/unzip.png -------------------------------------------------------------------------------- /img/url_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/url_example.png -------------------------------------------------------------------------------- /img/v2_1-interactive_interface.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/v2_1-interactive_interface.gif -------------------------------------------------------------------------------- /img/v2_2-interactive_interface.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AnselCh/ticket_refresh/41f22c6e51d55cafabc142c3fd7426321082008b/img/v2_2-interactive_interface.gif -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | import json 4 | from datetime import datetime 5 | import sys 6 | import os.path 7 | 8 | import requests 9 | from bs4 import BeautifulSoup 10 | 11 | from auto_run import AutoRun 12 | import config as cfg 13 | import requests_operations as req 14 | 15 | def get_config_path(): 16 | # CWD = os.path.abspath(os.path.dirname(sys.executable)) #py解釋器的執行檔路徑 17 | CWD = os.getcwd() # 使用 os.getcwd() 獲取當前工作目錄 18 | config_path = os.path.join(CWD, "config.json") 19 | if "venv" in config_path: # assume running in devel mode 20 | print("=== RUNNING IN DEVELOP MODE ===") 21 | config_path = "config.json" 22 | print(f"config path: {config_path}") 23 | return config_path 24 | 25 | def get_config(config_path): 26 | if not os.path.isfile(config_path): 27 | config = cfg.create_config_content() 28 | else: 29 | reset_config = cfg.ask_for_reset(config_path) 30 | if not reset_config: 31 | with open(config_path, "r") as f: 32 | return json.load(f) 33 | else: 34 | config = cfg.create_config_content() 35 | return config 36 | 37 | 38 | def save_config(config: dict, config_path): 39 | with open(config_path, "w") as f: 40 | f.write(json.dumps(config, indent=4)) 41 | 42 | def get_value_or_input(target, field, input_question): 43 | val = target.get(field) 44 | valid = False 45 | if type(val) is int: 46 | valid = True if val >= 0 else False 47 | if type(val) is str: 48 | valid = True if val else False 49 | if valid: 50 | return val 51 | 52 | answer = input(input_question) 53 | try: 54 | return int(answer) 55 | except ValueError: 56 | return answer 57 | 58 | def main(): 59 | config_path = get_config_path() 60 | config = get_config(config_path) 61 | not_find_flag = True 62 | out_of_range = False 63 | 64 | config = cfg.validate_config(config) 65 | save_config(config, config_path) 66 | target = config.get("target") 67 | url = target.get("url") 68 | 69 | cfg.pretty_print_config(config) 70 | 71 | ar = AutoRun(config) 72 | 73 | while not_find_flag: 74 | #while True: # 若要持續搜尋 75 | 76 | print('connecting...') 77 | response = req.request(url, headers=ar.get_header()) 78 | 79 | not_find_flag, ts, out_of_range = ar.check_ticket_status(response) 80 | if out_of_range: 81 | print('請檢查輸入區間是否有誤') 82 | break 83 | 84 | print("The current time is", datetime.now().strftime("%H:%M:%S")) 85 | print(f''.join(ts)) 86 | 87 | time.sleep(random.randint(1, 11)) 88 | 89 | 90 | if __name__ == '__main__': 91 | main() 92 | -------------------------------------------------------------------------------- /requests_operations.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import random 3 | import time 4 | from typing import Tuple 5 | 6 | from bs4 import BeautifulSoup 7 | 8 | import html_parser as parser 9 | 10 | from cookies import get_cookies 11 | 12 | def _validate_string(s: str, from_="") -> Tuple[bool, str]: 13 | if type(s) is not str: 14 | return False, f"type({s}) is not str, {type(s)} instead" 15 | if len(s) == 0: 16 | return False, f"len({s}) is 0" 17 | return True, "" 18 | 19 | def validate_url(url: str) -> Tuple[bool, str]: 20 | valid, reason = _validate_string(url, "url") 21 | if not valid: 22 | return valid, reason 23 | if not "tixcraft.com/ticket/" in url: 24 | return False, "只支援拓元售票系統(https://tixcraft.com/ticket/ 開頭)的頁面" 25 | if not url.startswith("http") or len(url) < 14: 26 | return False, "Not done yet." 27 | try: 28 | r = requests.head(url, cookies=get_cookies()) 29 | except Exception as e: 30 | return False, e 31 | return r.status_code in [200, 302], f"{r.status_code}: {r.content}" 32 | 33 | def validate_line_token(token: str) -> Tuple[bool, str]: 34 | valid, reason = _validate_string(token, "url") 35 | if not valid: 36 | return valid, reason 37 | 38 | headers = { 39 | "Authorization": "Bearer " + token, 40 | } 41 | r = requests.get("https://notify-api.line.me/api/status", headers=headers) 42 | return r.status_code == 200, f"{r.status_code}: {r.content}" 43 | 44 | def request(url: str, headers: dict = {}) -> BeautifulSoup: 45 | while True: 46 | try: 47 | response = requests.get(url, headers=headers, cookies=get_cookies()).text 48 | return BeautifulSoup(response, "html.parser") 49 | except Exception as e: 50 | delay = random.randint(2, 5) 51 | print(f"Requesting {url} failed. Will try in {delay} seconds. ({e})") 52 | time.sleep(delay) 53 | 54 | def send_line_msg(token: str, title:str, msg_body: str, url: str) -> int: 55 | # https://notify-bot.line.me/my/ 56 | headers = { 57 | "Authorization": "Bearer " + token, 58 | "Content-Type" : "application/x-www-form-urlencoded" 59 | } 60 | 61 | msg = f"\n[餘票監看程式通知] \n\n{title}\n\n" 62 | msg += f"{msg_body}" 63 | msg += f"\n網址: {url}" 64 | 65 | payload = {'message': msg } 66 | r = requests.post("https://notify-api.line.me/api/notify", headers=headers, params=payload) 67 | return r.status_code 68 | 69 | if __name__ == "__main__": 70 | print(validate_url("https://tixcraft.com/ticket/area/23_tanya/13966")) 71 | print(validate_url("https://tixcraft.com/ticket/area/23_reneliu/14411")) 72 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | altgraph==0.17.3 2 | auto-py-to-exe==2.34.0 3 | beautifulsoup4==4.12.2 4 | bottle==0.12.25 5 | bottle-websocket==0.2.9 6 | certifi==2022.12.7 7 | charset-normalizer==3.1.0 8 | Eel==0.16.0 9 | fake-useragent==1.2.1 10 | future==0.18.3 11 | gevent==22.10.2 12 | gevent-websocket==0.10.1 13 | greenlet==2.0.2 14 | idna==3.4 15 | importlib-resources==5.12.0 16 | macholib==1.16.2 17 | prompt-toolkit==3.0.38 18 | pyinstaller==5.10.1 19 | pyinstaller-hooks-contrib==2023.2 20 | pyparsing==3.0.9 21 | PyQt6==6.6.1 22 | PyQt6-Qt6==6.6.1 23 | PyQt6-sip==13.6.0 24 | questionary==1.10.0 25 | requests==2.29.0 26 | soupsieve==2.4.1 27 | urllib3==1.26.15 28 | wcwidth==0.2.6 29 | whichcraft==0.6.1 30 | zipp==3.15.0 31 | zope.event==4.6 32 | zope.interface==6.0 33 | --------------------------------------------------------------------------------