Computer Science,  tools

半自动刷课程序DEMO V1

程序内容

  • 程序概述

前置操作:脚本使用前置环境
此程序基于之前自动化刷客程序进行设计(参考自动刷课程序DEMO V1 基于慕课)基本内容不变是通过 Selenium 自动化浏览器,模拟登录并观看在线视频的过程。在保证根本功能(比如获取视频信息、控制视频播放、暂停以及跳转到下一个视频等)的前提下,主要解决一些全自动运行下的痛点,主要是平台适配性问题,在全自动平台下没有针对页面特定逻辑进行设计导致其鲁棒性不强。其次全自动工具开发阶段目前十分初级,还有许多功能未适配,如自动登录功能。在全自动运行受阻的情况下,需要针对代码进行重构,使用效率低,因此开发此半自动的程序,以逐渐增强适配性。

  • 程序目标

针对刷课程序而言,此程序选择将前期大量复杂的准备工作交由用户手动完成,比如页面的登录,部分功能的确定以及config文件的增改。在完成前置准备工作后,后续模拟流程(通过获取视频的状态,监控视频的播放、暂停、进度等信息,并在视频结束后自动切换到下一个视频)以自动化方式运行,其是使用成本并不高,在使用效率并未大幅降低的情况下,显著增加平台的实用性。

  • 程序结构与主要功能
  • 配置文件读取:
    程序通过 load_config() 函数读取 JSON 配置文件并加载各个平台的配置信息。
  • 用户选择平台并加载对应参数:
    用户通过输入选择平台,程序根据配置文件的内容加载不同平台的配置信息,包括:
    • 播放按钮的定位信息
    • 下一集按钮的定位信息
    • 是否启用自动播放
    • 视频进度编辑是否可用
    • 视频状态检查周期
  • 视频模拟观看:
    在获取到正确的按钮定位信息后,程序会模拟视频播放过程,检查视频播放状态(例如视频是否暂停、播放进度等),并且在每个视频播放完毕后自动点击“下一集”按钮(如果网站没有自动播放)。
  • 按钮点击:
    程序通过 click_button() 函数,使用 Selenium 模拟点击播放按钮和下一集按钮。通过提供的按钮的 class 名称进行元素定位。
  • 配置与进度:
    每个视频的播放进度都会通过 get_video_info() 来进行查询。程序在每次视频播放时,根据视频的播放状态和配置文件中的设置来调整视频进度或自动跳转到视频的结束时间。
  • 程序流程
  1. 加载配置文件
    • 在程序开始时,首先调用 load_config() 函数读取配置文件 config.json,获取不同平台的播放配置。
    • 配置文件包括每个平台的播放按钮、下一集按钮、自动播放设置、视频检查间隔等信息。
  2. 用户选择平台
    • 程序向用户展示可用的平台配置(从配置文件加载),并通过输入框让用户选择一个平台。
    • 根据用户的选择,程序读取该平台的配置,包括播放按钮、下一集按钮、是否自动播放下一集等。
  3. 初始化浏览器
    • 选择完平台后,程序初始化 Selenium 浏览器(Chrome),设置一些选项(如禁用自动化检测、无头模式等)。
    • 启动浏览器后,访问用户提供的课程 URL,并等待页面加载完成。
  4. 用户登录
    • 如果需要用户登录,程序会暂停并提示用户输入登录信息。用户按回车键后继续执行。
  5. 模拟观看视频
    • 调用 simulate_video_watch() 开始模拟观看视频。
    • 通过 get_video_info() 获取当前视频的信息(如视频时长、播放进度、暂停状态等)。
  6. 判断视频播放状态:
    • 程序通过 while 循环不断检查视频播放状态。如果视频暂停,程序会尝试点击播放按钮继续播放。
    • 如果视频播放完成,程序会记录播放统计信息(视频个数、总时长等)
  7. 跳转到视频末尾:
    • 程序根据配置判断是否启用视频进度编辑(VIDEO_EDITABLE)。
    • 如果启用,程序会通过 JavaScript 执行代码将视频播放进度跳转到视频的最后(video.currentTime = video.duration),模拟视频快进。
  8. 播放完毕后,根据配置判断是否继续播放下一集:
    • 如果视频播放完毕,程序会根据 AUTO_PLAY_NEXT 配置判断网站是否存在自动连播的设置。
    • 如果自动播放下一集 (AUTO_PLAY_NEXTTrue),则网站自己会自动连播,程序无操作,繁殖程序会等待视频结束后自动点击“下一集”按钮继续播放。
  9. 结束,输出统计信息
    • 在所有视频播放完毕后,程序输出总的视频播放次数和总时长。
    • 当所有操作完成后,程序会退出浏览器,结束脚本。
  • 配置文件config.json

标准的json结构体及释意如下(以bilibili为例):

{
      "name": "Bilibili", 
      "auto_play_next": "True",----此配置为网页自动连播参数,为True表示网页支持自动播放下一个,False则表示需要用户手动点击
      "button_play": "bpx-player-ctrl-btn-icon",----此配置为网页上视频播放按钮的元素参数
      "button_next": "bpx-player-ending-related-item-cover",----此配置为网页上播放下一个视频按钮的元素参数
      "check_interval": "3",----此配置为视频参数检查周期,3为每3秒读取下视频状态,检查是否播放暂停或者播放结束
      "Progress_edit": "True"----此配置为视频进度是否可拖动标志,部分网站视频无法拖动,或者拖动即视作无效,因此需要预先配置
    },

后续此结构体可能继续扩展,按钮参数的意义和获取可参考如下链接:
刷课程序支持工具

  • 后续改进计划 To Be Continue
  • 2025.1.20
    • 错误处理:完善错误处理,增强鲁棒性,调整参数如页面加载时间的延迟等。
    • 增强平台适配性:部分课程网站可能针对外挂软件有反制措施,如睡眠设置,页面提示等,针对这些平台可以进行代码改进
    • 并发支持:目前程序是单线程的,适用于逐个视频播放。如果需要,可以通过多线程或并发来提高效率,自动化播放多个视频。
    • 平台扩展:当前支持平台不多,后续维护增加更多的平台支持(config文件扩充)
  • 平台支持
    1. 慕课网IMOOC
    2. Bilibili
    3. Acfun
    4. chinaooc(国家高等教育智慧教育平台学堂在线)
  • 更新日志
  • 2025.1.20 更新初版功能 V1.0

程序代码

  • semiAuto.py
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
import time
import json

# 配置登录信息
COURSE_URL = None  # 课程视频页面 URL
BUTTON_PLAY_PRM = None  #播放按钮
BUTTON_NEXT_PRM = None  #下一个按钮
AUTO_PLAY_NEXT = None #自动播放下一个视频检测
CHECK_INTERVAL = None #默认视频检查间隔时间
VIDEO_EDITABLE = True #视频进度是否可操作

#统计数据
VIDEO_COUNT = 0
VIDEO_TIME = 0
VIDEO_SRC = None


# 设置 Selenium 浏览器
def setup_browser():
    options = webdriver.ChromeOptions()
    options.add_argument("--disable-blink-features=AutomationControlled")
    # options.add_argument('--headless')  # 无头模式(可选,不显示浏览器界面)
    options.add_argument("--start-maximized")
    options.add_argument('--disable-gpu')  # 禁用GPU加速(可选)
    options.add_argument("--no-sandbox")
    options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")#替换自己的浏览器代理
    driver = webdriver.Chrome(options=options)

    # 打开登录页面
    driver.get(COURSE_URL)
    time.sleep(2)  # 等待页面加载
    print(f"等待完毕")
    return driver

# 模拟观看视频
def simulate_video_watch(driver):
    video_info = get_video_info(driver)
    global VIDEO_COUNT,VIDEO_TIME,VIDEO_SRC
    if video_info is not None:
        if VIDEO_SRC != video_info['src']:
            VIDEO_SRC = video_info['src']
            print(f"播放视频{VIDEO_COUNT + 1},视频时长:{video_info['duration']} 秒")
            while not video_info['ended'] and VIDEO_SRC == video_info['src']:
                if video_info['paused']:
                    print(f"WARNING:检测到视频暂停")
                    if not click_button(driver,BUTTON_PLAY_PRM):
                        return 
                if video_info['duration'] > 0:
                    VIDEO_PROGRESS = round((video_info['currentTime'] / video_info['duration']) * 100, 2)
                else:
                    VIDEO_PROGRESS = 0
                print(f"check:")
                print(f"      当前播放时间:{round(video_info['currentTime'], 2)}秒")
                print(f"      当前视频进度:{VIDEO_PROGRESS}%")
                if VIDEO_EDITABLE:
                    print("       5s后尝试跳转至视频末尾")
                    time.sleep(5)
                    if driver.execute_script("""try{var video = document.querySelector('video');
                                        if (video) {       
                                            video.currentTime = video.duration;
                                            return true;
                                        } else {
                                            return false;
                                        }}catch(e){return false;}
                                    """
                                     ):
                        print("      尝试跳转完毕")
                    else:
                        print("      尝试跳转失败")
                time.sleep(CHECK_INTERVAL)  # 视频状态检查间隔
                video_info = get_video_info(driver)
            print(f"播放完毕")
            print(f"********************************")
            VIDEO_COUNT += 1
            VIDEO_TIME += video_info['duration']
            time.sleep(1)
            if not AUTO_PLAY_NEXT:
                if not click_button(driver,BUTTON_NEXT_PRM):
                    print("无可进行操作,无法播放下一步")
                    return
                time.sleep(2)
                simulate_video_watch(driver)
                    
            else:
                while VIDEO_SRC == video_info['src']:
                    print("等待1s")
                    time.sleep(1)
                    simulate_video_watch(driver)


#获取视频元素的信息
def get_video_info(driver):
    try:
    # 执行 JavaScript 获取视频元素的时长
        video_info = driver.execute_script("""
            var video = document.querySelector('video');  // 获取页面中的第一个 <video> 标签
            if (video) {
                return {
                    duration: video.duration,  // 视频时长(单位:秒)
                    currentTime: video.currentTime,  // 当前播放时间(单位:秒)
                    paused: video.paused,  // 是否暂停
                    ended: video.ended,  // 是否播放完毕
                    src: video.src    //视频地址
                };
            } else {
                return null;  // 如果没有找到视频元素,返回 null
            }
        """)

        if video_info:
            return video_info
        else:
            print("未能找到视频元素")
            return None
    except Exception as e:
        print(f"获取视频信息失败:{e}")
        return None


#点击按钮
def click_button(driver,button_prm):
    try:
        #driver.execute_script("arguments[0].click();", find_button_by_parm(driver,button_prm))
        find_button_by_parm(driver,button_prm).click()
        print(f"成功点击按钮")
        return True
    except Exception as e:
        print(f"error:无法点击按钮",{e})
        return False

def load_config(config_file="config.json"):
    """从配置文件读取配置数据"""
    with open(config_file, "r", encoding="utf-8") as file:
        config_data = json.load(file)
    return config_data


#根据用户选择执行相应逻辑
def process_choice(user_choice, config_data):
    if user_choice == "0":
        print("退出")
        return False
    elif int(user_choice) <= len(config_data["platforms"]):
        config = config_data["platforms"][int(user_choice) - 1]  # 获取选项2的配置
        print(f"选择配置{user_choice}")
        print(f"加载配置platform: {config_data['platforms'][int(user_choice) - 1].get('name', '未找到 name')}")
        global BUTTON_PLAY_PRM,AUTO_PLAY_NEXT,BUTTON_NEXT_PRM,CHECK_INTERVAL,VIDEO_EDITABLE
        BUTTON_PLAY_PRM = config_data['platforms'][int(user_choice) - 1].get('button_play', None)
        if BUTTON_PLAY_PRM is None:
            print("播放按钮参数未配置")
            return False
        else:
            print(f"播放按钮参数已配置:{BUTTON_PLAY_PRM}")
        AUTO_PLAY_NEXT = config_data['platforms'][int(user_choice) - 1].get('auto_play_next', None) == 'True'
        if AUTO_PLAY_NEXT is None:
            print("自动连播参数读取失败")
            return False
        else:
            print(f"自动连播配置为{AUTO_PLAY_NEXT}")
        BUTTON_NEXT_PRM = config_data['platforms'][int(user_choice) - 1].get('button_next', None)
        if BUTTON_NEXT_PRM is None:
            print("连播按钮参数未配置")
            return False
        else:
            print(f"连播按钮参数已配置:{BUTTON_NEXT_PRM}")
        CHECK_INTERVAL = int(config_data['platforms'][int(user_choice) - 1].get('check_interval', 10))
        print(f"检查间隔时间已配置:{CHECK_INTERVAL}")
        VIDEO_EDITABLE = config_data['platforms'][int(user_choice) - 1].get('Progress_edit', None) == 'True'
        print(f"视频进度拖动使能已配置:{VIDEO_EDITABLE}")
        print("加载完毕")
        return True
    else:
        print("无效选择,请重新运行程序")
        return False

#用户先择对应平台
def get_user_choice():
    #等待用户输入,返回选择
    config_data = load_config()
    if config_data is None:
        print("配置文件为空,执行默认配置")
        if COURSE_URL is None or BUTTON_PLAY is None or BUTTON_NEXT is None or AUTO_PLAY_NEXT is None or CHECK_INTERVAL is None:
            print("默认配置为空,结束")
            return False
    else:
        print("读取配置文件清单:")
        try:
            for index, platform in enumerate(config_data["platforms"]):
                print(f"{index + 1}. {platform['name']}")
            print("0. 退出")
            user_choice = input("请输入选择的配置: ")
            if not process_choice(user_choice, config_data):
                return False
            else:
                return True
        except Exception as e:
            print(f"配置文件异常: {e}")
            return False



#根据配置参数寻找页面按钮
def find_button_by_parm(driver,parm):
    if parm is None:
        return None
    try:
        button = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located(
                (By.XPATH,
                 f"//*[contains(@class, '{parm}')]"
                 )
                )
            )
        return button
    except Exception as e:
        print(f"{parm}参数按钮查找出错: {e}")
        return None
    


# 主程序
def main():
    global COURSE_URL
    if not get_user_choice():
        print(f"===============fdsfsf================")
        return
    COURSE_URL= input("请输入课程地址链接: ")
    driver = setup_browser()
    input("若需要登陆,请登陆后按回车键继续,若不需要登录请直接按回车键继续!")
    simulate_video_watch(driver)
    # 完成操作后退出
    print(f"===============================")
    print(f"播放结束,共计播放视频个数:{VIDEO_COUNT} 总视频时间:{VIDEO_TIME} 秒")
    driver.quit()


# 执行主程序
if __name__ == "__main__":
    main()
  • Config.json
{
  "platforms": [
    {
      "name": "iMooc", 
      "auto_play_next": "False",
      "button_play": "vjs-play-control vjs-control vjs-button",
      "button_next": "J-next-btn",
      "check_interval": "3",
      "Progress_edit": "True"

    },
    {
      "name": "Bilibili", 
      "auto_play_next": "True",
      "button_play": "bpx-player-ctrl-btn-icon",
      "button_next": "bpx-player-ending-related-item-cover",
      "check_interval": "3",
      "Progress_edit": "True"
    },
    {
      "name": "Acfun", 
      "auto_play_next": "False",
      "button_play": "btn-play control-btn",
      "button_next": "rotation",
      "check_interval": "3",
      "Progress_edit": "False"
    },
    {
      "name": "chinaooc", 
      "auto_play_next": "False",
      "button_play": "xt_video_player_play_btn fl",
      "button_next": "textover",
      "check_interval": "3",
      "Progress_edit": "False"
    }
  ]
}

留言

您的邮箱地址不会被公开。 必填项已用 * 标注