Skip to content

Commit 26063f5

Browse files
author
Jalin
committed
增加 cdn 查询
1 parent 532e165 commit 26063f5

File tree

11 files changed

+2518
-23
lines changed

11 files changed

+2518
-23
lines changed

data/cdn.txt

Lines changed: 2220 additions & 0 deletions
Large diffs are not rendered by default.

main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import sys
33

44
from py12306.app import *
5+
from py12306.helpers.cdn import Cdn
56
from py12306.log.common_log import CommonLog
67
from py12306.query.query import Query
78
from py12306.user.user import User
@@ -20,6 +21,7 @@ def main():
2021

2122
####### 运行任务
2223
Web.run()
24+
Cdn.run()
2325
User.run()
2426
Query.run()
2527
if not Const.IS_TEST:

py12306/cluster/cluster.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ class Cluster():
2727
KEY_USER_LAST_HEARTBEAT = KEY_PREFIX + 'user_last_heartbeat'
2828
KEY_NODES_ALIVE = KEY_PREFIX + 'nodes_alive'
2929

30+
KEY_CDN_AVAILABLE_ITEMS = KEY_PREFIX + 'cdn_available_items'
31+
KEY_CDN_LAST_CHECK_AT = KEY_PREFIX + 'cdn_last_check_at'
32+
3033
# 锁
3134
KEY_LOCK_INIT_USER = KEY_PREFIX + 'lock_init_user' # 暂未使用
3235
KEY_LOCK_DO_ORDER = KEY_PREFIX + 'lock_do_order' # 订单锁

py12306/config.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ class Config:
8282
WEB_PORT = 8080
8383
WEB_ENTER_HTML_PATH = PROJECT_DIR + 'py12306/web/static/index.html'
8484

85+
# CDN
86+
CDN_ENABLED = 0
87+
CDN_ITEM_FILE = PROJECT_DIR + 'data/cdn.txt'
88+
CDN_ENABLED_AVAILABLE_ITEM_FILE = QUERY_DATA_DIR + 'available.json'
89+
8590
envs = []
8691
retry_time = 5
8792
last_modify_time = 0
@@ -164,18 +169,22 @@ def update_configs_from_remote(self, envs, first=False):
164169
if envs == self.envs: return
165170
from py12306.query.query import Query
166171
from py12306.user.user import User
172+
from py12306.helpers.cdn import Cdn
173+
self.envs = envs
167174
for key, value in envs:
168175
if key in self.disallow_update_configs: continue
169176
if value != -1:
170177
old = getattr(self, key)
171178
setattr(self, key, value)
172-
if not first:
173-
if key == 'USER_ACCOUNTS' and old != value:
179+
if not first and old != value:
180+
if key == 'USER_ACCOUNTS':
174181
User().update_user_accounts(auto=True, old=old)
175-
elif key == 'QUERY_JOBS' and old != value:
182+
elif key == 'QUERY_JOBS':
176183
Query().update_query_jobs(auto=True) # 任务修改
177-
elif key == 'QUERY_INTERVAL' and old != value:
184+
elif key == 'QUERY_INTERVAL':
178185
Query().update_query_interval(auto=True)
186+
elif key == 'CDN_ENABLED':
187+
Cdn().update_cdn_status(auto=True)
179188

180189
@staticmethod
181190
def is_master(): # 是不是 主
@@ -190,20 +199,16 @@ def is_slave(): # 是不是 从
190199
def is_cluster_enabled():
191200
return Config().CLUSTER_ENABLED
192201

193-
# @staticmethod
194-
# def get_members():
195-
# members = []
196-
# for name, value in vars(Config).items():
197-
# if name.isupper():
198-
# members.append(([name, value]))
199-
# return members
202+
@staticmethod
203+
def is_cdn_enabled():
204+
return Config().CDN_ENABLED
200205

201206

202207
class EnvLoader:
203208
envs = []
204209

205210
def __init__(self):
206-
self.envs = [] # 不是单例不初始化怎么还会有值
211+
self.envs = []
207212

208213
@classmethod
209214
def load_with_file(cls, file):

py12306/helpers/api.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
# 查询余票
33
import time
44

5-
BASE_URL_OF_12306 = 'https://kyfw.12306.cn'
5+
HOST_URL_OF_12306 = 'kyfw.12306.cn'
6+
BASE_URL_OF_12306 = 'https://' + HOST_URL_OF_12306
67

78
LEFT_TICKETS = {
89
"url": BASE_URL_OF_12306 + "/otn/{type}?leftTicketDTO.train_date={left_date}&leftTicketDTO.from_station={left_station}&leftTicketDTO.to_station={arrive_station}&purpose_codes=ADULT",
@@ -46,3 +47,5 @@
4647

4748
API_FREE_CODE_QCR_API = 'http://60.205.200.159/api'
4849
API_FREE_CODE_QCR_API_CHECK = 'http://check.huochepiao.360.cn/img_vcode'
50+
51+
API_CHECK_CDN_AVAILABLE = 'https://{}/otn/dynamicJs/omseuuq'

py12306/helpers/cdn.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import random
2+
import json
3+
from datetime import timedelta
4+
from os import path
5+
6+
from py12306.cluster.cluster import Cluster
7+
from py12306.config import Config
8+
from py12306.app import app_available_check
9+
from py12306.helpers.api import API_CHECK_CDN_AVAILABLE, HOST_URL_OF_12306
10+
from py12306.helpers.func import *
11+
from py12306.helpers.request import Request
12+
from py12306.log.common_log import CommonLog
13+
14+
15+
@singleton
16+
class Cdn:
17+
"""
18+
CDN 管理
19+
"""
20+
items = []
21+
available_items = []
22+
unavailable_items = []
23+
recheck_available_items = []
24+
recheck_unavailable_items = []
25+
retry_time = 3
26+
is_ready = False
27+
is_finished = False
28+
is_ready_num = 10 # 当可用超过 10,已准备好
29+
is_alive = True
30+
is_recheck = False
31+
32+
safe_stay_time = 0.2
33+
retry_num = 1
34+
thread_num = 8
35+
check_time_out = 3
36+
37+
last_check_at = 0
38+
save_second = 5
39+
check_keep_second = 60 * 60 * 24
40+
41+
def __init__(self):
42+
self.cluster = Cluster()
43+
create_thread_and_run(self, 'watch_cdn', False)
44+
45+
def init_data(self):
46+
self.items = []
47+
self.available_items = []
48+
self.unavailable_items = []
49+
self.is_finished = False
50+
self.is_ready = False
51+
self.is_recheck = False
52+
53+
def update_cdn_status(self, auto=False):
54+
if auto:
55+
if Config().is_cdn_enabled():
56+
self.run()
57+
else:
58+
self.destroy()
59+
60+
@classmethod
61+
def run(cls):
62+
self = cls()
63+
app_available_check()
64+
self.is_alive = True
65+
self.start()
66+
pass
67+
68+
def start(self):
69+
if not Config.is_cdn_enabled() or Config().is_slave(): return
70+
self.load_items()
71+
CommonLog.add_quick_log(CommonLog.MESSAGE_CDN_START_TO_CHECK.format(len(self.items))).flush()
72+
self.restore_items()
73+
for i in range(self.thread_num): # 多线程
74+
create_thread_and_run(jobs=self, callback_name='check_available', wait=Const.IS_TEST)
75+
76+
def load_items(self):
77+
with open(Config().CDN_ITEM_FILE, encoding='utf-8') as f:
78+
for line, val in enumerate(f):
79+
self.items.append(val.rstrip('\n'))
80+
81+
def restore_items(self):
82+
"""
83+
恢复已有数据
84+
:return: bool
85+
"""
86+
result = False
87+
if path.exists(Config().CDN_ENABLED_AVAILABLE_ITEM_FILE):
88+
with open(Config().CDN_ENABLED_AVAILABLE_ITEM_FILE, encoding='utf-8') as f:
89+
result = f.read()
90+
try:
91+
result = json.loads(result)
92+
except json.JSONDecodeError as e:
93+
result = {}
94+
95+
# if Config.is_cluster_enabled(): # 集群不用同步 cdn
96+
# result = self.get_data_from_cluster()
97+
98+
if result:
99+
self.last_check_at = result.get('last_check_at', '')
100+
if self.last_check_at: self.last_check_at = str_to_time(self.last_check_at)
101+
self.available_items = result.get('items', [])
102+
self.unavailable_items = result.get('fail_items', [])
103+
CommonLog.add_quick_log(CommonLog.MESSAGE_CDN_RESTORE_SUCCESS.format(self.last_check_at,
104+
self.last_check_at + timedelta(
105+
seconds=self.check_keep_second))).flush()
106+
return True
107+
return False
108+
109+
# def get_data_from_cluster(self):
110+
# available_items = self.cluster.session.smembers(Cluster.KEY_CDN_AVAILABLE_ITEMS)
111+
# last_time = self.cluster.session.get(Cluster.KEY_CDN_LAST_CHECK_AT, '')
112+
# if available_items and last_time:
113+
# return {'items': available_items, 'last_check_at': last_time}
114+
# return False
115+
116+
def is_need_to_recheck(self):
117+
"""
118+
是否需要重新检查 cdn
119+
:return:
120+
"""
121+
if self.last_check_at and (
122+
time_now() - self.last_check_at).seconds > self.check_keep_second:
123+
return True
124+
return False
125+
126+
def get_unchecked_item(self):
127+
if not self.is_recheck:
128+
items = list(set(self.items) - set(self.available_items) - set(self.unavailable_items))
129+
else:
130+
items = list(set(self.items) - set(self.recheck_available_items) - set(self.recheck_unavailable_items))
131+
if items: return random.choice(items)
132+
return None
133+
134+
def check_available(self):
135+
while True and self.is_alive:
136+
item = self.get_unchecked_item()
137+
if not item: return self.check_did_finished()
138+
self.check_item_available(item)
139+
140+
def watch_cdn(self):
141+
"""
142+
监控 cdn 状态,自动重新检测
143+
:return:
144+
"""
145+
while True:
146+
if self.is_alive and not self.is_recheck and self.is_need_to_recheck(): # 重新检测
147+
self.is_recheck = True
148+
self.is_finished = False
149+
CommonLog.add_quick_log(
150+
CommonLog.MESSAGE_CDN_START_TO_RECHECK.format(len(self.items), time_now())).flush()
151+
for i in range(self.thread_num): # 多线程
152+
create_thread_and_run(jobs=self, callback_name='check_available', wait=Const.IS_TEST)
153+
stay_second(self.retry_num)
154+
155+
def destroy(self):
156+
"""
157+
关闭 CDN
158+
:return:
159+
"""
160+
CommonLog.add_quick_log(CommonLog.MESSAGE_CDN_CLOSED).flush()
161+
self.is_alive = False
162+
self.init_data()
163+
164+
def check_item_available(self, item, try_num=0):
165+
session = Request()
166+
response = session.get(API_CHECK_CDN_AVAILABLE.format(item), headers={'Host': HOST_URL_OF_12306},
167+
timeout=self.check_time_out,
168+
verify=False)
169+
170+
if response.status_code == 200:
171+
if not self.is_recheck:
172+
self.available_items.append(item)
173+
else:
174+
self.recheck_available_items.append(item)
175+
if not self.is_ready: self.check_is_ready()
176+
elif try_num < self.retry_num: # 重试
177+
stay_second(self.safe_stay_time)
178+
return self.check_item_available(item, try_num + 1)
179+
else:
180+
if not self.is_recheck:
181+
self.unavailable_items.append(item)
182+
else:
183+
self.recheck_unavailable_items.append(item)
184+
if not self.is_recheck and (
185+
not self.last_check_at or (time_now() - self.last_check_at).seconds > self.save_second):
186+
self.save_available_items()
187+
stay_second(self.safe_stay_time)
188+
189+
def check_did_finished(self):
190+
self.is_ready = True
191+
if not self.is_finished:
192+
self.is_finished = True
193+
if self.is_recheck:
194+
self.is_recheck = False
195+
self.available_items = self.recheck_available_items
196+
self.unavailable_items = self.recheck_unavailable_items
197+
self.recheck_available_items = []
198+
self.recheck_unavailable_items = []
199+
CommonLog.add_quick_log(CommonLog.MESSAGE_CDN_CHECKED_SUCCESS.format(len(self.available_items))).flush()
200+
self.save_available_items()
201+
202+
def save_available_items(self):
203+
self.last_check_at = time_now()
204+
data = {'items': self.available_items, 'fail_items': self.unavailable_items,
205+
'last_check_at': str(self.last_check_at)}
206+
with open(Config().CDN_ENABLED_AVAILABLE_ITEM_FILE, 'w') as f:
207+
f.write(json.dumps(data))
208+
209+
# if Config.is_master():
210+
# self.cluster.session.sadd(Cluster.KEY_CDN_AVAILABLE_ITEMS, self.available_items)
211+
# self.cluster.session.set(Cluster.KEY_CDN_LAST_CHECK_AT, time_now())
212+
213+
def check_is_ready(self):
214+
if len(self.available_items) > self.is_ready_num:
215+
self.is_ready = True
216+
else:
217+
self.is_ready = False
218+
219+
@classmethod
220+
def get_cdn(cls):
221+
self = cls()
222+
if self.is_ready:
223+
return random.choice(self.available_items)
224+
return None
225+
226+
227+
if __name__ == '__main__':
228+
# Const.IS_TEST = True
229+
Cdn.run()
230+
while not Cdn().is_finished:
231+
stay_second(1)

py12306/helpers/request.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import requests
12
from requests.exceptions import *
23

34
from py12306.helpers.func import *
45
from requests_html import HTMLSession, HTMLResponse
56

7+
requests.packages.urllib3.disable_warnings()
8+
69

710
class Request(HTMLSession):
811
"""
@@ -57,3 +60,11 @@ def request(self, *args, **kwargs): # 拦截所有错误
5760
response.status_code = 500
5861
expand_class(response, 'json', Request.json)
5962
return response
63+
64+
def cdn_request(self, url: str, cdn=None, method='GET', **kwargs):
65+
from py12306.helpers.api import HOST_URL_OF_12306
66+
from py12306.helpers.cdn import Cdn
67+
if not cdn: cdn = Cdn.get_cdn()
68+
url = url.replace(HOST_URL_OF_12306, cdn)
69+
70+
return self.request(method,url, headers={'Host': HOST_URL_OF_12306}, verify=False, **kwargs)

py12306/log/common_log.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ class CommonLog(BaseLog):
4949

5050
MESSAGE_RESPONSE_EMPTY_ERROR = '网络错误'
5151

52+
MESSAGE_CDN_START_TO_CHECK = '正在筛选 {} 个 CDN...'
53+
MESSAGE_CDN_START_TO_RECHECK = '正在重新筛选 {} 个 CDN...当前时间 {}\n'
54+
MESSAGE_CDN_RESTORE_SUCCESS = 'CDN 恢复成功,上次检测 {},下次检测 {}\n'
55+
MESSAGE_CDN_CHECKED_SUCCESS = '# CDN 检测完成,可用 CDN {} #\n'
56+
MESSAGE_CDN_CLOSED = '# CDN 已关闭 #'
57+
5258
def __init__(self):
5359
super().__init__()
5460
self.init_data()
@@ -82,13 +88,16 @@ def print_configs(cls):
8288
disable = '未开启'
8389
self.add_quick_log('**** 当前配置 ****')
8490
self.add_quick_log('多线程查询: {}'.format(get_true_false_text(Config().QUERY_JOB_THREAD_ENABLED, enable, disable)))
91+
self.add_quick_log('CDN 状态: {}'.format(get_true_false_text(Config().CDN_ENABLED, enable, disable))).flush()
92+
self.add_quick_log('通知状态:')
8593
self.add_quick_log(
8694
'语音验证码: {}'.format(get_true_false_text(Config().NOTIFICATION_BY_VOICE_CODE, enable, disable)))
8795
self.add_quick_log('邮件通知: {}'.format(get_true_false_text(Config().EMAIL_ENABLED, enable, disable)))
8896
self.add_quick_log('钉钉通知: {}'.format(get_true_false_text(Config().DINGTALK_ENABLED, enable, disable)))
8997
self.add_quick_log('Telegram通知: {}'.format(get_true_false_text(Config().TELEGRAM_ENABLED, enable, disable)))
9098
self.add_quick_log('ServerChan通知: {}'.format(get_true_false_text(Config().SERVERCHAN_ENABLED, enable, disable)))
91-
self.add_quick_log('PushBear通知: {}'.format(get_true_false_text(Config().PUSHBEAR_ENABLED, enable, disable)))
99+
self.add_quick_log(
100+
'PushBear通知: {}'.format(get_true_false_text(Config().PUSHBEAR_ENABLED, enable, disable))).flush(sep='\t\t')
92101
self.add_quick_log('查询间隔: {} 秒'.format(Config().QUERY_INTERVAL))
93102
self.add_quick_log('用户心跳检测间隔: {} 秒'.format(Config().USER_HEARTBEAT_INTERVAL))
94103
self.add_quick_log('WEB 管理页面: {}'.format(get_true_false_text(Config().WEB_ENABLE, enable, disable)))

0 commit comments

Comments
 (0)