├── .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 |
--------------------------------------------------------------------------------