我是如何与在线选修课(水课)作斗争,同时复习前端知识的

我是如何与在线选修课(水课)作斗争,同时复习前端知识的

  • 太困了未完待续
  • 题记:从我们的新选修课开始
  • 于是:有压迫就有反抗
  • 作战方针
  • 1战争打响
    • 1.1 取消自动停止
      • 1.1.1打倒video
      • 1.1.2擒贼擒王
    • 1.2播放时自动答题
      • 1.2.1暴力消除
      • 1.2.2暴力答题
      • 1.2.3刚柔并济
    • 1.3自动进入下一节
      • 1.3.1兵来将挡(变换网址)
      • 1.3.2水来土掩(修改文档)
      • 1.3.3见贤思齐(ajax异步)
      • 1.3.4实际情况
    • 1.4课后练习(这个似乎有点难啊)
  • 2前车之鉴
            • 1. 减少html中的信息量
            • 2. 使用css+js代替html属性作为标志
            • 3.保存数据依靠服务器进行验证
            • 4.更新DOM而非内容
            • 5.js定期重置
            • 6.不固定的内容套路
            • 6.使用非文本内容显示文字
  • 3可能的扩展和求助
    • 3.1扩展
      • 3.1.1从登陆到听课的自动化
      • 3.1.2完整的对象封装
    • 3.2进度记录
      • 3.2.1 No.1 2018-12-2 v1.0.0 初步完成
      • 3.2.2 No.1 2018-12-3 v1.0.2 更上一层
      • 3.2.3No.22019-3-1 浏览器插件

太困了未完待续

题记:从我们的新选修课开始

大三,是迷茫的一年,一边稚气未脱,一边就要独当一面,在这一年,我也不得不开始思考自己未来的方向,并为之努力。
但是,就在这个时候,我们忽然多了一门选修课(必选的选修)。要去我们学校与访问全国大学生创新创业实践联盟合作的网站观看一门创业课程课程并完成练习考试。观看情况,练习成绩和考试结果都会计入最终成绩。
打开网页,登录,进入课程,然后,一点点火花,在黑暗中闪过,引燃了地下的枯木,并迅速挥舞蔓延,唤醒了我从小学到初中到高中一直以来被老师家长不停监督的恐惧。
一课程五章,每张分节,每节听的时间和时长最多查10秒,听完一节才能去下一节,每节中间还有测试,作对才能继续听,这都是网课套路,没什么。但过分的是————不能 切 页 面
只要鼠标离开网页显示部分,无论是最小化,切页面,切其它应用,甚至是移动到浏览器上边框和控制台,视频都会立刻停止播放!
我知道你是不想让学生开网页玩游戏来刷课,但是
这!不是监督!
这!简直是!不信任! 逼迫!

不过想到导员平时的谆谆教诲,还是决定 好(LAO)好(ZI)地(BU)学(XIANG)习(KAN)
上图
我是如何与在线选修课(水课)作斗争,同时复习前端知识的_第1张图片
我难受,所以,我要力所能及地让自己开心

于是:有压迫就有反抗

我决定 开发一段js脚本,来帮助我刷课(如果可以)
应该也可以用Python等编程语言开发一个爬虫变种,但是目前我的发展方向是前端,于是就靠纯前端方案实现。
(PS:听了前两节,内挺还不错,而且最后考试也是讲课的内容,所以我还是会亲自听的)
但是我还是要写js 因为这是我的态度(有志从事开发的男人)

作战方针

对方人多势众,而我一个穷学生没实力没有体力没有时间没有钱,所以我要找到对方的重要目标,绑架!撕票,然后看剩下的作鸟兽散
基于360浏览器(学校建议使用360打开),在开发者工具–>source–>sinppets中保存脚本,然后手动运行载入网页(因为在这个页面的js代码会有浏览器保存)
测试过程中使用的代码直接由控制台输入
首先会记录我的思考过程和代码,并根据需求分析原网站的代码实现,最后会将先前代码整合。之后会添加完善过程中涉及的前端知识,并尝试更多的方案。

1战争打响

1.1 取消自动停止

1.1.1打倒video

暂停也好,播放也好,都是发生在video上的事件,从它入手应该是个好方法
观察原来文档源文档的video
播放器有一个video元素实现,绑定了一个点击函数
先绑架人质

var videoDom = documet.getElementById('video');

控制台输入 LearnCourse.stop;
我是如何与在线选修课(水课)作斗争,同时复习前端知识的_第2张图片
没有改写toString()美滋滋,
这里看到点击视频暂停和播放是通过调用video的原生play()和pause()实现的,自动暂停大概也是如此。那,打倒pause()?

videoDom.onpause = ()=>{video.play()}; //视频无法暂停了

这里给视频的暂停函数添加了一个回调,会在视频暂停完成后立即执行(立即播放(~ ̄▽ ̄)~ )
当然也可以直接对pause方法下手

videoDom.pause = ()=>{}; 	//视频无法暂停了
videoDom.pause = null; 	//不暂停,会报错,不过没有影响

修改pause和onpause有一个运行逻辑上的区别,前者仍然可以通过点击暂停按钮来暂停播放,后者则是一切暂停的方法都无法使用了

代码执行后,随意移出鼠标都不会暂停了,相当顺利,不过缺乏成就感,所以,继续寻找其他方案

1.1.2擒贼擒王

自动暂停虽然被取消了,但是我仍然不知道该功能的具体实现,而且video毕竟只是个组件,错误应该有指挥他的人来承担,所以我准备寻探索一下原程序是如何得知鼠标移出的。
大致猜测一下,可能使用的事件有通过focus相关,mouse相关等,绑定的对象可能是html,body,外层div甚至是透明蒙版,而实现方案也有事件绑定,注册监听器,修改on方法等。这还只是我第一步能想到的,实际上还会有更多的可能性。向上面的那样逐方法的尝试自然不可行,我要直接去找到源代码中的相关部分。

先让pause方法抛出异常

videoDom.pause = ()=>{throw new Error('vide should not stop')}; //是pause不是onpause

不修改onpause是因为我想通过控制台输出找到调用pause()的代码位置
鼠标移动到控制台,报错信息输出
我是如何与在线选修课(水课)作斗争,同时复习前端知识的_第3张图片
点击第二行定位到代码

		//这是网站原代码,调用了jquery的hover方法,底层依靠 mouseenter 和 mouseleave 事件实现
            $("#hostBody").hover(function(){
              return false;
            },function(){
                if(!cli){
                    var video = document.getElementById("video");
                    if(video){
                        video.pause();
                    }
                }
            });

这还不好办?覆盖之!

//#hostBody对应body标签
//其实并不是覆盖
 $("#hostBody").hover(function(){videoDom.play()},function(){videoDom.play()});

这里牵扯到一个监听器链的概念,jquery底层调用的是addEventListener方法,当对同一个Dom对象的同一事件使用多次addEventListener时,并不会相互覆盖,而是形成一个先注册先运行的监听器链,所以这里的逻辑其实并不是覆盖,而是类似上面onPause那样在暂停之后迅速开始播放。(感觉会很耗性能)
当然,也可以从监听器链中删除之前注册的监听器。
于是,我有了两种取消暂停的方案(・ω<) てへぺろ

第一场战役以自由和民主的胜利告终

后来在代码中发现还注册有visibilityChange监听器,不知为何没有触发(兼容?),以后再研究

1.2播放时自动答题

网课最常见的套路,播放中一个对话框忽然弹出,长这样
我是如何与在线选修课(水课)作斗争,同时复习前端知识的_第4张图片
老规矩,先抓人质,但是这个文本框是用layui弹出层框架生成的,也就是说,在出现之前,答题框在html文档中是不存在的,又谈何捕获呢?不过跑得了和尚跑不了呢啥,找不到弹出框,就找弹出框的父元素啊,大不了用定时器循环查询,总能找到的。
观察代码
在这里插入图片描述
即使并不了解layui,也不难看出 弹出层由蒙版(layui-layer-shade)和内容区域(layui-anim)两个平行大div组成,选中后控制台输入$0.parentNode找到父节点

$0.parentNode;//>>> ...

捕获

var bodyDom = document.getElementById('hostBody');//捕获body节点
var classTestDom$;

当然,我们并不需要真的用计时器跑循环,因为,可直接监听文档变化

//@ callback 找到后的回调函数,用来答题或者做其他的什么
function findClassTask(callback)
{ 
	//这里出现了一个闭包
	return function(e){
		classTestDom = $(bodyDom).find('.layui-layer.layui-layer-page.layer-anim');
		if(classTestDom)
		{
			var classTestDomtitle =  $(classTestDom).find('.layui-layer-title')[0];//jquery对象是一个隐式数组
			var title = classTestDomtitle.innerHTML;
			//匹配到标题内容就认为这个弹窗是答题弹窗
			if(/随[\s\S]*堂[\s\S]*测[\s\S]*试/.test(title))
			{
				callback();
			}
		}
	};
}
bodyDom.addEventListener('DOMNodeInserted',findClassTask(callback),false);

既然已经抓到了答题框,那就可以开始愉快的答题了
//后来发现这种方法不够严谨,因为答题期间可能会有其他的页面变化触发DOMNodeInserted事件,同时查看文档发现Mutation类事件已经不建议使用,由MutationObserver对象代替

//改写后的代码
var classTestDom;       //答题框
function findClassTask(afterFound)
{ 
    //这里出现了一个闭包
    return function(mutations){
        // classTestDom = $(bodyDom).find('.layui-layer.layui-layer-page.layer-anim');
          mutations.forEach(function(mutation) {
            for (var i = 0; i < mutation.addedNodes.length; i++)
            {
                if($(addedNodes[i]).is(.layui-layer.layui-layer-page.layer-anim))
                {
                    var title = addedNodes[i].querySelector(.layui-layer-title).innerHTML;
                    if(/随[\s\S]*堂[\s\S]*测[\s\S]*试/.test(title))
                    {
                        //是layui弹窗且有对应的title 就认为是答题框
                        classTestDom = addedNodes[i];
                        afterFound()
                    }
                }
            }
        });
    };
}
function afterFound(){...}

var observer = new MutationObserver(findClassTask(afterFound));         //添加回调
observer.observe(bodyDom, options{ 'childList': true,'subtree':false}); //开始监听

此外也可以去插看源代码,消除弹出方法(不考虑后续影响的话),但是本人并不熟悉layui,所以不使用本方案(后面还有其他原因)

1.2.1暴力消除

题目弹出时,控制台输出了“video should not stop” 的字样,视频继续播放,说明之前弹出式视频时暂停只是靠一次简单的pause()实现的。应该也没有什么复杂的回调。
那么,直接删掉对应元素似乎就好了

$(classTestDom).remove();//jquery方法
classTestDom.parentNode.removeChild(classTestDom[0]);    //原生方法

在删除前,其实还有一个隐含的风险(使用服务器验证答案),打开network面板,静等弹出.
发现答题前后没有发生新的网络请求,说明验证完全由客户端进行,remove!(反正服务器不知道发生了什么);
即使这样,也不能保证无恙,因为客户端代码还有可能临时保存答题结果,然后在某一时刻发送给服务器,对前端显示和业务并不造成影响。
况且,我觉得脚本行为应该尽可能的像人,直接屏蔽一个操作是很不像人的行为,应该让脚本能自己选择并提交才对。(这样才能规避网站目前以及以后的反作弊措施)
PS:虽然经过测试直接的删除并没有什么影响

1.2.2暴力答题

这里分为两个部分:暴力和答题
暴力就是穷举,列出所有可能的组合,并且逐个尝试,至死或答对方休,
专业一点说就是多个输出列出所有排列 不想用递归emmmmm

//遍历二维数组排列
//将每个数组的元素都看作是是对应的下标,求下标的排列,然后按下标取对应元素
//这么说方法名有点不合理,不过算了
function toAnswerList(arrayList,isReturnIndex)
{
	
	var result = [];			//最终返回的结果集
	var resultIndex = [];	//返回结果的下标集
	var tempResInd = resIndex; //一个临时量
	var answersList = [];	//arrayList的copy
	var resLength = 1;		//排列总数
	var resIndex = [];		//一组下标,每次按这个下标从各数组中取出一个元素组成一种情况
	//计算排列情况的总数
	for(let index=0;index<arrayList.length;index++)
	{
		//过滤掉空对象和空数组
		if(arrayList[index]&&arrayList[index].length==0)
		{
			index++;
			continue;
		}
		else
		{
			answersList.push(arrayList[index]);
			//resIndex初始值是数组最后一个元素下标
			resIndex.push(arrayList[index].length-1);
			resLength *=arrayList[index].length;
		}
	}
	//逐个计算排列
	while(resLength>0)
	{
		let res = [];
		let answer;		//备选答案

		//生成一个答案(数组)并添加返回数组中去
		for(let index=0;index<answersList.length;index++)
		{
			answer = answersList[index];			//第index个数组
			res[index] = answer[resIndex[index]];	//该数组下标为resIndex[index]的元素
		}
		result.push(res);
		resultIndex.push(resIndex.concat());	//存入一份深拷贝

		//我要管这个方法叫拨算盘
		//修改下表排列
		for(let index=0;index<resIndex.length;index++)
		{
			resIndex[index]--;
			if(resIndex[index]<0)
			{
				resIndex[index] = answersList[index].length-1;
			}
			else
			{
				break;
			}
		}
		resLength--;
	}
	return isReturnIndex?resultIndex:result; //参数isReturnIndex存在且为真 返回下标集合
}


答题,就是模拟点击,可以在浏览器中模拟mouse事件,也可以调用对应元素的click方法,在这里目标元素很明确,所以我选择后者

当然还是要先抓人质的了
观察文档

<div class="layui-layer layui-layer-page  layer-anim" id="layui-layer100002" type="page" times="100002" showtime="0" contype="string" style="z-index: 100003; width: 600px; height: 400px; top: 150px; left: 656.5px;">
	<div class="layui-layer-title" style="cursor: move;">随堂测试div>
	<div id="doclasswork" class="layui-layer-content" style="height: 299px;"> 
		<div style="top: 10px;position: absolute;padding: 20px 40px;" id="homeWorkTemp">  
			<p>  
				<label>1. label>  
				<lable>  (单选)  lable> 
				<label>全食超市超越沃尔玛的方法是()label> 
			p> 
			<p class="options" cwid="50112" itemtype="1">      
				<span class="radioCli" style="cursor: pointer;"> 
					<i class="input radio bg" type="radio" name="50112" id="A0" value="A">i> 
					<label style="cursor: pointer;">A.label> 
					<label for="A0" style="margin-right: 20px;cursor: pointer;">模仿label><br>
				span>     
				<span class="radioCli" style="cursor: pointer;"> 
					<i class="input radio bg" type="radio" name="50112" id="B0" value="B">i> 
					<label style="cursor: pointer;">B.label> 
					<label for="B0" style="margin-right: 20px;cursor: pointer;">就业label><br> 
				span>      
				<span class="radioCli" style="cursor: pointer;"> 
					<i class="input radio bg" type="radio" name="50112" id="C0" value="C">i> 
					<label style="cursor: pointer;">C.label> 
					<label for="C0" style="margin-right: 20px;cursor: pointer;">昂贵label><br> 
				span>     
				<span class="radioCli" style="cursor: pointer;"> 
					<i class="input radio bg" type="radio" name="50112" id="D0" value="D">i> 
					<label style="cursor: pointer;">D.label> 
					<label for="D0" style="margin-right: 20px;cursor: pointer;">创新label><br> 
				span>      
			p> 

。。。那个单选好吓人。。。 做了几节,没有发现多选题,那就先不考虑这个 ,留下一个TODO,以后再实现吧。
这里的单选框也不是input 而是用i标签模拟的

//捕获目标dom
var options$ = classTestDom$.find('p.options');
var classTestSubmintDom = classTestDom$.find('div.layui-layer-btn layui-layer-btn-c');
var optionDoms = function(){
	var opts = [];
	for(let i=0;i<options$.length;i++)
	{
		let radios = [];
		let radios$ = $(options$[i]).find('span.radioCli');
		//由属性 style="cursor: pointer;"得知 整个span标签都添加了点击事件
		for(let x=0;x<radios$.length;x+)
		{
			radios.push(radios$[x]);
		}
		opts.push(radios);
	}
	return radios;
}();

组合起来,有一点要注意,在答题正确后,弹窗会消失,这时要即时终止循环

//答题的方法
function doClassTest(options)
{
	//options起码是一个二维数组
	//三维数组是多选
	});
	var tryList = toAnswerList(options);
	//按照tryList的顺序尝试
	var isRight=false;
	for(i in tryList)//选一组答案
	{
		for(x in tryList[i])  //选一道题
		{
			//兼容多选
			if(Array.isArray)
			{
				if(!Array.isArray(tryList[i][x]))
				tryList[i][x] = [tryList[i][x]];
			}
			else
			{//仍然不保险
				if(! tryList[i][x]instanceof Array)
				tryList[i][x] = [tryList[i][x]];
			}

			for(y in tryList[i][x])
			{
				tryList[i][x][y].click(); //这是一个jquery对象,不过并不影响
			}
			
		}

		classTestSubmintDom.click();
		//提交答案
		if($(bodyDom).find('.layui-layer.layui-layer-page.layer-anim').length==0) break;//找不到答题框框说明回答正确
			
		//如果原逻辑是异步的(应该就是异步的),有可能会继续循环到报错
		//不过报错并没有影响
	}
}

此刻 脚本的行为就像一个不好好上课的学生,企图靠疯狂试错蒙混过关,我需要想办法提高一下成功率.
观察代码…
我是如何与在线选修课(水课)作斗争,同时复习前端知识的_第5张图片
这。。。。把正确答案直接写html里也太。。。 好歹拿个js对象包装一下啊
好了,既然小抄在手,那天下我有,作弊!直接满分!

//拿到正确答案
var  trueAnswers = function(){
	var answers = [];
	var answers$ = classTestDom$.find('.answer');
	for(let i=0;i<answers$.length-1;i--)
	{
		answers.push(answers$[i].html().replace(/[^A-z]*/i,''));
	}

}();

1.2.3刚柔并济

作弊是很容易被抓的,万一老师喜欢提问表现突出的学生呢?所以 中庸才是我的菜
一个中庸的学生有很多特征
很少一次全对,但也很少一次全错
答题速度不快,间隔不等
答题之后会有错误提示,不会修改已经正确的答案
emmmmm。。。。。。功能好多,不过都可以解决,以后再说

//TODO

哦对了,在答题框弹出的时候,视频应该是暂停状态。如果我继续使用前面敲掉pause的方法的话,等于视频没有暂停方法了,这个需求也就不可能实现
而通过hover()添加监听器的话,即防止自动暂停,又不影响video本身的属性,这样就有了扩展的余地(论分模块的重要性)

保险起见,修改一下hover实现

function show(isTask)//isTask表示是否正在做题
{
	isTask?videoDom.play():return false;
}
("#hostBody").hover( show(isTask), show(isTask));
//写到这里就发现之前代码不完善的地方了
//之前通过dom查找修改istask的值,但是异步可能会导致报错,使方法在修改isTask的值之前就停止,
//还是移除监听比较好吧  TODO 

实现这些操作之后,脚本走起来像学生,说话也像学生,那对客户端程序来说,它就是学生了(鸭子)

1.3自动进入下一节

原来的逻辑,视频观看结束后暂停播放,手动点击进入下一节

这里有三个问题要处理:
进入下一节的时机,如何进入下一节,

时机很好判断,视频播放完毕该继续

var toNextTitle;
videoDom.addEventListener('onended',toNextTitle,fasle);

而进入下一节的方式(先不看代码)则会影响脚本的实现逻辑,我大致想到三种可能情形
变换网址
修改文档或者样式
ajax异步加载新页面
下文详细描述

1.3.1兵来将挡(变换网址)

变换网址看起来很处理,无论在控制台里做了什么操作,地址栏一刷新什么都没了,在原网页没有漏洞的情况下又不能用get传递可执行语句。
但是 还是有办法的
这个办法就是Window对象 有人会说js代码的执行环境就是当前的网页,这样的说法很不严谨,客户端js执行的直接环境其实是由浏览器根据当前页面创建的window对象(全局),js文件中的变量和方法定义,实际上是为widow对象添加属性或者属性的属性。不同的页面(包括iframe)中的window不同,所以js只能操作本网页,然而,window对象有一个属性window,是一个对自己的引用,使得内部属性(的属性)可以使用window而不是this来访问它。所以如果能有一个属性存储其它的window对象,不就可以用js操作其它页面了?
首先要屏蔽掉原来的翻页方法(防止他在我之前刷新页面)
然后获得新window

var w = window.open(url);//window.open()返回新的window对象

为了方便,我应该把之前的脚本封装为一个对象(或者函数),作为属性添加进新的window后执行

function auto(window){.....}	//这样也可以,就无所谓在那个window对象里执行了

最后再添加一点点代码确定让新建window和关闭旧window循环执行

PS:不同window间互相操作室收到浏览器同源策略限制的,不能跨域操作,所以不要幻想能靠他做什么坏事了
还有,不同页面间也是可以通过一些可读写共享变量传递信息的(比如windw.name)

1.3.2水来土掩(修改文档)

这里也分两种情况,如果换节时文档结构不变,只是标签内容(比如video的src)发生变化,那脚本无需任何变动,以DOM对象为基础的操作,既然对象不变,就没有变动的必要
而如果是靠元素的隐藏和显示切换,或者js删除原dom并新建标签这样改朝换代,我们原来绑架的领导人就失去了意义,此时需要绑架新的dom元素并把原对象交给垃圾处理机制(虽然不交也可以)

1.3.3见贤思齐(ajax异步)

其实ajax和修改文档的思路非常类似,是指数据来源从页面数据编程了服务器获取,只要多考虑一个异步问题就好,因为ajax函数的回调是异步执行而且有不确定延时的,所以新的脚本代码不能立即执行,而设置为在一个靠谱的时间后或者确定回调完成后再执行。

1.3.4实际情况

为了下一节是哪一节,还要观察文档构造
我是如何与在线选修课(水课)作斗争,同时复习前端知识的_第6张图片
发现两个特征:
1. 正在播放的章节currentknowledge=“1”;反之为"0"。 2. 已经看过的章节有类名videoFinish;

var currentTitleDom = document.querySelector('li .partul a[currentknowledge="1"]');
var currentTitleParentDom = currentTitleDom.parentNode.parentNode;	//对应一个li元素
var nextCurrenTitleDom; //下一个章节对应的元素
if(!currentTitleParentDom.nextElementSibling.nextElementSibling) //TODO需要判断是否是当前章最后一节
{
    //这里的dom查找有点麻烦,不过我还没有发现每一章的li有什么特点
    // document.querySelector('li .partul a[currentknowledge="1"]').parentNode.parentNode.currentTitleParentDom.parentNode.parentNode.parentNode.nextElementSibling;
    var nextTitleListDom = currentTitleParentDom.parentNode.parentNode.nextElementSibling;
    nextCurrenTitleDom = nextTitleListDom.querySelector('a[currentknowledge="0"]');
    console.log('路径1',nextCurrenTitleDom);
}
else
{
	nextCurrenTitleDom =  currentTitleParentDom.nextElementSibling.querySelector('a[currentknowledge="0"]'); 
}

表示title的虽然是a链接 但是 herf属性为javascript:void(0),点击不会发生跳转,而是调用οnclick=“LearnCourse.initVideo(940,36547,1,event)” ;
//之前已经注意到存在整体控制的LearnCourse对象,本来还担心切换章节会产生新的LearnCourse对象导致之前的部分代码失效,应该也不用担心了
定位到代码,发现了一个ajax方法

            $.ajax({
                url : "web/students/getAjaxVideo",
                type : "POST",
                data : {courseId:cbiId,catId:catId},
                dataType : "json",
                success : function(data){
                    if(data != null){
                        viewSwitch("video");
                        video.src= data.teachVideo;
                        video.play();  
                        teachingResource= data.teachingResource;
                        currentCatId= catId;
                        totalHour= data.totalHour;
                        maxTime= data.totalHour;
                        videoLength= data.videoLength;

从这里看,应该只是video修改了src属性 (和其他一些边角操作),是比较容易的方案。打开控制台再进入下一节,也没有dom本身的更新,还会自动播放下一节。
那,就不用管什么AJAX了 , 实际上什么都不用管啊 木哈哈哈哈

1.4课后练习(这个似乎有点难啊)

点开课后练习,新页面出现,使用window.open获得新对象即可,不过这次不但需要将答案发给服务器,而且还有主观简答题,题目也是pdf显示而非html,没有脑子的脚本可能真的做不到了。(;´д`)ゞ
不过还是有一些余地的
因为使用pdf显示题目,所以不同同学的题目顺序是一样的(不会变态到卷子不一样吧),这种情形下一般总会出现"XX科目习题及测试答案.doc"(主观题除外),准备好代码,答案发下来之后,然后手动输入(粘贴复制)配置信息,填写什么的交给脚本就好(但是我还是会自己写的,真的!有点想法也不行吗) 还好答题使用html实现的

第一个问题, 没有a链接。。。。。
我是如何与在线选修课(水课)作斗争,同时复习前端知识的_第7张图片
可以看到,a链接的herf属性同样没有用,打开页面由LearnCourse.examination方法内部实现。
忽然害怕…TM的不会是变动的地址吧!!!
原代码中找到的这些

function examination(curThis,courseId,ccId,knoId,type){
            var chapterId= ccId||courseId;
            var ccId= (knoId||ccId||0);
            var id= "";
            ............
        	function doNext(courseId,type,ccId,chapterId){
            layer.load(1, {shade: [0.1,'#fff']});
        	$.ajax({
                url:"web/examination/exam/check/"+courseId+"/"+type+"?ccId="+ccId,
                type : "post",
                dataType : "json",
                async: false,
                success:function(res){
                    var id=type<=2?"#cata_"+ccId:"#course_"+type;
                    if(res.code==1){
                        newWin(getUrl(8,courseId+"_"+ type+"_"+ ccId+"_"+chapterId));
  			.........
        }
        function newWin(url) {}window.open(url)}

大致逻辑似乎是examination(curThis,courseId,ccId,knoId,type)接受参数,先使用type判断类型,如果是测试的话由ajax得到是否可以进行,可以进行的话用getUrl(index,str)方法得对应的url,最后用newWin(url)打开新页面。分支情况很多,不过我只关心我需要的部分即可。
手动测试了几次,发现网址不变,直接复制到URL也可以打开…(n??这样的话测试能否进行的意义是??)
又定位到了getUrl()方法
我是如何与在线选修课(水课)作斗争,同时复习前端知识的_第8张图片
混洗处理… 防脚本的吗…
但是… 这是个全局方法啊!!!! 连参数都是在html文档中提供出来的。
我管你内部逻辑是什么呢!
直接调用不就行了! 就算每次访问地址会变又有几毛钱关系?

地址问题解决了,去看看答题的页面
我是如何与在线选修课(水课)作斗争,同时复习前端知识的_第9张图片
也没有什么复杂结构。开始的上代码
不知为何,在sinppets面板中无法使用window.open()方法,先写成一个函数在控制台调用

var W;
function sleep(d)
{
  for(var t = Date.now();Date.now() - t <= d;);
}
var aIndex =0;//第几课的课后练习  (仅在第一章内有效)
//type:1单选 2多选 3判断 4填空 5主观
//答案,看一眼就懂的
var orders = [
	inputOrder(1,'A',1),inputOrder(2,'B',1),
	inputOrder(3,'C',1),inputOrder(4,'D',1),
	inputOrder(5,['A','B'],2),inputOrder(6,['C','D'],2),
	inputOrder(7,['创业有益','创业有害'],4),
	inputOrder(8,['创业随心','创业维艰'],4),
	inputOrder(9,['量力而行'],4),
	inputOrder(10,true,3),inputOrder(11,false,3),
	inputOrder(11,'我觉得创业还有很长的路要走',5)
];
function inputOrder(index,value,type)
{
	return{
		index:index,
		value:value,
		type:type
	};
}
//不知为何,在sinppets面板中无法使用window.open()方法,先写成一个函数在控制台调用
function openTestPage(data)
{
	//先随便抓一个对应考试的 a 链接
	var aDom = document.querySelectorAll('input.endTime')[aIndex].parentNode.querySelector('a');
	//得到参数列表
	var params = aDom.getAttribute('onclick').split(/[\(\)]/)[1].split(",")
	var courseId = params[1];	var ccId = params[2];
	var knoId = params[3];		var type = params[4];
	//按照原网站逻辑拼接参数
	var chapterId= ccId||courseId;
	ccId= (knoId||ccId||0);
	var param = courseId+"_"+ type+"_"+ ccId+"_"+chapterId;
	var url = getUrl(8,param);
	window.name = 'master';
	var w = window.open(url,'test');
	w.masterHerf = window.herf;
	w.autoDoTest = ()=>{autoDoTest(w,orders)};
	w.open(w.masterHerf,'master');
	W = w;
	setTimeout(()=>{w.autoDoTest();},2000); 
	//这里有一个大坑,open方法是异步延时加载的
}
function autoDoTest(w,datas)
{
	var funcs = [];
	var TMS = {A:0,B:1,C:2,D:3};
	var index =0;
	var testDom = w.document.querySelector('.answer');
	var submitButtonDom = w.document.getElementById('submitBtn');
	var currentQuestionDom = testDom.querySelector('.cnts p');
	//type:1单选 2多选 3判断 4填空 5主观
	funcs[1] = (questionDom,value)=>{
		var optIndex = TMS[value];
		questionDom.querySelectorAll('.radioAndValue')[optIndex].click();
	};
	funcs[2] = (questionDom,value)=>{
		var optionDoms = questionDom.querySelectorAll('.checkboxAndValue');
		console.log(questionDom,optionDoms);
		for(i in value){
			optionDoms[TMS[value[i]]].click();
		}
	};
	funcs[3] = (questionDom,value)=>{
		if(value)
		{
			questionDom.querySelectorAll('.radioAndValue')[0].click();
		}
		else
		{
			questionDom.querySelectorAll('.radioAndValue')[1].click();
		}
	};
	funcs[4] = (questionDom,value)=>{
		var inputDoms = questionDom.querySelectorAll('input.cardText');
		var inputDom;
		for(i in value){
			inputDom = inputDoms[i];
			inputDom.value = value[i];
		}
	};
	funcs[5] = (questionDom,value)=>{
		var inputDom = questionDom.querySelectorAll('textarea');
		inputDom.value = value;
	};
	//答题方法
	function doQuestion(questionDom,data)
	{
		var func;
		//简单验证一下  不严格相等
		if(questionDom.querySelector('.num').innerHTML.trim()==data.index)
		{
			func = funcs[data.type];
			func(questionDom,data.value);
		}
	}

	//迭代
	doQuestion(currentQuestionDom,datas[index]);
	while((currentQuestionDom =currentQuestionDom.nextElementSibling))
	{
		sleep(100);
		console.log('第'+index+'次迭代',currentQuestionDom);
		index++;
		doQuestion(currentQuestionDom,datas[index]);
		
	}
	sleep(500);
	//点击提交按钮
	submitButtonDom.click();
	//通知window回来
	window.open(w.herf,'test');
	w.open(w.masterHerf,'master');
}

测试可用
我是如何与在线选修课(水课)作斗争,同时复习前端知识的_第10张图片
这个写下来还真的不是很轻松…一个注意点是两个window对象分别是不同的作用域。
还有就是浏览器对window方法的各种限制(以前没有发现的);

  1. .open()方法打开新页面后,不会直接加载文档,而是要等到当前脚本执行完毕(可能是因为后面的代码可能修改新window的属性),在这之前winow中的dom只有一个空的document.
  2. var w = window.open(url).
    通过window修改window的Dom不会立即生效,而是在脚本执行完后统一修改(应该是性能问题)。
  3. 还没有找到办法打开新页面再回到旧页面。

因为这些原因,目前实现的方案和最佳预期还有一些小差距。不过已经没有大碍, 睡觉。

2前车之鉴

如果是我来开发的前端
假设我防止我这样的人
我可以做什么

1. 减少html中的信息量

类似于课堂测试弹窗的答案,这部分信息对于正常用户来说是完全不可见的。把它放入html中就是没有必要的(个人觉得html中的信息尽量应该只有显示相关的),同时也给了脚本可乘之机。 对于这一类数据封装成js对象可以为脚本编写增加不少困难(要读懂部分js逻辑)。或者,干脆封装为一个对象不可读的私有变量。(当然没有什么拦得住穷举)。
另外还有一种信息是HTML中的事件定义 ,可以依靠分离js代码来减少(组件化开发就是另一回事了)

2. 使用css+js代替html属性作为标志

在从list抓取当前dom的时候,因为当前的dom元素总有显著的属性标志(类名或者其它html属性),大大的加速了我的编写速度。个人觉得可以使用js创建新css代码 用:nth-child(n)等方法来标记当前dom。仍然有迹可循,但不会那么轻松了。

3.保存数据依靠服务器进行验证

前端验证就是纸糊的,不依靠服务器,前台的验证就完全不可信。对于用于行为判断和资格验证的操作,最好还是交给服务器审查。如果这样开销太大,可以先保存起数据,在有必要进行网络请求的时候一并送往服务器(虽然大部分数据可能没给服务器就关页面了)

4.更新DOM而非内容

内容变幻时直接修改DOM对象,可以让之前脚本捕获的对象和注册事件失去作用。加大脚本难度

5.js定期重置

原网站中,各种监听器的注册由一个LearbnCourse对象进行(只进行了一次),如果可以在进入下一节时,将LearnCouse的初始化操作在进行一次,将事件监听,关键属性等重置一次,或者干脆定时执行。把一写重要属性设置为不可配置也可以压缩脚本的功能空间。

6.不固定的内容套路

比如,在每次加载文档或者服务器返回前(或者是更新dom时),依据一个结果不可预测的方法将一些标志属性字段全部修改为另一个值(比如元素ID class)。这样脚本在元素捕获时就会面对很大的困难

6.使用非文本内容显示文字

比如EMMMM-- 图片?

真的只是意淫啊!以上内容不保证任何可行性,不保证没有(几乎一定有)负面影响,不保证符合任何技术标准 不喜勿喷哦

当然,系统的成本和完备性都是开发时的考量,我也只是以自嗨为目的乱想一下,并没有任何贬低该网站和开发者的意思,相反,源代码中有很多巧妙的实现值得我去学习,开发者的水准很有可能远在我之上,只是没有处处完善的需求。之前我的种种尝试也只是刚入门者的花拳绣腿罢了,况且 或许是开发者特意网开一面呢?

3可能的扩展和求助

3.1扩展

3.1.1从登陆到听课的自动化

如果能只配置一下用户名和密码,就可以自动完成(登出)登入,听课这一系列造作,岂不是很美妙(我可以拿去买吗ヽ( ̄▽ ̄)ノ),大多数操作都可以由上文中提到过的方法进行,只有在登录时需要输入验证码,不过是最简单的数字验证码,如果没有后台校验,直接敲掉就好,如果有的话,《白帽子讲web安全》一书中提到过用canvs实现的解决方案(别说彩虹表)

3.1.2完整的对象封装

上述的代码仍较为零散,实现比较单一,也没有考虑浏览器兼容性,也有很多边界情况和非法输入不予考虑,因为编码操作太零散,甚至代码风格也不太一致(移除和覆盖乱着用,原生js和jquery乱着用,添加监听和onXX乱着用,要说好处可能就是大部分有用的变量都定义为全局方便未来调用了。)
PS:用jquery是因为原网站已经导入了jquery
有时间的话会打包为一个完整的,可参数化配置的对象,简化修改和使用(还能防止污染命名空间)

3.2进度记录

3.2.1 No.1 2018-12-2 v1.0.0 初步完成

终于(大部分时间都是在写博客QAQ不是代码),一组可以使用的刷课脚本编写完成了(不能和外挂比啊~~ 差得远呢)。
简单理顺了一下,因为定时弹出的答题框没有任何实质作用,课后习题提交与否也不影响观看,(其它都自己扩展出的考量和实现方案)所以实际上只要屏蔽暂停功能和自动进入下一节就足矣。
一个最简短的可使用脚本

var videoDom = document.getElementById('video');
$("#hostBody").hover(function(){videoDom.play()},function(){videoDom.play()});

videoDom.onended = ()=>{
    var currentTitleDom = document.querySelector('li .partul a[currentknowledge="1"]');
    var currentTitleParentDom = currentTitleDom.parentNode.parentNode;	//对应一个li元素
    var nextCurrenTitleDom; //下一个章节对应的元素
    if(!currentTitleParentDom.nextElementSibling.nextElementSibling) 
    {
	   //这里的dom查找有点麻烦,不过我还没有发现每一章的li有什么特点
	   // document.querySelector('li .partul a[currentknowledge="1"]').parentNode.parentNode.currentTitleParentDom.parentNode.parentNode.parentNode.nextElementSibling;
	   var nextTitleListDom = currentTitleParentDom.parentNode.parentNode.nextElementSibling;
	   nextCurrenTitleDom = nextTitleListDom.querySelector('a[currentknowledge="0"]');
	   console.log('路径1',nextCurrenTitleDom);
    }
    else
    {
	   nextCurrenTitleDom =  currentTitleParentDom.nextElementSibling.querySelector('a[currentknowledge="0"]'); 
    }
    nextCurrenTitleDom.click();
};

虽然可以开始愉快的刷课了,但是要求不能这么低,上文描述的部分非必须功能还没有实现。很多实现的代码也没有用,明天会整合出一个功能较为全面的代码,未实现的功能和未尝试的方案以后也会更新。
整个过程并没有遇到什么太大的困难,但是涉及到了大量的前端基础知识,其中有不少有些不清晰甚至遗忘的部分顺便复习了一下。所以还是想挑战一点难度,让脚本更上一层楼。 不过就今天来说,可以先睡觉了(。-ω-)zzz。
PS:要是有猜出来我是那个学校的同校校友,拿走不谢(ノ ̄▽ ̄)

3.2.2 No.1 2018-12-3 v1.0.2 更上一层

在1.0.0版本上加入了自动答题的功能(代码量膨胀了200行)
编写了一段独立脚本用来在新页面自动完成每一节的课后测试(参见1.4

3.2.3No.22019-3-1 浏览器插件

间隔了3个月 ,应班委要求再次跟新 这次将代码完整封装成了浏览器插件 使用难度------
不过还是有一些问题 ,插件执行的js似乎无法调用页面原来的js代码 ,就是说jq无法使用,之前代码中用$实现的操作全部修改为原生js实现(好烦啊),
插件源代码和打包后的代码已经上传git ,有需要可参考
最后晒一个效果图(加了个页面嘻嘻嘻)
我是如何与在线选修课(水课)作斗争,同时复习前端知识的_第11张图片

你可能感兴趣的:(前端,尝试)