前言

2022-8-19 更新0.2版脚本

需要在某学习课堂挂视频。该网站视频播放期间没有干扰,所以只要实现将课程下所有的视频片段从头走到尾即可。

  • 之前也做过一样的挂视频py脚本,使用python+selenium实现。所以起初就想使用同样的方式来实现,这是第一种方法。缺点:需要python,需要selenium webdriver。相对其它两种方法优点是啥,目前还真想不出。
  • 由于视频的播放动作和点击下一片段都是使用selenium的webdriver.execute_script()函数把js代码发送至浏览器中执行实现的,就是向页面中添加一些js代码。突然想起这样的实现方式使用油猴脚本不就完了吗?还不需要python和webdriver,只要在安装油猴的浏览器上打开该视频页面就能添加自动播放功能。这是第二种方法。缺点:需要能安装油猴的浏览器(firefox/chreom/edge)、并且安装油猴扩展、还得在油猴中新增脚本,对计算机小白确实还有点不够友好。优点:直接就在浏览器中实现功能,而且打开视频页面就自动起作用,不需要其它什么操作。
  • 受Reabble的Sent to Kindle第三种使用方式的启发,把js代码做成一个书签按钮,然后进入视频页面点击一下该书签按钮就可以了。缺点:得知道怎么设置书签按钮,并把浏览器中设置“显示收藏夹栏”。优点:只要把js代码事先做成一个书签(和IE收藏夹中网页快捷方式一样的东西,不同的叫法),然后你带到任意一个浏览器,只要点击收藏夹栏中该书签就能启用自动播放功能,使用起来方便简单。

三种方法,我比较倾向于使用第三种。因为只要记下那段js代码,不管在哪、使用什么浏览器,它都能用,并不需要安装其它软件或者扩展。

第一种方法:phthon+selenium

V0.1版实现了基本功能,然后V0.2版做了面向对象的重构。因为想到更方便的实现方法,也就没再仔细去继续优化代码。

# 通服云课堂 自动播放视频
# V0.2  :功能重构
import time

from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException

LOGIN_URL = 'https://www.ccspx.com.cn/login'
USER = '*******'
PWD = '*******'

driver = webdriver.Edge(executable_path='msedgedriver.exe')


class Player:
    def __init__(self, parallel_num, proceed, task_center_title='任务中心'):
        self.parallel_num = parallel_num
        self.proceed = proceed
        self.task_center_title = task_center_title
        self.tasks = []

        self.logined = False

    def run(self):
        self._login()
        self._create_tasks()
        for task in self.tasks:
            task.start()
            time.sleep(3)

        for task in self.tasks:
            print(f'{task.name}\n{task.command}\n{task.video_fragment_commands}\n----------------------')

        while self.tasks:
            time.sleep(10)
            for i in range(len(self.tasks)):
                self.tasks[i].check_video_ended()
                if self.tasks[i].completed:
                    self.tasks.remove(self.tasks[i])

    def _login(self):
        driver.get(LOGIN_URL)
        time.sleep(1)
        driver.find_element_by_id('username').send_keys(USER)
        driver.find_element_by_id('password').send_keys(PWD)
        driver.find_element_by_id('validateCode').click()

        while True:
            try:
                driver.find_element_by_id('username')
            except NoSuchElementException:
                self.logined = True
                break
            time.sleep(2)

    def _create_tasks(self):
        window = None
        while window is None:
            time.sleep(10)
            for w in driver.window_handles:
                driver.switch_to.window(w)
                if driver.title == self.task_center_title:
                    window = w

        driver.switch_to.window(window)
        time.sleep(2)
        task_commands = [a.get_attribute('onclick') for a in
                         driver.find_elements_by_xpath('//div[@id="dataList"]//li/a')]
        progresses = [span.text for span in driver.find_elements_by_xpath('//div[@id="dataList"]//li//span')]
        for c, p in zip(task_commands, progresses):
            if self.proceed:
                if p != '100%':
                    self.tasks.append(Task(command=c, parent_window=window, proceed=self.proceed))
            else:
                self.tasks.append(Task(command=c, parent_window=window, proceed=self.proceed))


class Task:
    def __init__(self, command, parent_window, proceed):
        self.command = command
        self.parent_window = parent_window
        self.proceed = proceed

        self.running = False
        self.completed = False
        self.name, self.window = None, None

        self.video_fragment_commands = []

    def start(self):
        driver.switch_to.window(self.parent_window)
        driver.execute_script(self.command)
        time.sleep(3)
        self.window = driver.window_handles[-1]
        driver.switch_to.window(self.window)
        self.name = driver.title

        self._get_video_fragments()
        self._play()

    def _get_video_fragments(self):
        driver.switch_to.window(self.window)
        fragment_commands = [li.get_attribute('onclick') for li in
                             driver.find_elements_by_xpath('//li[contains(@class, "task-item")]')]
        progresses = [sp.text for sp in driver.find_elements_by_xpath('//li[contains(@class, "task-item")]//span')][
                     2::3]
        for c, p in zip(fragment_commands, progresses):
            if self.proceed:
                if p != '100%':
                    self.video_fragment_commands.append(c)
            else:
                self.video_fragment_commands.append(c)

    def _play(self):
        if len(self.video_fragment_commands) != 0:
            driver.switch_to.window(self.window)
            driver.execute_script(self.video_fragment_commands[0])
            time.sleep(2)
            driver.execute_script('document.getElementsByTagName("video")[0].play();')
        else:
            self.completed = True
            driver.execute_script('alert("页面无视频或采集错误");')

    def check_video_ended(self):
        driver.switch_to.window(self.window)
        if driver.find_element_by_tag_name('video').get_property('ended'):
            self.video_fragment_commands.pop(0)
            if len(self.video_fragment_commands) == 0:
                self.completed = True
                driver.execute_script('alert("视频播放完毕");')
                # driver.close()
            else:
                self._play()
        else:
            driver.execute_script('document.getElementsByTagName("video")[0].play();')

    def __repr__(self):
        return f'{self.__class__.__name__}(name={self.name}, command={self.command}, ' \
               f'video_fragments={self.video_fragment_commands})'


def main():
    player = Player(parallel_num=5, proceed=False)
    player.run()


if __name__ == '__main__':
    main()

第二种方法:使用油猴扩展

思路:添加一个定时调用器 setInterval 。每隔4秒执行一次,检查视频播放是否结束(判断player.ended),以及播放下一个片段。
版本0.1,误打误中的版本,不建议使用。

// ==UserScript==
// @name         通服云课堂 - 自动播放/静音
// @namespace    *
// @version      0.1
// @description  自动播放“通服云课堂”视频
// @author       czbuyi
// @match        https://www.ccspx.com.cn/course/detail*
// @match        https://www.ccspx.cn/course/detail*
// @grant        none
// ==/UserScript==

(function() {
    if(confirm("开始视频自动播放?")){
        setInterval(function(){
            let player = document.getElementsByTagName('video')[0];	//这就是误打误中的地方,把video元素赋值到player
            if(player.ended){
                let all = [].slice.call(document.getElementsByClassName('task-item'));
                let active = document.getElementsByClassName('task-item active')[0];
                let next_index = all.indexOf(active)+1;
                console.log(all.indexOf(active));
                if (next_index <= all.length-1){
                    all[next_index].click();
                }else{
                    alert("视频播放结束");
                    window.close();
                }
            };
            player.play();
            player.muted=true;
        }, 4000)
    }
    // Your code here...
})();

版本0.2,直接使用网站中的player,增加4X倍速播放,而且会对页面中非100%的课程自动播放。

// ==UserScript==
// @name         通服云课堂 - 自动播放/静音/4X倍速
// @namespace    *
// @version      0.2
// @description  自动播放“通服云课堂”视频
// @author       czbuyi
// @match        https://www.ccspx.com.cn/course/detail*
// @match        https://www.ccspx.cn/course/detail*
// @grant         none
// ==/UserScript==

(function() {
    console.log('进入 **通服云课堂 - 自动播放/静音/4X倍速** 油猴脚本');
    setInterval(function(){
    	// 等网站生成player之后,才能进行一系统的播放操作
        if(player){
            let current_task = document.getElementsByClassName('task-item active')[0];
            if(current_task.outerText.split('\n')[2] == "100%" || player.ended){
            	// 如果当前任务已播放完毕,则播放下一个任务
                let tasks = document.getElementsByClassName('task-item');
                let task = null;
                for(let i in tasks){
                    if(tasks[i].outerText.split('\n')[2] != "100%"){
                        task = tasks[i];
                        break;
                    }
                }
                if (task) {
                    task.click();
                }else{
                    confirm('视频播放完毕');
                    window.close();
                };
            }else{
            	// 播放、静音、4倍速
                player.play();
                if(!player.muted()){
                    player.mute(true);
                };
                if (player.tag.playbackRate!=4){
                    player.tag.playbackRate=4;
                };
                console.log('【油猴脚本】当前是否静音:', player.muted(), ' 播放速度:', player.tag.playbackRate);
            };
        };
    }, 4000);
})();

第三种方法:使用标签按钮

js代码与第二种方法是一样的。为了能做成标签,js代码前面需加入"javascript:"并且去掉回车,整理成一行。(注:已修改为0.2版的油猴脚本)

javascript:(function() {console.log('进入 **通服云课堂 - 自动播放/静音/4X倍速** 油猴脚本');setInterval(function(){if(player){let current_task = document.getElementsByClassName('task-item active')[0];if(current_task.outerText.split('\n')[2] == "100%" || player.ended){let tasks = document.getElementsByClassName('task-item');let task = null;for(let i in tasks){if(tasks[i].outerText.split('\n')[2] != "100%"){task = tasks[i];break;}}if (task) {task.click();}else{confirm('视频播放完毕');window.close();};}else{player.play();if(!player.muted()){player.mute(true);};if (player.tag.playbackRate!=4){player.tag.playbackRate=4;};console.log('【油猴脚本】当前是否静音:', player.muted(), ' 播放速度:', player.tag.playbackRate);};};}, 4000);})();

记录几个坑

  • 在网页中查找操作某元素或者测试js代码,之前我一直都是使用运行代码(通过python,或者html、js脚本)来检测,每次可能要涉及登录、点击至目标页面、刷新等等,浪费了非常多的时间精力。后来才发现可以在浏览器“开发者调试工具”(按F12调出来)的控制台下直接输入测试,马上就看到结果,非常直接方便。
  • 这个网站使用的是一种播放器(Aliplayer类,怀疑是阿里家的,没去深究哪来哪去),网页加载完成后会实例化一个player。之前走了弯路(0.1版的油猴脚本),把播放器当成是一个HTML5自有的video元素,所以所有操作都是HTML5的默认操作,也就是下面删除线的内容。
  • 为何思路错了,但还能起作用,那只是因为误打误撞,刚好把获取到的video元素定义为player。
  • 所以0.2版的脚本,必须判断网站是否已经实例化了player,其它都一样了。
  • 0.2版添加了非100%课程都会自动播放,如果有两个视频播放完后没正常更新百分比,那么有可能会出现死循环,一直在不停的播放这两个视频。目前未想到怎么解决,只能期待网站能在正常更新百分比。
  • 这个网站的视频播放页面里 video 元素会变化,所以每次操作都需要重新获取一下 video 元素。也因同样的问题,不能为 video 元素添加 ended 事件来处理视频片段播放完毕后的操作。
  • 为了实现连续播放,需要找到下一个片段的il元素。但是各个页面结构有些差异,只好将页面内所有带“task-item”类的il全部找出来转成数组,再查找“task-item active”类的il元素,通过检查其在数组中的位置来确定下一个片段的il。
  • js的document.getElementsBy***返回的是一个HTMLCollection对象,它类似数组,可以用for遍历,但没有indexOf的函数,不能确定某元素在对象中的位置。因为在实现播放下一下片段,我需要找到当前“task-item active”类的il,并click下一个il元素,所以只能把HTMLCollection对象转换成数组:all = [].slice.call(document.getElementsByClassName(‘task-item’))
  • html元素在<>内定义的属性可以使用getAttribute(“属性名”)来获取,比如 video元素.getAttribute(“scr”) 。但video元素的状态,比如播放结束"ended"、静音状态"muted"等,直接使用 video元素.ended 来判断。
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐