智慧树知到刷课插件(包括直播课)-智慧树视频自动观看脚本
这个脚本的根本原则是完全的自动化。除了之一次使用需要花些时间配置,后续就只要点击运行即可,登陆、跳转、关闭等等功能都会自动完成。
为了达成这个效果,我主要实现了以下功能:
基础部分:
1. 首次配置脚本
配置内容包括账号、密码、课程链接和部分Chromedriver configs(静音、无头模式)
之一次配置完成后,输入的信息会被保存在同目录下的zhh_config.txt文件中,后续只要这个文件存在,就不再需要任何输入。如果想更改信息,直接更改txt文件即可。
2. 自动输入账号(手机号)和密码并登录
这个不多说了,模拟登陆都实现不了的话还谈什么刷课。
3. 根据输入的URL自动判断类型
对于输入的URL,脚本会自动判断类型(目前只有Lesson课程和Live直播两种),根据不同的类型调用不同的类进行处理。
针对不同的视频类型,进行以下处理:
课程视频:
1. 自动跳过警告和学前必读
这个智慧树警告我觉得挺有毒的...而且还每次都会跳出来,无语,所以在开始刷课之前必须要检测窗口,然后关掉。
2. 自动点击并关闭视频中跳出的问题界面(答对是不可能的)
手动挂机刷智慧树视频不可行,就在于视频中经常会跳出不计分的问题,且关闭问题之后,视频并不会自动继续播放。
所以脚本就很有必要了,利用脚本,可以实时监控跳出的问题,并点击之一个选项(反正不计分)后关闭,再自动点击播放按钮实现不间断后台刷课。
3. 自动续播
这听起来是一个很蠢的功能,但实际上,智慧树的课程视频播放结束后并不会自动下一节。这大概率也是为了防止挂机有意为之,所以脚本加入这个功能也是必须的。
3. 自动关闭视频中跳出的今日时长提示
我没有具体计时,但这个跳出的时间应该远远大于25分钟,一般出现这个的话就可以关了。但如果想要继续看视频,就仍然需要用脚本把这个提示框关掉,再点击视频播放按钮。
4. 自动定时30分钟关闭
虽然可以在跳出今日时长提示后就关闭,但为了防止时间不够,我还是设置了定时半小时关闭。
5. 在console显示
(1)当前视频标题
(2)实时进度更新
(3)观看时间提示(3分钟1次)
直播视频:
1. 自动关闭提示
无语+1,这些提示为什么每次都要跳出来啊?
2. 跟踪并显示签到进度
在直播页面的下方有一个蓝色的签到进度,显示了这堂直播课已经观看的百分比。这对于下一个功能的实现有极大帮助。
3. 自动前进至签到上次位置
一节直播课时间很长,大概2个多小时,因此很难一次刷完。但和课程视频不同,直播视频每次重新进入都会从00:00:00开始,非常影响效率。
为了解决这个问题,我设置了自动加速,当视频位置在签到位置之前,视频会以每秒2分钟速度加速播放,到了签到位置后则恢复正常速度,因为没有视频,所以只能意会了...这部分也是最复杂的,我想了很多方法,最后找到了一个可以用的 *** 指令。
部分功能实现方法说明:
全说篇幅太长了,稍微挑几个重要的。
1. 实时进度条:
以Live.show_progress()为例:
def show_progress(self):
progress=self.dr.find_element_by_xpath('//*[@id="container"]/div[1]/div/div[2]/div[1]/p[2]/span').get_attribute('textContent')
print("目前签到进度:"+progress+"%")
while True:
time.sleep(1)#每间隔1秒检测视频是否播放完
try:
#该视频的总时间
total_time=self.dr.find_element_by_xpath('//*[@id="vjs_forFollowBackDiv"]/div[10]/div[4]/span[2]').get_attribute('textContent')
#获取当前播放的进度
current_time=self.dr.find_element_by_xpath('//*[@id="vjs_forFollowBackDiv"]/div[10]/div[4]/span[1]').get_attribute('textContent')
print("进度:{0}/{1}".format(current_time,total_time), end="\r")
if current_time!='00:00:00' and total_time!='00:00:00':
if current_time[3:5]==total_time[3:5] and int(current_time[6:])>=(int(total_time[6:])-2):
print('\n')
print('已完成本次直播',end='\n')
self.dr.quit() #退出
quit()
except:
current_time = '00:00'
total_time = '00:05' #随意填,两个不同就行
并不复杂,主要就是每隔一秒,找到网页中的 current_time 和 total_time 两个元素提取出文本,然后按格式 print 出来。
唯一需要注意的是 print 最后的 end=’\r’ ,这表示回车,回到某一行的开头,这样可以覆盖前一秒的进度,看起来就像是实时更新了。
2. 直播加速播放:
先看实现的代码:
def play_time(self):
while True:
time.sleep(0.5)
try:
progress=self.dr.find_element_by_xpath('//*[@id="container"]/div[1]/div/div[2]/div[1]/p[2]/span').get_attribute('textContent')
total_time=self.dr.find_element_by_xpath('//*[@id="vjs_forFollowBackDiv"]/div[10]/div[4]/span[2]').get_attribute('textContent')
total_sec=Live.str2sec(total_time)
current_time=self.dr.find_element_by_xpath('//*[@id="vjs_forFollowBackDiv"]/div[10]/div[4]/span[1]').get_attribute('textContent')
current_sec=Live.str2sec(current_time)
sign_sec=int(total_sec*int(progress)/100) #最后签到的时间
iterate=int((sign_sec-current_sec)/60)
for i in range(iterate):
self.dr.execute_script("document.getElementsByTagName('video')[0].currentTime = document.getElementsByTagName('video')[0].currentTime + 60")
time.sleep(1)
i+=1
if i==iterate:
print('\n')
print('已到达上次观看位置', end='\n')
break
except:
pass
这是一个障眼法,实际并不是加速播放,而是每隔0.5秒将进度条向右拖1分钟,实现方法是这样一个JavaScript:
document.getElementsByTagName('video')[0].currentTime = document.getElementsByTagName('video')[0].currentTime + 60
只要进行条件判断,符合条件的话就不间断执行这条脚本,看起来就像是加速播放了。
3. 多线程:
上述功能几乎都是并行的,仅靠一个线程难以实现,所以我使用了多线程,这可以从主函数中看出:
def main():
configs=config()
configs=[i.strip('\n') for i in configs]
logins=login(configs)
driver=logins[0]
class_type=logins[1]
threads=[]
if class_type=='lesson':
Zhihuishu=Lesson(driver)
threads.append(threading.Thread(target=Zhihuishu.is_exist))
threads.append(threading.Thread(target=Zhihuishu.show_progress))
threads.append(threading.Thread(target=Zhihuishu.close_web))
threads.append(threading.Thread(target=Zhihuishu.close_timeup))
elif class_type=='live':
Zhihuishu=Live(driver)
threads.append(threading.Thread(target=Zhihuishu.show_progress))
threads.append(threading.Thread(target=Zhihuishu.play_time))
else:
raise Exception('请检查输入的链接,需包含http头')
for thr in threads:
thr.setDaemon(True)
thr.start()
for thr in threads:
if thr.isAlive: #阻止主线程直接结束
thr.join()
首先进行视频类型的判断。
对于课程视频(Lesson类),我定义了五个函数实现功能,自动跳过警告和学前必读的skip_iknow()是更先触发的,因此在__init__()中运行。
而剩下四个是同时进行的,为了防止它们相互干扰,我建立了4个子线程并行处理。直播视频(Live类)也是类似。
对子线程的要求是:每个子线程既可以完整运行(即主线程不会提前结束),又可以在主线程结束后同时结束。
对于之一点,考虑使用Thread.isAlive判断每个子线程是否活跃,如果活跃,则利用Thread.join()将该子线程再次并入,保证主线程始终处于阻塞状态,不会自动终止;
对于第二点,只要将每个子线程都设为守护线程,即可保证当主线程终止时,子线程也全都结束。
一个提问:
身边有很多人也在学Python,所以这里留一个问题,有兴趣的可以想想:
下面这段是Live类中的_init_和is_element_present两个方法:
class Live():
def __init__(self, dr):
self.dr=dr
self.dr.implicitly_wait(10)
time.sleep(2)
WebDriverWait(self.dr, 10, 0.5).until_not(EC.presence_of_element_located((By.XPATH,'//*[@id="popbox_title"]')))
if not Live.is_element_present(self.dr, By.XPATH, '//*[@id="popbox_title"]'):
#首次设置流畅
self.dr.execute_script('document.querySelector("#vjs_forFollowBackDiv > div.controlsBar > div.definiBox > div > b.line1bq.switchLine").click()')
title=self.dr.find_element_by_xpath('//*[@id="wh_live_name"]').get_attribute('textContent')
print("正在播放回放:"+title)
def is_element_present(driver, by, value): #判断网页元素是否存在
try:
element = driver.find_element(by=by, value=value)
except NoSuchElementException:
return False
return True
其中第7行,进行判断时使用了
Live.is_element_present(self.dr, By.XPATH, '//*[@id="popbox_title"]'):
这个语句,其中调用了is_element_present这个方法。
那么问题来了,
为什么使用的是Live.is_element_present() 而不是 self.is_element_present() 呢?
能回答出来这个问题的话,就说明对类和类方法的理解还是比较深入的。
最后:
这个脚本使用的第三方库只有 selenium 和 webdriver_manager 两个
from selenium import webdriverfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.support.wait import WebDriverWaitfrom selenium.common.exceptions import NoSuchElementExceptionfrom webdriver_manager.chrome import ChromeDriverManagerimport time, threading, re, os
webdriver_manager 的用处是自动配置 ChromeDriver 简化 *** 作。如果有人需要这个脚本的话,可以找我。
电脑里有 Python 3.8+环境和 Chrome,再安装这两个库应该就可以跑了,当然肯定还有Bug。
至于源码...太长这次就不发了,反正发了也没人看。