feat(*): 初始化代码仓库
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.html
|
||||||
18
LICENSE
Normal file
18
LICENSE
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 zhilv
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
36
README.md
Normal file
36
README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 重庆城市科技学院刷课
|
||||||
|
|
||||||
|
## CKWK 自动刷课脚本
|
||||||
|
|
||||||
|
### 介绍
|
||||||
|
|
||||||
|
`CKWK` 是一个 Python 脚本,旨在帮助用户自动化刷课过程,支持多个学习平台。该脚本通过模拟登录、获取课程信息、解析学习记录以及自动播放视频等方式来实现自动学习。支持验证码识别,适用于一些需要登录并观看课程视频的学习平台。
|
||||||
|
|
||||||
|
#### 警告:
|
||||||
|
- 本脚本仅用于学习与技术研究,禁止将其用于任何非法用途。
|
||||||
|
- 作者不对因使用本脚本造成的任何损失或后果承担责任。
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
|
||||||
|
- 自动登录学习平台
|
||||||
|
- 识别验证码进行登录
|
||||||
|
- 获取用户的课程信息
|
||||||
|
- 自动浏览课程内容,模拟学习行为
|
||||||
|
- 自动获取学习记录,跳过已学课程
|
||||||
|
- 定时保持在线状态
|
||||||
|
|
||||||
|
### 使用环境
|
||||||
|
|
||||||
|
- Python 3.x
|
||||||
|
- 安装以下依赖库:
|
||||||
|
- `requests`
|
||||||
|
- `ddddocr`
|
||||||
|
- `lxml`
|
||||||
|
- `python-dotenv`
|
||||||
|
- `lxml`
|
||||||
|
|
||||||
|
可以使用 `pip` 安装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install requests ddddocr lxml python-dotenv lxml
|
||||||
|
```
|
||||||
393
main.py
Normal file
393
main.py
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
"""
|
||||||
|
--------------------------------------------
|
||||||
|
文件名称: ckwk.py
|
||||||
|
作者: zhilv
|
||||||
|
邮箱: zhilv666@qq.com
|
||||||
|
版本: 1.0
|
||||||
|
--------------------------------------------
|
||||||
|
说明:
|
||||||
|
本脚本仅用于学习与技术研究,禁止将其用于任何非法用途。
|
||||||
|
作者不对因使用本脚本造成的任何损失或后果承担责任。
|
||||||
|
--------------------------------------------
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import ddddocr
|
||||||
|
from datetime import datetime
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from lxml import etree
|
||||||
|
from threading import Thread
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from urllib3.util.retry import Retry
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
class CKWK:
|
||||||
|
def __init__(self, un: str, pw: str, host: str) -> None:
|
||||||
|
self.session = requests.Session()
|
||||||
|
# 定义重试策略
|
||||||
|
retry_strategy = Retry(
|
||||||
|
total=5, # 最多重试 5 次
|
||||||
|
backoff_factor=1, # 重试间隔:1s, 2s, 4s, 8s...
|
||||||
|
status_forcelist=[429, 500, 502, 503, 504], # 哪些状态码触发重试
|
||||||
|
allowed_methods=["HEAD", "GET", "OPTIONS", "POST"], # 允许重试的方法
|
||||||
|
)
|
||||||
|
|
||||||
|
# 挂载适配器
|
||||||
|
adapter = HTTPAdapter(max_retries=retry_strategy)
|
||||||
|
self.session.mount("http://", adapter)
|
||||||
|
self.session.mount("https://", adapter)
|
||||||
|
# self.session.verify = False
|
||||||
|
# self.session.proxies.update(
|
||||||
|
# {
|
||||||
|
# "http": "http://127.0.0.1:9000",
|
||||||
|
# "https": "http://127.0.0.1:9000",
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
self.username = un
|
||||||
|
self.host = host
|
||||||
|
self.password = pw
|
||||||
|
self.user = dict()
|
||||||
|
self.courses = []
|
||||||
|
self.ocr = ddddocr.DdddOcr()
|
||||||
|
self.session.headers.update(
|
||||||
|
{
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
|
||||||
|
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def coder(self) -> str:
|
||||||
|
resp = self.session.get(
|
||||||
|
f"https://{self.host}/service/code",
|
||||||
|
params={"r": f"{random.random()}"},
|
||||||
|
)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return f"{self.ocr.classification(resp.content)}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def login(self) -> bool:
|
||||||
|
yzm = self.coder()
|
||||||
|
resp = self.session.post(
|
||||||
|
f"https://{self.host}/user/login",
|
||||||
|
data={
|
||||||
|
"username": self.username,
|
||||||
|
"password": self.password,
|
||||||
|
"code": yzm,
|
||||||
|
"redirect": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if "登录成功" in resp.text:
|
||||||
|
return True
|
||||||
|
self.log(resp.text)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def parse_user(self, html: str) -> None:
|
||||||
|
tree = etree.HTML(html)
|
||||||
|
try:
|
||||||
|
self.user = {
|
||||||
|
"name": tree.xpath('//div[@class="name"]/text()')[0],
|
||||||
|
"course_number": tree.xpath(
|
||||||
|
'//div[@class="intro"]/div[1]/div[2]/text()'
|
||||||
|
)[0],
|
||||||
|
"huping": tree.xpath('//div[@class="intro"]/div[2]/div[2]/text()')[0],
|
||||||
|
"lexueyuan": tree.xpath('//div[@class="intro"]/div[3]/div[2]/text()')[
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"taolunzhuti": tree.xpath('//div[@class="intro"]/div[4]/div[2]/text()')[
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"study_time": "".join(
|
||||||
|
tree.xpath('//div[@class="intro"]/div[5]/div[2]//text()')
|
||||||
|
),
|
||||||
|
"courses": [
|
||||||
|
{
|
||||||
|
"name": course.xpath('.//div[@class="name"]/a/text()')[0],
|
||||||
|
"href": course.xpath('.//div[@class="name"]/a/@href')[0],
|
||||||
|
"id": course.xpath('.//div[@class="name"]/a/@href')[0].split(
|
||||||
|
"="
|
||||||
|
)[-1],
|
||||||
|
"progress": course.xpath(
|
||||||
|
'.//div[@class="progress"]/div[3]/text()'
|
||||||
|
)[0],
|
||||||
|
}
|
||||||
|
for course in tree.xpath('//div[@class="user-course"]/div')
|
||||||
|
],
|
||||||
|
}
|
||||||
|
# print(self.user)
|
||||||
|
except Exception as e:
|
||||||
|
self.log("[parse] 提取用户信息失败: ", e)
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def parse_course(self, html: str) -> tuple[int, int]:
|
||||||
|
tree = etree.HTML(html)
|
||||||
|
video = 0
|
||||||
|
state = 1
|
||||||
|
try:
|
||||||
|
if self.courses == []:
|
||||||
|
courses = [
|
||||||
|
{
|
||||||
|
"title": i.get("title", ""),
|
||||||
|
"href": i.get("href", ""),
|
||||||
|
"id": i.get("href", "").split("=")[-1],
|
||||||
|
"class": i.get("class", ""),
|
||||||
|
}
|
||||||
|
for i in tree.xpath(
|
||||||
|
# '//div[@class="item"]/a[contains(text(), "mp4")]'
|
||||||
|
'//div[@class="item"]/a'
|
||||||
|
# '//div[@class="item"]/a[not(contains(text(), "考试") or contains(text(), "课件") or contains(text(), "练习") or contains(text(), "实验"))]'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
index = next(
|
||||||
|
(i for i, c in enumerate(courses) if c["class"] == "on"), None
|
||||||
|
)
|
||||||
|
|
||||||
|
if index is not None:
|
||||||
|
self.courses = courses[index:]
|
||||||
|
# print(self.courses)
|
||||||
|
video = int(tree.xpath('.//input[@id="video-duration"]/@value')[0])
|
||||||
|
state = int(tree.xpath('.//input[@id="study-state"]/@value')[0])
|
||||||
|
except Exception as e:
|
||||||
|
self.log("[parse] 提取课程列表失败: ", e)
|
||||||
|
# raise e
|
||||||
|
finally:
|
||||||
|
return video, state
|
||||||
|
|
||||||
|
def get_course(self, _id: str) -> tuple[int, int]:
|
||||||
|
resp = self.session.get("https://{}/user/node?nodeId={}".format(self.host, _id))
|
||||||
|
if resp.status_code == 200 and "错误提示" not in resp.text:
|
||||||
|
return self.parse_course(resp.text)
|
||||||
|
elif "错误提示" in resp.text:
|
||||||
|
msg = etree.HTML(resp.text).xpath('//div[@class="name"]/text()')[0]
|
||||||
|
self.log(msg)
|
||||||
|
return 0, 1
|
||||||
|
self.log(resp.text)
|
||||||
|
return 0, 1
|
||||||
|
|
||||||
|
def get_course_id(self, _id):
|
||||||
|
resp = self.session.get(
|
||||||
|
"https://{}/user/course?courseId={}".format(self.host, _id)
|
||||||
|
)
|
||||||
|
tree = etree.HTML(resp.text)
|
||||||
|
|
||||||
|
self.get_course(
|
||||||
|
"".join(
|
||||||
|
tree.xpath('//div[@class="ncoursecon-intro"]/div/div[1]/a/@href')
|
||||||
|
).split("=")[-1]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_study_record(self, course_id: str, page=1):
|
||||||
|
resp = self.session.get(
|
||||||
|
f"https://{self.host}/user/study_record.json",
|
||||||
|
params={
|
||||||
|
"courseId": course_id,
|
||||||
|
"page": page,
|
||||||
|
"_": str(int(time.time() * 1000)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if resp.status_code == 200 and resp.json().get("status", False):
|
||||||
|
self.log(f'{resp.json().get("msg", "")} page -> {page}')
|
||||||
|
self.courses.extend(resp.json().get("list", []))
|
||||||
|
if page <= resp.json().get("pageInfo", {}).get("pageCount", 0):
|
||||||
|
self.get_study_record(course_id, page + 1)
|
||||||
|
|
||||||
|
def log(self, *args, **kwargs) -> None:
|
||||||
|
exit_flag = False
|
||||||
|
time_str = datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")
|
||||||
|
prefix = f'[{self.user.get("name", "")}] {time_str}'
|
||||||
|
# 检查 args 中是否有 \r,若有则将前缀拼在 \r 之后
|
||||||
|
if args and isinstance(args[0], str) and "\r" in args[0]:
|
||||||
|
# 将前缀插入到最后一个 \r 后面
|
||||||
|
msg = args[0].rsplit("\r", 1)
|
||||||
|
log_content = (
|
||||||
|
f"{msg[0]}\r{prefix} {msg[1]}"
|
||||||
|
if len(msg) > 1
|
||||||
|
else f"{prefix} {args[0]}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log_content = f'{prefix} {" ".join(map(str, args))}'
|
||||||
|
print(log_content, **kwargs)
|
||||||
|
if exit_flag:
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
def online(self) -> bool:
|
||||||
|
resp = self.session.post(f"https://{self.host}/user/online")
|
||||||
|
if resp.status_code == 200 and resp.json().get("status", False):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def study_start(self, node_id: str) -> None:
|
||||||
|
headers = self.session.headers
|
||||||
|
headers["x-requested-with"] = "XMLHttpRequest"
|
||||||
|
resp = self.session.post(
|
||||||
|
f"https://{self.host}/user/node/study",
|
||||||
|
data={
|
||||||
|
"nodeId": node_id,
|
||||||
|
"studyId": 0,
|
||||||
|
"studyTime": 1,
|
||||||
|
"code": self.coder(),
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if resp.status_code == 200 and resp.json().get("status", False):
|
||||||
|
self.log(f'{resp.json().get("msg", "")} --> 1', end="", flush=True)
|
||||||
|
self.study_id = resp.json().get("studyId", 0)
|
||||||
|
except:
|
||||||
|
if resp.status_code == 200:
|
||||||
|
data = json.loads(
|
||||||
|
re.findall(">var data =(.*?);</script>", resp.text)[0]
|
||||||
|
)
|
||||||
|
self.log(data.get("msg", ""))
|
||||||
|
self.study_id = data.get("studyId", 0)
|
||||||
|
|
||||||
|
def study(self, node_id: str, study_time: str) -> None:
|
||||||
|
headers = self.session.headers
|
||||||
|
headers["x-requested-with"] = "XMLHttpRequest"
|
||||||
|
resp = self.session.post(
|
||||||
|
f"https://{self.host}/user/node/study",
|
||||||
|
data={
|
||||||
|
"nodeId": node_id,
|
||||||
|
"studyId": self.study_id,
|
||||||
|
"studyTime": study_time,
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if resp.status_code == 200 and resp.json().get("status", False):
|
||||||
|
self.log(
|
||||||
|
f'\r{" " * 100}\r{resp.json().get("msg", "")} --> {study_time}',
|
||||||
|
end="",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.study(node_id, study_time)
|
||||||
|
|
||||||
|
def study_over(self, node_id, study_time: str) -> bool:
|
||||||
|
headers = self.session.headers
|
||||||
|
headers["x-requested-with"] = "XMLHttpRequest"
|
||||||
|
resp = self.session.post(
|
||||||
|
f"https://{self.host}/user/node/study",
|
||||||
|
data={
|
||||||
|
"nodeId": node_id,
|
||||||
|
"studyId": self.study_id,
|
||||||
|
"studyTime": study_time,
|
||||||
|
"close": "1",
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
if resp.status_code == 200 and resp.json().get("status", False):
|
||||||
|
self.log(
|
||||||
|
f'\r{" " * 100}\r{resp.json().get("msg", "")} --> {study_time}',
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_user(self) -> None:
|
||||||
|
resp = self.session.get(f"https://{self.host}/user")
|
||||||
|
if resp.status_code == 200:
|
||||||
|
self.parse_user(resp.text)
|
||||||
|
else:
|
||||||
|
self.log(resp.text, exit=True)
|
||||||
|
|
||||||
|
def start_loop(self):
|
||||||
|
while True:
|
||||||
|
time.sleep(120)
|
||||||
|
self.online()
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
while self.login():
|
||||||
|
self.get_user()
|
||||||
|
thread = Thread(target=self.start_loop, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
cs = self.user.get("courses", [])
|
||||||
|
if len(cs) > 1:
|
||||||
|
for i, _c in enumerate(cs):
|
||||||
|
print(f"{i+1}: {_c.get("name", "")} {_c.get("progress", "")}")
|
||||||
|
cs_order = input("请输入刷课顺序(用英文逗号分隔,不需要耍的不用加): ")
|
||||||
|
cs_list = [cs[int(_c) - 1] for _c in cs_order.split(",")]
|
||||||
|
|
||||||
|
cs_list = cs
|
||||||
|
# for c in self.user.get("courses", []):
|
||||||
|
for c in cs_list:
|
||||||
|
# if c.get("progress", "0%").split("%")[0] == 100:
|
||||||
|
# self.log(f"{c.get("name", "")} --> {c.get("id", "")} 已经完成")
|
||||||
|
# continue
|
||||||
|
self.log(f"{c.get("name", "")} --> {c.get("id", "")}")
|
||||||
|
# self.get_course_id(c.get("id", ""))
|
||||||
|
self.get_study_record(c.get("id", ""))
|
||||||
|
# print(self.courses)
|
||||||
|
# os._exit(0)
|
||||||
|
for l in self.courses:
|
||||||
|
if "已学" in l.get("state", "") :
|
||||||
|
self.log(
|
||||||
|
f'{l.get("name", "")} --> {l.get("id", "")} --> 已经学习过了'
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
# elif "1" == l.get("lock", "0"):
|
||||||
|
# self.log(
|
||||||
|
# f'{l.get("name", "")} --> {l.get("id", "")} --> 未解锁'
|
||||||
|
# )
|
||||||
|
# continue
|
||||||
|
|
||||||
|
node_id = l.get("id", "")
|
||||||
|
total_time = 0
|
||||||
|
video_time, video_state = self.get_course(node_id)
|
||||||
|
self.log(
|
||||||
|
f'{l.get("name", "")} --> {l.get("id", "")} --> {video_time} --> {video_state}'
|
||||||
|
)
|
||||||
|
# if video_state == 2:
|
||||||
|
# self.log(
|
||||||
|
# f'{l.get("name", "")} --> {l.get("id", "")} --> 已经学习过了'
|
||||||
|
# )
|
||||||
|
# continue
|
||||||
|
if video_time <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.study_start(node_id)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed = video_time - total_time
|
||||||
|
if elapsed <= 0:
|
||||||
|
self.study_over(node_id, f"{video_time}")
|
||||||
|
break
|
||||||
|
time.sleep(30)
|
||||||
|
total_time += 30
|
||||||
|
self.study(node_id, f"{total_time}")
|
||||||
|
self.log("所有课程已经结束")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
host_server = [
|
||||||
|
["cqcst.yuruixxkj.com", "御瑞科技(选修课)"],
|
||||||
|
["cqcst.zjxkeji.com", "重庆城市科技学院(其他1)"],
|
||||||
|
["cqcst.yuncanjykeji.com", "重庆城市科技学院实训平台(其他2)"],
|
||||||
|
]
|
||||||
|
for i, h in enumerate(host_server):
|
||||||
|
print(f"{i+1}: {h[0]} {h[1]}")
|
||||||
|
c = int(input("请选择刷课平台: ")) - 1
|
||||||
|
u = os.environ.get("YURU_ACCOUNT", "")
|
||||||
|
p = os.environ.get(
|
||||||
|
"YURU_PASSWORD",
|
||||||
|
)
|
||||||
|
if u == "":
|
||||||
|
u = input("请输入账号: ")
|
||||||
|
if p == "":
|
||||||
|
p = input("请输入密码: ")
|
||||||
|
if u is None and p is None and c in [0, 1, 2]:
|
||||||
|
os._exit(0)
|
||||||
|
CKWK(f"{u}", f"{p}", host_server[c][0]).run()
|
||||||
Reference in New Issue
Block a user