学习通批量下载

查看 66|回复 9
作者:nianboy   
楼主在22年已经发过一次帖子了,最近有吾爱的朋友来问我有没有打包版本,突然想到已经毕业好久了,借此机会把之前的代码更新一下,增加了GUI界面,由此对自己大学生活画下一个完美的句号。楼主发布了一个C#版本发布在原创区,另外请大家动动小手给楼主点点免费的评分,主要是想把头像给换了:lol ,感谢大家的支持!!!
【原创工具】超星学习通作业批量下载神器
[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

作业, 课程

sanfengzhang   

我来抢个位置。。学习一下。
15035875059   

试试看 感谢跟新
dysunb   

增加了GUI界面
青春莫相随   

感谢分享
沉住气   

作者辛苦了
bx05   

感谢分享
yueye1122   

试试看 感谢跟新
doubleA   

学习一下
52PJ070   

学习下,辛苦楼主分享源码,900+行,辛苦了
您需要登录后才可以回帖 登录 | 立即注册

返回顶部