【原创工具】超星学习通作业批量下载神器
[Python] 纯文本查看 复制代码import sys
import os
import requests
import json
import logging
import time
from datetime import datetime
from PyQt5.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QListWidget, QMessageBox, QInputDialog, QCheckBox,
QProgressBar, QFileDialog, QGroupBox, QGridLayout, QComboBox
)
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QSettings
from bs4 import BeautifulSoup
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
import re
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("chaoxing_downloader.log", encoding='utf-8'),
logging.StreamHandler()
]
)
class ChaoXingWorkDownloader:
def __init__(self):
self.session = requests.Session()
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"
}
self.class_list = []
self.current_class_name = ""
self.work_list = []
self.download_folder = os.path.join(os.path.expanduser("~"), "Downloads", "ChaoXing")
# 确保下载目录存在
os.makedirs(self.download_folder, exist_ok=True)
# 在login方法中添加更详细的URL日志
def login(self, user, password):
logging.info("正在尝试登录...")
login_url = "https://passport2.chaoxing.com/api/login"
logging.info(f"登录URL: {login_url}")
data = {
"name": user,
"pwd": password,
"loginType": "1",
"verify": "0",
"schoolid": ""
}
try:
res = self.session.post(login_url, data=data, headers=self.headers, timeout=15)
json_data = res.json()
if json_data.get("result") is True:
logging.info("登录成功")
return True, ""
else:
error_msg = json_data.get("msg", "未知错误")
logging.error(f"登录失败: {error_msg}")
return False, error_msg
except requests.exceptions.Timeout:
logging.error("登录超时,请检查网络连接")
return False, "登录超时,请检查网络连接"
except requests.exceptions.ConnectionError:
logging.error("网络连接错误")
return False, "网络连接错误"
except Exception as e:
logging.error(f"登录异常: {str(e)}")
return False, f"登录异常: {str(e)}"
# 在get_courses方法中添加URL日志
def get_courses(self):
logging.info("获取课程列表中...")
course_url = "https://mooc2-ans.chaoxing.com/visit/courses/list?v=1652629452722&rss=1&start=0&size=500&catalogId=0&searchname="
logging.info(f"课程列表URL: {course_url}")
try:
res = self.session.get(course_url, headers=self.headers, timeout=15)
res.raise_for_status()
except requests.exceptions.Timeout:
logging.error("获取课程列表超时")
return []
except requests.exceptions.ConnectionError:
logging.error("网络连接错误")
return []
except Exception as e:
logging.error(f"请求课程列表失败: {str(e)}")
return []
soup = BeautifulSoup(res.text, 'html.parser')
items = soup.select('li.course')
if not items:
logging.warning("无法找到课程列表,请确认已登录")
return []
self.class_list = []
for idx, item in enumerate(items, start=1):
try:
name_elem = item.select_one('.course-name')
name = name_elem.text.strip() if name_elem else "未知课程"
link_elem = item.select_one('a[href]')
link = link_elem['href'] if link_elem else ""
if not link.startswith("http"):
link = "https://mooc1.chaoxing.com" + link
# 获取课程图片
img_elem = item.select_one('img[src]')
img_url = img_elem['src'] if img_elem else ""
self.class_list.append({
"index": idx,
"name": name,
"url": link,
"img_url": img_url
})
logging.debug(f"找到课程: {name}")
except Exception as e:
logging.error(f"解析课程项失败: {str(e)}")
continue
logging.info(f"共获取到 {len(self.class_list)} 个课程")
return self.class_list
def select_course(self, index):
if 0 = max_retries:
break
time.sleep(2) # 等待2秒后重试
if not download_link:
continue
try:
# 获取href属性并清理
href_value = download_link['href'].strip()
# 处理被反引号包围的URL
if href_value.startswith('`') and href_value.endswith('`'):
href_value = href_value.strip('`').strip()
logging.info(f"清理反引号后的链接: {href_value}")
elif '`' in href_value: # 处理可能的部分反引号
href_value = href_value.replace('`', '').strip()
logging.info(f"清理部分反引号后的链接: {href_value}")
# 处理相对URL
if href_value.startswith('/'):
href_value = f"https://mooc1.chaoxing.com{href_value}"
logging.info(f"处理相对路径后的链接: {href_value}")
elif not (href_value.startswith('http://') or href_value.startswith('https://')):
href_value = f"https://mooc1.chaoxing.com/{href_value.lstrip('/')}"
logging.info(f"处理非标准URL后的链接: {href_value}")
final_download_url = href_value
# 设置完整的 headers
download_headers = {
"User-Agent": self.headers["User-Agent"],
"Referer": read_url,
}
# 开始下载文件
logging.info(f"开始下载文件: {file_name}")
file_path = os.path.join(work_folder, self._sanitize_filename(file_name))
# 获取文件大小
file_size_res = self.session.head(final_download_url, headers=download_headers, timeout=15)
file_size = int(file_size_res.headers.get('content-length', 0))
with self.session.get(final_download_url, stream=True, headers=download_headers,
timeout=30) as r:
r.raise_for_status()
downloaded_size = 0
with open(file_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded_size += len(chunk)
if progress_callback and file_size > 0:
progress = int((downloaded_size / file_size) * 100)
progress_callback(file_name, progress)
logging.info(f"文件已保存至: {file_path}")
downloaded_files.append(file_path)
break # 下载成功,退出重试循环
except Exception as e:
logging.error(f"下载文件 {file_name} 失败: {str(e)}")
retry_count += 1
if retry_count ', '|']
for char in invalid_chars:
filename = filename.replace(char, '_')
return filename
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.downloader = ChaoXingWorkDownloader()
self.download_thread = None # 这行可以保留,不会影响功能
self.settings = QSettings("ChaoXingDownloader", "Settings")
self.init_ui()
self.load_settings()
def init_ui(self):
self.setWindowTitle("超星作业下载工具")
self.resize(800, 600)
main_layout = QVBoxLayout()
# 登录区域
login_group = QGroupBox("登录信息")
login_layout = QGridLayout()
self.user_edit = QLineEdit()
self.user_edit.setPlaceholderText("账号")
self.pwd_edit = QLineEdit()
self.pwd_edit.setPlaceholderText("密码")
self.pwd_edit.setEchoMode(QLineEdit.Password)
self.remember_checkbox = QCheckBox("记住账号密码")
self.login_btn = QPushButton("登录")
self.login_btn.clicked.connect(self.login)
login_layout.addWidget(QLabel("账号:"), 0, 0)
login_layout.addWidget(self.user_edit, 0, 1)
login_layout.addWidget(QLabel("密码:"), 0, 2)
login_layout.addWidget(self.pwd_edit, 0, 3)
login_layout.addWidget(self.remember_checkbox, 0, 4)
login_layout.addWidget(self.login_btn, 0, 5)
login_group.setLayout(login_layout)
main_layout.addWidget(login_group)
# 课程区域
course_group = QGroupBox("课程列表")
course_layout = QVBoxLayout()
course_btn_layout = QHBoxLayout()
self.course_btn = QPushButton("获取课程")
self.course_btn.clicked.connect(self.get_courses)
self.course_btn.setEnabled(False)
course_btn_layout.addWidget(self.course_btn)
course_btn_layout.addStretch()
self.course_list = QListWidget()
self.course_list.setSelectionMode(QListWidget.SingleSelection)
course_layout.addLayout(course_btn_layout)
course_layout.addWidget(self.course_list)
course_group.setLayout(course_layout)
main_layout.addWidget(course_group)
# 作业区域
work_group = QGroupBox("作业列表")
work_layout = QVBoxLayout()
work_btn_layout = QHBoxLayout()
self.work_btn = QPushButton("获取作业")
self.work_btn.clicked.connect(self.get_works)
self.work_btn.setEnabled(False)
self.download_btn = QPushButton("下载选中作业")
self.download_btn.clicked.connect(self.download_selected_works)
self.download_btn.setEnabled(False)
self.download_all_btn = QPushButton("下载全部作业")
self.download_all_btn.clicked.connect(self.download_all_works)
self.download_all_btn.setEnabled(False)
self.select_folder_btn = QPushButton("选择下载目录")
self.select_folder_btn.clicked.connect(self.select_download_folder)
work_btn_layout.addWidget(self.work_btn)
work_btn_layout.addWidget(self.download_btn)
work_btn_layout.addWidget(self.download_all_btn)
work_btn_layout.addWidget(self.select_folder_btn)
work_btn_layout.addStretch()
self.work_list = QListWidget()
self.work_list.setSelectionMode(QListWidget.MultiSelection)
self.work_list.itemSelectionChanged.connect(self.update_download_button)
work_layout.addLayout(work_btn_layout)
work_layout.addWidget(self.work_list)
work_group.setLayout(work_layout)
main_layout.addWidget(work_group)
# 下载进度区域
progress_group = QGroupBox("下载进度")
progress_layout = QVBoxLayout()
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_label = QLabel("准备就绪")
progress_layout.addWidget(self.progress_label)
progress_layout.addWidget(self.progress_bar)
progress_group.setLayout(progress_layout)
main_layout.addWidget(progress_group)
# 状态栏
status_layout = QHBoxLayout()
self.status_label = QLabel("就绪")
self.folder_label = QLabel(f"下载目录: {self.downloader.download_folder}")
status_layout.addWidget(self.status_label)
status_layout.addStretch()
status_layout.addWidget(self.folder_label)
main_layout.addLayout(status_layout)
self.setLayout(main_layout)
def login(self):
user = self.user_edit.text().strip()
pwd = self.pwd_edit.text().strip()
if not user or not pwd:
QMessageBox.warning(self, "提示", "请输入账号和密码")
return
self.status_label.setText("正在登录...")
QApplication.processEvents()
success, error_msg = self.downloader.login(user, pwd)
if success:
QMessageBox.information(self, "提示", "登录成功")
self.course_btn.setEnabled(True)
self.status_label.setText("已登录")
# 保存设置
if self.remember_checkbox.isChecked():
self.save_settings()
else:
QMessageBox.critical(self, "错误", f"登录失败: {error_msg}")
self.status_label.setText("登录失败")
def save_settings(self):
if self.remember_checkbox.isChecked():
self.settings.setValue("username", self.user_edit.text())
self.settings.setValue("password", self.pwd_edit.text())
self.settings.setValue("remember", True)
else:
self.settings.remove("username")
self.settings.remove("password")
self.settings.setValue("remember", False)
self.settings.setValue("download_folder", self.downloader.download_folder)
def load_settings(self):
if self.settings.value("remember", False, type=bool):
self.user_edit.setText(self.settings.value("username", ""))
self.pwd_edit.setText(self.settings.value("password", ""))
self.remember_checkbox.setChecked(True)
saved_folder = self.settings.value("download_folder", "")
if saved_folder and os.path.exists(saved_folder):
self.downloader.download_folder = saved_folder
self.folder_label.setText(f"下载目录: {saved_folder}")
def get_courses(self):
self.course_list.clear()
courses = self.downloader.get_courses()
for c in courses:
self.course_list.addItem(f"{c['index']}. {c['name']}")
self.work_btn.setEnabled(True)
def get_works(self):
selected = self.course_list.currentRow()
if selected 0)
def select_download_folder(self):
"""选择下载目录"""
folder = QFileDialog.getExistingDirectory(self, "选择下载目录", self.downloader.download_folder)
if folder:
self.downloader.download_folder = folder
self.folder_label.setText(f"下载目录: {folder}")
self.save_settings()
def download_selected_works(self):
"""下载选中的作业"""
selected_items = self.work_list.selectedItems()
if not selected_items:
QMessageBox.warning(self, "提示", "请先选择要下载的作业")
return
selected_indices = [self.work_list.row(item) for item in selected_items]
self._start_download(selected_indices)
def download_all_works(self):
"""下载全部作业"""
if not self.downloader.work_list:
QMessageBox.warning(self, "提示", "作业列表为空")
return
all_indices = list(range(len(self.downloader.work_list)))
self._start_download(all_indices)
def _start_download(self, indices):
"""直接下载作业(单线程)"""
self.progress_bar.setValue(0)
self.progress_label.setText("准备下载...")
self.status_label.setText("正在下载...")
# 禁用下载按钮,防止重复点击
self.download_btn.setEnabled(False)
self.download_all_btn.setEnabled(False)
# 直接在主线程中下载
results = []
errors = []
for idx in indices:
if 0