cetnext 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CETNext/__init__.py +272 -0
- CETNext/__main__.py +20 -0
- CETNext/api.py +352 -0
- CETNext/cli.py +514 -0
- CETNext/edu.py +119 -0
- CETNext/user.py +99 -0
- CETNext/work.py +281 -0
- cetnext-1.1.0.dist-info/METADATA +105 -0
- cetnext-1.1.0.dist-info/RECORD +13 -0
- cetnext-1.1.0.dist-info/WHEEL +5 -0
- cetnext-1.1.0.dist-info/entry_points.txt +2 -0
- cetnext-1.1.0.dist-info/licenses/LICENSE +202 -0
- cetnext-1.1.0.dist-info/top_level.txt +1 -0
CETNext/__init__.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CETNext
|
|
3
|
+
==================================
|
|
4
|
+
作者: Argon
|
|
5
|
+
GitHub: https://github.com/mifongjvav/CETNext
|
|
6
|
+
|
|
7
|
+
欢迎使用 CETNext!
|
|
8
|
+
这是一个Python包,你可以使用 import CETNext 来导入它
|
|
9
|
+
|
|
10
|
+
开发者不对您使用本项目造成的风险负责,请自行考虑是否使用,谢谢!
|
|
11
|
+
"""
|
|
12
|
+
# 版权所有 (C) 2026 Argon
|
|
13
|
+
# 根据 Apache 2.0 许可证发布
|
|
14
|
+
#
|
|
15
|
+
# 修改声明:
|
|
16
|
+
# 本文件基于原作者 WangZixu(2025)的作品修改而来。
|
|
17
|
+
# 主要修改内容:
|
|
18
|
+
# 将原有的 argparse CLI 完全重写为 click 库实现
|
|
19
|
+
# 添加 @click.group() 主命令组及所有子命令装饰器
|
|
20
|
+
# 使用 click.option 和 click.pass_context 替代 argparse.ArgumentParser
|
|
21
|
+
# 将全局参数 --token-file 通过上下文对象传递给子命令
|
|
22
|
+
# 修改 __main__.py 入口调用方式
|
|
23
|
+
# 加入新功能
|
|
24
|
+
# 修改日期:2026-06-06
|
|
25
|
+
|
|
26
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
__version__ = version("CETNext")
|
|
30
|
+
except PackageNotFoundError:
|
|
31
|
+
__version__ = "1.1.0"
|
|
32
|
+
__author__ = "Argon"
|
|
33
|
+
__description__ = "为编程猫社区的“老师”们提供更便捷的API调用方案,且用且珍惜"
|
|
34
|
+
|
|
35
|
+
# 线程配置
|
|
36
|
+
max_workers = 8
|
|
37
|
+
|
|
38
|
+
# 举报配置
|
|
39
|
+
report_readtoken_line = 20
|
|
40
|
+
|
|
41
|
+
# 学生名字列表
|
|
42
|
+
student_names = [
|
|
43
|
+
"xvbnmklq",
|
|
44
|
+
"asdfghjk",
|
|
45
|
+
"qwertyui",
|
|
46
|
+
"zxcvbnml",
|
|
47
|
+
"poiuytre",
|
|
48
|
+
"lkjhgfds",
|
|
49
|
+
"mnbvcxza",
|
|
50
|
+
"plokmijn",
|
|
51
|
+
"uhbygvtd",
|
|
52
|
+
"crfvtgby",
|
|
53
|
+
"edcrfvtg",
|
|
54
|
+
"qazwsxed",
|
|
55
|
+
"rfvtgbyh",
|
|
56
|
+
"nujmikol",
|
|
57
|
+
"zxasqwde",
|
|
58
|
+
"plmnkoij",
|
|
59
|
+
"bvcdxsza",
|
|
60
|
+
"qwermnbp",
|
|
61
|
+
"asxcvgfr",
|
|
62
|
+
"lpoikmju",
|
|
63
|
+
"yhnujmik",
|
|
64
|
+
"tgbzdxew",
|
|
65
|
+
"rfvgyhuj",
|
|
66
|
+
"edcwsxqa",
|
|
67
|
+
"zaqxswcd",
|
|
68
|
+
"vfrcdews",
|
|
69
|
+
"bgtnhyuj",
|
|
70
|
+
"mkiopluj",
|
|
71
|
+
"nhybtgvr",
|
|
72
|
+
"cdexswza",
|
|
73
|
+
"qwerfdsa",
|
|
74
|
+
"zxcvfdsa",
|
|
75
|
+
"poiuytrw",
|
|
76
|
+
"lkjhgfda",
|
|
77
|
+
"mnbvcxzs",
|
|
78
|
+
"asdfqwer",
|
|
79
|
+
"zxcvqwer",
|
|
80
|
+
"poiulkjh",
|
|
81
|
+
"mnbvcxas",
|
|
82
|
+
"qwertzui",
|
|
83
|
+
"yxcvbnmq",
|
|
84
|
+
"plokmnji",
|
|
85
|
+
"uhbgyvft",
|
|
86
|
+
"crfvtgyn",
|
|
87
|
+
"edcrfvbg",
|
|
88
|
+
"qazwsxrf",
|
|
89
|
+
"rfvtgbyu",
|
|
90
|
+
"nujmiklp",
|
|
91
|
+
"zxasqwed",
|
|
92
|
+
"plmnkoji",
|
|
93
|
+
"bvcdxsaz",
|
|
94
|
+
"qwermnbo",
|
|
95
|
+
"asxcvfgd",
|
|
96
|
+
"lpoikmjn",
|
|
97
|
+
"yhnujmki",
|
|
98
|
+
"tgbzdxec",
|
|
99
|
+
"rfvgyhuk",
|
|
100
|
+
"edcwsxqz",
|
|
101
|
+
"zaqxswce",
|
|
102
|
+
"vfrcdewa",
|
|
103
|
+
"bgtnhyum",
|
|
104
|
+
"mkioplun",
|
|
105
|
+
"nhybtgvf",
|
|
106
|
+
"cdexswzb",
|
|
107
|
+
"qwerfdsz",
|
|
108
|
+
"zxcvfdsz",
|
|
109
|
+
"poiuytrq",
|
|
110
|
+
"lkjhgfdz",
|
|
111
|
+
"mnbvcxzc",
|
|
112
|
+
"asdfqwez",
|
|
113
|
+
"zxcvqwez",
|
|
114
|
+
"poiulkjm",
|
|
115
|
+
"mnbvcxaq",
|
|
116
|
+
"qwertzuy",
|
|
117
|
+
"yxcvbnmr",
|
|
118
|
+
"plokmnjh",
|
|
119
|
+
"uhbgyvfr",
|
|
120
|
+
"crfvtgyb",
|
|
121
|
+
"edcrfvbn",
|
|
122
|
+
"qazwsxre",
|
|
123
|
+
"rfvtgbyi",
|
|
124
|
+
"nujmiklj",
|
|
125
|
+
"zxasqweg",
|
|
126
|
+
"plmnkojh",
|
|
127
|
+
"bvcdxsay",
|
|
128
|
+
"qwermnbu",
|
|
129
|
+
"asxcvfgh",
|
|
130
|
+
"lpoikmjh",
|
|
131
|
+
"yhnujmko",
|
|
132
|
+
"tgbzdxer",
|
|
133
|
+
"rfvgyhun",
|
|
134
|
+
"edcwsxqv",
|
|
135
|
+
"zaqxswec",
|
|
136
|
+
"vfrcdewq",
|
|
137
|
+
"bgtnhyup",
|
|
138
|
+
"mkiopluh",
|
|
139
|
+
"nhybtgvc",
|
|
140
|
+
"cdexswzg",
|
|
141
|
+
"qwerfdsx",
|
|
142
|
+
"zxcvfdsx",
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
# 请在创建新功能后更新此处!
|
|
146
|
+
|
|
147
|
+
_LAZY_IMPORTS = {
|
|
148
|
+
# API
|
|
149
|
+
"PostAPI": (".api", "PostAPI"),
|
|
150
|
+
"PostWithoutTokenAPI": (".api", "PostWithoutTokenAPI"),
|
|
151
|
+
"PostEduAPI": (".api", "PostEduAPI"),
|
|
152
|
+
"GetAPI": (".api", "GetAPI"),
|
|
153
|
+
"GetWithoutTokenAPI": (".api", "GetWithoutTokenAPI"),
|
|
154
|
+
"PutAPI": (".api", "PutAPI"),
|
|
155
|
+
"DeleteAPI": (".api", "DeleteAPI"),
|
|
156
|
+
# 用户
|
|
157
|
+
"GetUserToken": (".user", "GetUserToken"),
|
|
158
|
+
"CheckToken": (".user", "CheckToken"),
|
|
159
|
+
"SignatureUser": (".user", "SignatureUser"),
|
|
160
|
+
"FollowUser": (".user", "FollowUser"),
|
|
161
|
+
# 作品
|
|
162
|
+
"GetUserWork": (".work", "GetUserWork"),
|
|
163
|
+
"GetStudioWork": (
|
|
164
|
+
".work",
|
|
165
|
+
"GetStudioWork",
|
|
166
|
+
),
|
|
167
|
+
"LikeWork": (".work", "LikeWork"),
|
|
168
|
+
"CollectionWork": (".work", "CollectionWork"),
|
|
169
|
+
"ReportWork": (".work", "ReportWork"),
|
|
170
|
+
"SendReviewToWork": (".work", "SendReviewToWork"),
|
|
171
|
+
"TopReview": (".work", "TopReview"),
|
|
172
|
+
"UnTopReview": (".work", "UnTopReview"),
|
|
173
|
+
"ViewWork": (".work", "ViewWork"),
|
|
174
|
+
"ForkWork": (".work", "ForkWork"),
|
|
175
|
+
# CodemaoEDU
|
|
176
|
+
"CreateClassOnEdu": (".edu", "CreateClassOnEdu"),
|
|
177
|
+
"CreateStudentOnEdu": (".edu", "CreateStudentOnEdu"),
|
|
178
|
+
"MergeStudentXls": (".edu", "MergeStudentXls"),
|
|
179
|
+
"LoginUseEdu": (".edu", "LoginUseEdu"),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# 请在创建新功能后更新此处!导入控制
|
|
183
|
+
__all__ = [
|
|
184
|
+
# 配置
|
|
185
|
+
"__version__",
|
|
186
|
+
"__author__",
|
|
187
|
+
"__description__",
|
|
188
|
+
"max_workers",
|
|
189
|
+
"student_names",
|
|
190
|
+
"report_readtoken_line",
|
|
191
|
+
# API
|
|
192
|
+
"PostAPI",
|
|
193
|
+
"PostWithoutTokenAPI",
|
|
194
|
+
"PostEduAPI",
|
|
195
|
+
"GetAPI",
|
|
196
|
+
"GetWithoutTokenAPI",
|
|
197
|
+
"PutAPI",
|
|
198
|
+
"DeleteAPI",
|
|
199
|
+
# 用户
|
|
200
|
+
"GetUserToken",
|
|
201
|
+
"CheckToken",
|
|
202
|
+
"SignatureUser",
|
|
203
|
+
"FollowUser",
|
|
204
|
+
# 作品
|
|
205
|
+
"GetUserWork",
|
|
206
|
+
"GetStudioWork",
|
|
207
|
+
"LikeWork",
|
|
208
|
+
"LikeReview",
|
|
209
|
+
"CollectionWork",
|
|
210
|
+
"ReportWork",
|
|
211
|
+
"SendReviewToWork",
|
|
212
|
+
"TopReview",
|
|
213
|
+
"UnTopReview",
|
|
214
|
+
"ViewWork",
|
|
215
|
+
"ForkWork",
|
|
216
|
+
# CodemaoEDU
|
|
217
|
+
"CreateClassOnEdu",
|
|
218
|
+
"CreateStudentOnEdu",
|
|
219
|
+
"MergeStudentXls",
|
|
220
|
+
"LoginUseEdu",
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def __getattr__(name: str):
|
|
225
|
+
if name in _LAZY_IMPORTS:
|
|
226
|
+
module_name, attr_name = _LAZY_IMPORTS[name]
|
|
227
|
+
from importlib import import_module
|
|
228
|
+
|
|
229
|
+
module = import_module(module_name, package=__name__)
|
|
230
|
+
value = getattr(module, attr_name)
|
|
231
|
+
globals()[name] = value
|
|
232
|
+
return value
|
|
233
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def __dir__() -> list[str]:
|
|
237
|
+
return sorted(list(globals().keys()) + list(_LAZY_IMPORTS.keys()))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# 包函数
|
|
241
|
+
def get_version() -> str:
|
|
242
|
+
"""获取版本号"""
|
|
243
|
+
return __version__
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def info() -> str:
|
|
247
|
+
"""显示包信息"""
|
|
248
|
+
return f"""
|
|
249
|
+
CETNext v{__version__}
|
|
250
|
+
作者: {__author__}
|
|
251
|
+
GitHub: https://github.com/mifongjvav/CETNext/
|
|
252
|
+
|
|
253
|
+
导入方式:
|
|
254
|
+
import CETNext
|
|
255
|
+
from CETNext import * # 不推荐
|
|
256
|
+
|
|
257
|
+
命令行使用:
|
|
258
|
+
python main.py check-token
|
|
259
|
+
|
|
260
|
+
注意事项:
|
|
261
|
+
请合理使用本工具,遵守编程猫平台的使用规则。
|
|
262
|
+
开发者不对滥用本工具造成的后果负责。
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def about() -> str:
|
|
267
|
+
"""显示关于信息"""
|
|
268
|
+
return f"""
|
|
269
|
+
CETNext v{__version__}
|
|
270
|
+
作者: {__author__}
|
|
271
|
+
GitHub: https://github.com/mifongjvav/CETNext/
|
|
272
|
+
"""
|
CETNext/__main__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# __main__.py
|
|
2
|
+
import logging
|
|
3
|
+
import coloredlogs
|
|
4
|
+
|
|
5
|
+
from CETNext.cli import cli
|
|
6
|
+
|
|
7
|
+
logging.basicConfig(
|
|
8
|
+
filename='latest.log',
|
|
9
|
+
filemode='w',
|
|
10
|
+
encoding='utf-8',
|
|
11
|
+
level=logging.INFO,
|
|
12
|
+
format="%(asctime)s - %(levelname)s - %(funcName)s: %(message)s"
|
|
13
|
+
)
|
|
14
|
+
coloredlogs.install(level="INFO", fmt="%(asctime)s - %(levelname)s - %(funcName)s: %(message)s")
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
cli()
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
CETNext/api.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API调用
|
|
3
|
+
"""
|
|
4
|
+
# 版权所有 (C) 2026 Argon
|
|
5
|
+
# 根据 Apache 2.0 许可证发布
|
|
6
|
+
#
|
|
7
|
+
# 修改声明:
|
|
8
|
+
# 本文件基于原作者 WangZixu(2025)的作品修改而来。
|
|
9
|
+
# 主要修改内容:
|
|
10
|
+
# 将 requests 改为 playwright
|
|
11
|
+
# 将 UA 改为常量
|
|
12
|
+
# 优化史山
|
|
13
|
+
# 修改日期:2026-05-05
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import atexit
|
|
17
|
+
import time
|
|
18
|
+
import json
|
|
19
|
+
import threading
|
|
20
|
+
import queue
|
|
21
|
+
import uuid
|
|
22
|
+
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
|
|
23
|
+
|
|
24
|
+
_browser = None
|
|
25
|
+
_context = None
|
|
26
|
+
_playwright = None
|
|
27
|
+
|
|
28
|
+
_request_queue = queue.Queue()
|
|
29
|
+
_response_queues = {}
|
|
30
|
+
_worker_thread = None
|
|
31
|
+
_browser_ready = threading.Event()
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
USER_AGENT = (
|
|
37
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
38
|
+
"(KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_default_headless = True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def set_default_headless(value: bool):
|
|
45
|
+
"""设置全局无头浏览器模式"""
|
|
46
|
+
global _default_headless
|
|
47
|
+
_default_headless = value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _init_browser(headless: bool = None):
|
|
51
|
+
global _playwright, _browser, _context
|
|
52
|
+
if _browser is not None:
|
|
53
|
+
_browser_ready.set()
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
logger.info("正在启动 Playwright...")
|
|
58
|
+
_playwright = sync_playwright().start()
|
|
59
|
+
logger.info("正在启动 Chromium 浏览器...")
|
|
60
|
+
_browser = _playwright.chromium.launch(
|
|
61
|
+
headless=headless,
|
|
62
|
+
args=[
|
|
63
|
+
"--disable-blink-features=AutomationControlled",
|
|
64
|
+
"--no-sandbox",
|
|
65
|
+
"--disable-gpu",
|
|
66
|
+
],
|
|
67
|
+
)
|
|
68
|
+
logger.info("正在创建浏览器上下文...")
|
|
69
|
+
_context = _browser.new_context(
|
|
70
|
+
user_agent=USER_AGENT,
|
|
71
|
+
viewport={"width": 1920, "height": 1080},
|
|
72
|
+
locale="zh-CN",
|
|
73
|
+
timezone_id="Asia/Shanghai",
|
|
74
|
+
)
|
|
75
|
+
_context.add_init_script("""
|
|
76
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => false });
|
|
77
|
+
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
|
|
78
|
+
Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en'] });
|
|
79
|
+
window.chrome = { runtime: {}, loadTimes: function(){}, csi: function(){}, app: {} };
|
|
80
|
+
""")
|
|
81
|
+
logger.info("浏览器初始化完成")
|
|
82
|
+
_browser_ready.set()
|
|
83
|
+
except Exception:
|
|
84
|
+
logger.exception("浏览器初始化失败,请 playwright install chromium 安装浏览器")
|
|
85
|
+
_browser_ready.set()
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _worker(headless: bool = None):
|
|
90
|
+
global _context, _browser, _playwright
|
|
91
|
+
_init_browser(headless)
|
|
92
|
+
try:
|
|
93
|
+
while True:
|
|
94
|
+
logger.debug("Worker 等待任务...")
|
|
95
|
+
task = _request_queue.get()
|
|
96
|
+
if task is None:
|
|
97
|
+
logger.info("Worker 收到退出信号")
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
request_id, method, url, headers, retry, post_data = task
|
|
101
|
+
logger.info(f"处理请求 [{request_id[:8]}] {method} {url}")
|
|
102
|
+
result = None
|
|
103
|
+
|
|
104
|
+
for i in range(retry):
|
|
105
|
+
page = None
|
|
106
|
+
try:
|
|
107
|
+
logger.debug(f"第 {i + 1} 次尝试: 创建新页面")
|
|
108
|
+
page = _context.new_page()
|
|
109
|
+
logger.debug("新页面已创建,正在发起请求")
|
|
110
|
+
|
|
111
|
+
request_options = {
|
|
112
|
+
"method": method,
|
|
113
|
+
"headers": headers or {},
|
|
114
|
+
"timeout": 30000,
|
|
115
|
+
}
|
|
116
|
+
if post_data is not None:
|
|
117
|
+
request_options["data"] = json.dumps(
|
|
118
|
+
post_data, ensure_ascii=False
|
|
119
|
+
)
|
|
120
|
+
request_options["headers"]["Content-Type"] = "application/json"
|
|
121
|
+
|
|
122
|
+
response = page.request.fetch(url, **request_options)
|
|
123
|
+
status_code = response.status
|
|
124
|
+
response_text = response.text()
|
|
125
|
+
logger.debug(f"请求完成,状态码: {status_code}")
|
|
126
|
+
|
|
127
|
+
# 记录非 200 状态码的内容
|
|
128
|
+
if status_code != 200:
|
|
129
|
+
logger.warning(
|
|
130
|
+
f"非 200 状态码: {status_code}, 返回内容预览: {response_text[:500]}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# 检查 WAF 拦截
|
|
134
|
+
if (
|
|
135
|
+
"renderData" not in response_text
|
|
136
|
+
and "aliyun_waf" not in response_text
|
|
137
|
+
):
|
|
138
|
+
result = PlaywrightResponse(status_code, response_text, url)
|
|
139
|
+
logger.info(f"请求成功: {status_code}")
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
logger.warning(
|
|
143
|
+
f"请求被 WAF 拦截,状态码: {status_code}, 返回内容预览: {response_text[:500]}"
|
|
144
|
+
)
|
|
145
|
+
time.sleep((i + 1) * 2)
|
|
146
|
+
|
|
147
|
+
except PlaywrightTimeoutError:
|
|
148
|
+
logger.warning(f"请求超时,重试 {i + 1}/{retry}")
|
|
149
|
+
time.sleep((i + 1) * 2)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
# 尝试从异常中提取响应内容
|
|
152
|
+
error_msg = str(e)
|
|
153
|
+
response_preview = ""
|
|
154
|
+
if hasattr(e, "response") and e.response:
|
|
155
|
+
try:
|
|
156
|
+
response_preview = e.response.text()[:500]
|
|
157
|
+
except Exception:
|
|
158
|
+
response_preview = "<无法读取响应内容>"
|
|
159
|
+
logger.warning(
|
|
160
|
+
f"请求异常: {error_msg}, 响应预览: {response_preview}, 重试 {i + 1}/{retry}"
|
|
161
|
+
)
|
|
162
|
+
time.sleep((i + 1) * 2)
|
|
163
|
+
finally:
|
|
164
|
+
if page:
|
|
165
|
+
try:
|
|
166
|
+
page.close()
|
|
167
|
+
logger.debug("页面已关闭")
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
if request_id in _response_queues:
|
|
172
|
+
_response_queues[request_id].put(result)
|
|
173
|
+
logger.debug(f"结果已返回给请求者 [{request_id[:8]}]")
|
|
174
|
+
finally:
|
|
175
|
+
logger.info("正在关闭浏览器…")
|
|
176
|
+
if _context:
|
|
177
|
+
try:
|
|
178
|
+
_context.close()
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
if _browser:
|
|
182
|
+
try:
|
|
183
|
+
_browser.close()
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
if _playwright:
|
|
187
|
+
try:
|
|
188
|
+
_playwright.stop()
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
_context = _browser = _playwright = None
|
|
192
|
+
_browser_ready.clear()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _start_worker(headless: bool = None):
|
|
196
|
+
global _worker_thread
|
|
197
|
+
if _worker_thread is None or not _worker_thread.is_alive():
|
|
198
|
+
_worker_thread = threading.Thread(target=_worker, args=(headless,), daemon=True)
|
|
199
|
+
_worker_thread.start()
|
|
200
|
+
if not _browser_ready.wait(timeout=30):
|
|
201
|
+
raise RuntimeError("浏览器启动超时")
|
|
202
|
+
if _browser is None:
|
|
203
|
+
raise RuntimeError("浏览器初始化失败")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _close_browser():
|
|
207
|
+
global _worker_thread
|
|
208
|
+
if _request_queue:
|
|
209
|
+
_request_queue.put(None)
|
|
210
|
+
if _worker_thread and _worker_thread.is_alive():
|
|
211
|
+
_worker_thread.join(timeout=10)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class PlaywrightResponse:
|
|
215
|
+
def __init__(self, status_code, text, url):
|
|
216
|
+
self._status_code = status_code
|
|
217
|
+
self._text = text
|
|
218
|
+
self.url = url
|
|
219
|
+
self.ok = 200 <= status_code < 300
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def status_code(self):
|
|
223
|
+
return self._status_code
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def text(self):
|
|
227
|
+
return self._text
|
|
228
|
+
|
|
229
|
+
def json(self):
|
|
230
|
+
return json.loads(self._text)
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def code(self):
|
|
234
|
+
return self._status_code
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _request(method, url, headers=None, retry=3, headless: bool = None, **kwargs):
|
|
238
|
+
_start_worker(headless)
|
|
239
|
+
post_data = kwargs.get("json")
|
|
240
|
+
request_id = str(uuid.uuid4())
|
|
241
|
+
result_queue = queue.Queue()
|
|
242
|
+
_response_queues[request_id] = result_queue
|
|
243
|
+
_request_queue.put((request_id, method, url, headers, retry, post_data))
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
result = result_queue.get(timeout=60)
|
|
247
|
+
except queue.Empty:
|
|
248
|
+
logger.error(f"主线程等待超时: {method} {url}")
|
|
249
|
+
result = None
|
|
250
|
+
finally:
|
|
251
|
+
_response_queues.pop(request_id, None)
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_headers(token=None, include_auth=True):
|
|
256
|
+
headers = {
|
|
257
|
+
"Content-Type": "application/json",
|
|
258
|
+
"Accept": "application/json, text/plain, */*",
|
|
259
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
260
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
261
|
+
"Origin": "https://shequ.codemao.cn",
|
|
262
|
+
"Referer": "https://shequ.codemao.cn/",
|
|
263
|
+
"User-Agent": USER_AGENT,
|
|
264
|
+
"sec-ch-ua": '"Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147"',
|
|
265
|
+
"sec-ch-ua-mobile": "?0",
|
|
266
|
+
"sec-ch-ua-platform": '"Windows"',
|
|
267
|
+
}
|
|
268
|
+
if include_auth and token:
|
|
269
|
+
headers["authorization"] = token
|
|
270
|
+
return headers
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def GetAPI(Path, Token, headless=None):
|
|
274
|
+
if headless is None:
|
|
275
|
+
headless = _default_headless
|
|
276
|
+
return _request(
|
|
277
|
+
"GET", f"https://api.codemao.cn{Path}", get_headers(Token), headless
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def GetWithoutTokenAPI(Path, headless=None):
|
|
282
|
+
if headless is None:
|
|
283
|
+
headless = _default_headless
|
|
284
|
+
return _request(
|
|
285
|
+
"GET",
|
|
286
|
+
f"https://api.codemao.cn{Path}",
|
|
287
|
+
get_headers(include_auth=False),
|
|
288
|
+
headless,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def PostAPI(Path, PostData, Token, headless=None):
|
|
293
|
+
if headless is None:
|
|
294
|
+
headless = _default_headless
|
|
295
|
+
return _request(
|
|
296
|
+
"POST",
|
|
297
|
+
f"https://api.codemao.cn{Path}",
|
|
298
|
+
get_headers(Token),
|
|
299
|
+
json=PostData,
|
|
300
|
+
headless=headless,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def PostWithoutTokenAPI(Path, PostData, headless=None):
|
|
305
|
+
if headless is None:
|
|
306
|
+
headless = _default_headless
|
|
307
|
+
return _request(
|
|
308
|
+
"POST",
|
|
309
|
+
f"https://api.codemao.cn{Path}",
|
|
310
|
+
get_headers(include_auth=False),
|
|
311
|
+
json=PostData,
|
|
312
|
+
headless=headless,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def PostEduAPI(Path, PostData, Token, headless=None):
|
|
317
|
+
if headless is None:
|
|
318
|
+
headless = _default_headless
|
|
319
|
+
h = get_headers(Token)
|
|
320
|
+
h.update(
|
|
321
|
+
{
|
|
322
|
+
"Origin": "https://eduzone.codemao.cn",
|
|
323
|
+
"Referer": "https://eduzone.codemao.cn/",
|
|
324
|
+
"authorization": f"Bearer {Token}",
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
return _request(
|
|
328
|
+
"POST", f"https://eduzone.codemao.cn{Path}", h, json=PostData, headless=headless
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def PutAPI(Path, Token, headless=None):
|
|
333
|
+
if headless is None:
|
|
334
|
+
headless = _default_headless
|
|
335
|
+
return _request(
|
|
336
|
+
"PUT", f"https://api.codemao.cn{Path}", get_headers(Token), headless=headless
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def DeleteAPI(Path, Token, headless=None):
|
|
341
|
+
if headless is None:
|
|
342
|
+
headless = _default_headless
|
|
343
|
+
return _request(
|
|
344
|
+
"DELETE", f"https://api.codemao.cn{Path}", get_headers(Token), headless=headless
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def close():
|
|
349
|
+
_close_browser()
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
atexit.register(close)
|