起因


故事的开始是大二的上学期,有一门叫计算机结构(computer organization)的课。新教授这门课的教授在原来的政策上做了一些变动。他引入了一个叫做zybook的作业平台来确保我们能跟上每周的课的进度,即每周做一章(400-500道题,前几周甚至有1500题一周的章节)各种各样的小题。

但是!!!

这些题目简直又多又无聊!!

这些题目主要由选择题,填空题,视频播放题(多个阶段的动画演示,需要你看完一个section点击继续以进入下个section)构成。听着好像很多样,但是每道题,尤其最开始的几周,都简单的就像给完全没上过课的人做的...

就这样,每个周末都得做在床上耗一下午一边看着柯南国配版一边应付这些无脑的题,让我不禁开始思考人生:是否可以写个脚本来省去这不必要的工程

可行性分析


题目性质

在这里就不得不提到zybook这些被叫做activity的题目最大的,让我眼前一亮直接打开ide开写脚本的特点:可以暴力破解。这上面所有的activity都没有做错这一概念,即你有无数次的试错机会。

举例来说,对于选择题,你只需要把所有选项都选一遍,就算做对了;对于填空题,它提供了一个双击按钮来展示答案;对于视频播放题,你只需要在每个section的动画播完后,点击一下继续的按钮就行

工具

很久很久以前,有位学数学的研究生学姐来问我能不能帮她爬取一个网站的论文。当时功力不到家的我一看到那网页是动态加载就直接认输了,失去了一个跟漂亮学姐聊天的好机会QAQ

自此之后,我就开始奋发图强!!搜遍全网,了解到有一个神奇的东西叫做selenium

Selenium是一个自动化测试工具,可以模拟用户对网站进行操作,意味着它可以代替我完成那些定位元素,鼠标点击和复制粘贴工作。在这个情况下,没有工具能比它更能满足我的需求了

关于selenium的用法,由于篇幅原因,在此不再详细叙述,感兴趣的大家可以去看这个教程:tutorial

总结:完全可行,开始写码

code, code, code


准备工作

这一步没啥好说的,创建webdriver对象以进行之后的操作

if __name__ == "__main__":
browser = webdriver.Chrome()
action = ActionChains(browser)
browser.get('https://learn.zybooks.com/library')

登录

使用过的人都知道,使用自动化测试工具对网页进行操作不会进入正常的浏览器模式,而是进入专门给测试工具的窗口,类似于隐私模式。这就意味着你每次运行脚本都需要登陆一次,因为里面没有你的历史记录。这个是zybook平台的登陆界面



可以看到,两个Input元素非常显眼的耸立在那。对于这种数量少且位置确定的元素,直接用ID来定位就好。定位到之后用send_key()来把邮箱和密码输入进去

def login(browser):
user_id = browser.find_element(By.ID, "ember9")
user_id.clear()
user = input("input your username(mail address): ").strip()
user_id.send_keys(user)
password_id = browser.find_element(By.ID, "ember11")
password_id.clear()
password = input("input your password: ").strip()
password_id.send_keys(password)
browser.find_element(By.CLASS_NAME, "signin-button").click()

选择课程和章节

登录之后,你会进入zybook的主页面,里面有你注册的课程

检查html后会发现,每一个课程的元素都是名为class的class。那么用class来定位所有课程,之后再选择想要完成的课程就行。

然而出现了一个问题,heading虽然能区分课程,但是无法被点击。那么只好重新找到div里面肯定能被点击的a元素(可能还有别的元素可以点,这里当时懒得细想就直接去找a了)。这里使用了xpath去定位

def get_class(browser):
classes = browser.find_elements(By.CLASS_NAME, "heading")
print("----------")
for x, y in enumerate(classes):
if x == len(classes)-1:
break
print(str(x), ": ", y.text)
print("----------")
choose = int(input("input(0-" + str(len(classes)-2) + ") to choose the course: "))
while choose < 0 or choose > len(classes)-2:
print("invalid number")
choose = int(input("input(0-" + str(len(classes) - 2) + ") to choose the course: "))
browser.find_element(By.XPATH, "//div[@class='zybooks-container large']/a[" + str(choose+1) + "]").click()

之后我们会进入课程页面,里面有每个章节的目录。相同道理,我们需要选择我们想完成的章节来打开里面的section目录

(当然,你也可以直接遍历所有的章节,一次性全部做完。我只是懒得等它运行那么长时间,选择一周运行一次)

def get_week(browser, action):
xpath = "//ul[@class='table-of-contents-list fixed-header header-absent']/li"
week = browser.find_elements(By.XPATH, xpath)
choose = int(input("input week number(1-" + str(len(week)) + ") or -1 to quit: "))
if choose == -1:
return True
while choose < 1 or choose > len(week):
print("invalid number")
choose = int(input("input week number(1-" + str(len(week)) + "): "))
action.move_to_element(week[choose-1]).perform()
week[choose-1].click()
return False

选择题

现在我们终于来到了今天的重头戏,做题!

对于选择题,逻辑非常简单,我们定位所有的选项按钮,遍历并点击即可

def multiple_choice(browser, action):
multi_choice = browser.find_elements(By.CLASS_NAME, "zb-radio-button")
for x, y in enumerate(multi_choice):
if not y.is_enabled() and not y.is_displayed():
continue
action.move_to_element(y).perform()
y.click()

注意,这里,包括前面的get_week() function,我们多了一个叫做action的parameter。这是selenium里面的一个叫做Actionchain的功能。

在之前的测试过程中,我发现有时候网页太长,元素与元素间隔相差太大,脚本会无法立刻跳到那个元素进行操作,因此会直接报错终止。所以,在这里我使用了actionchain,每次在进行操作前先把页面移动到那个元素的位置,这样就可以进行操作了。

并且,由于zybook的网页会有些区别,class定位有时会定位到不是选项按钮的不可见元素,所以我加了is_enable()is_displayed()的if条件来防止误判

填空题

对于填空题,我们看到有一个show answer的按钮,我们可以双击来使右侧出现答案。因此,要做的是:

  1. 定位show answer按钮
  2. 点击,等一小段时间,再点击
  3. 定位答案,储存到变量里
  4. send_key()到input元素中
def filling(browser, action):
show_answer_list = browser.find_elements(By.CLASS_NAME, "show-answer-button")
for x, y in enumerate(show_answer_list):
if not y.is_enabled() and not y.is_displayed():
continue
action.move_to_element(y).perform()
y.click()
time.sleep(0.1)
y.click()
time.sleep(0.1)
# get answer
answer = browser.find_elements(By.CLASS_NAME, "forfeit-answer")[x].text
textarea = browser.find_elements(By.CLASS_NAME, "ember-text-area")[x]
textarea.send_keys(answer)
time.sleep(0.1)
browser.find_elements(By.CLASS_NAME, "check-button")[x].click()
time.sleep(0.5)

视频播放题!!! 最精彩的部分!!!

这个是三种题型里面写码逻辑相对复杂的题。首先,我们需要点击start按钮来开始播放视频。之后,我们需要不断判断视频告一段落没有。因为很显然,播放和暂停是同一个按钮,如果你在播放过程中点击,会暂停动画。只有在动画停止之后,才能点击以进入下一阶段。

经过一段时间的听音乐,观察,还有头脑风暴。我发现,当动画结束(也可以说是没开始)时,按钮的class是play-button。但当动画播放时,这个class会被移除。取而代之的是pause-button

也就是说,我们会有这么几种可能:

  1. 动画播放完了的之前的题,有play-button
  2. 当前的题目,播放时没有play-button,结束时有play-button
  3. 后面的题目,还没有点击start按钮,因此pay-button还没出现

    那么,我们有已知最多能有多少个play-button,便可以用其来判断动画是否还在进行。

举个例子,假设我们现在做到第二道动画题,那么我们最多就应该有1+1=2个play-button。我们保存下这个数字,并每隔一段时间去用play-button class去定位一次元素。

如果只定位到1个元素,就说明动画还在播放。反之如果定位到2个,就说明动画已经结束。

这时候,根据上面提到的可能我们知道,play-button定位到的元素列表里,我们在做的这道题在列表的最后一位。由此我们可以点击去进行下一段动画。

代码里我为求谨慎,又去检查了一下元素里有没有Bounce class。这个class只在动画结束一部分的时候出现。这样就确认一定是该元素

至于判断何时动画结束,移步到下一道题,类似于上面的bounce class,结束的按钮都会有一个rotate-180的class。我们只要每次检查一下该元素有无这个class就行

def video(browser, action):
video_list = browser.find_elements(By.CLASS_NAME, "start-graphic")
for x, y in enumerate(video_list):
if not y.is_enabled() and not y.is_displayed():
continue
action.move_to_element(y).perform()
y.click()
time.sleep(1)
# judge whether this animation is finished or not
while True:
time.sleep(3)
play_button_img = browser.find_elements(By.CLASS_NAME, "play-button")
if len(play_button_img) <= x: # animation is still processing
continue
else:
play_button_img = play_button_img[x]
judge_end = play_button_img.get_attribute("class").find("rotate-180")
judge = play_button_img.get_attribute("class").find("bounce")
if judge_end != -1: # the animation has finished
break
if judge != -1: # step finished
play_button_img.click()

意料之外,情理之中的意外

就在我渐入佳境,不断写码不断运行不断测试的时候,之前的一个伏笔在悄然回收。

就在写完动画题进行最终测试时,我突然不能登录进网站了。不管怎么刷新页面,网页一直都停在登陆界面。我缓缓打出一个问号,不会被教授或平台发现,账号被封了吧??!!

颤抖地抽完根烟,仔细思索了要不要drop这门课后,上楼坐下做最后的抵抗。

仔细的检查了一下console log后才发现,进不去是因为status变成429了,即too many requests。好家伙,世界线收束了

记得我介绍登录部分的时候说过,每次运行一次脚本都需要重新登录一次平台。这样频繁的测试代码,频繁的登录操作,让服务器以为我在进行ddos攻击啥的,把我暂时ban掉了。

那么原因了解了,幸好不是因为自动化测试。但是!这个ban的时间是多久呢?我查了很多经验,说严重的会一直拒绝访问,原本放下的心又开始紧张起来...幸好,24小时后我又能正常登录了WOW

结尾


经过一夜肾上腺素飙升的coding和429的大起大落后,生活和内心又恢复平静。但是!脚本还差最终测试!不作死到底怎么对得起极客之名(自称)!最终,脚本在一个摸鱼的讲座上悄悄发布到了github上。把脚本分享给了几个一起上课的朋友。从此,周末晚上少了几个坐在电脑前机械地点击选择题选项的计算机结构学生

思考


很久以前我看过Ele实验室老师的一个经验分享视频,他说编程是一个很纯粹的工科,与其枯燥的读书,不如多写多实验,一步一步走出自己的路,这样才能学到更多东西。我深以为然。计算机只是一个工具(当然工具也能与你建立深厚的关系),更重要的是运用这些技能的思想。学计算机的人能在数码的世界里自由的发挥,这是莫大的自由!所以,如果有天,你的脑中突然浮现出一些想法,或许只是一些碎片,或许很天马行空,请不要让它一闪而过。你可能会因此错过一个精彩的世界


最后,这个脚本的github链接:https://github.com/Yujjio/Zybook_Auto_Completer

我的第一个自动刷作业脚本(大起大落的selenium经验分享)的更多相关文章

  1. Selenium执行测试脚本稳定性的一些经验分享交流

    Selenium执行测试脚本稳定性的一些经验分享交流 公司的自动化WEB测试框架IATA已上线运行了一段时间,期间发现一些脚本稳定性的问题,与大家分享一下. CASE执行游览器:ie firefox ...

  2. 用node写一个皖水公寓自动刷房源脚本

    因为住的地方离公司太远,每天上下班都要坐很久的班车,所以最近想搬到公司旁边的皖水公寓住.去问了一下公寓的客服,客服说房源现在没有了,只能等到别人退房,才能在网站上申请到. 如果纯靠手动F5刷新浏览器, ...

  3. 网课应该这么刷(油猴Tampermonkey脚本自动刷课)

    懒人福利 首先有些人不想学怎么用脚本,满足你们,压缩包解压之后直接登录即可.戳我下载 脚本已经集成好了,登录即可刷课.章节测试还会自动答题呦,正确率高达97%呦. 油猴及脚本安装 油猴的脚本不知可以刷 ...

  4. 自动备份并保存最近几天的SQL数据库作业脚本

    DECLARE @filename VARCHAR(255) DECLARE @date DATETIME SELECT @date=GETDATE() SELECT @filename = 'G:\ ...

  5. python网课自动刷课程序-------selenium+chromedriver

    python的强大之处就在于有许多已经写好的功能库提供,这些库强大且易用,对于写一些有特定功能的小程序十分方便. 现在就用pyhton的selenium+谷歌游览器写一个可以自动刷课的程序,以智慧树上 ...

  6. 【BZOJ-4590】自动刷题机 二分 + 判定

    4590: [Shoi2015]自动刷题机 Time Limit: 10 Sec  Memory Limit: 256 MBSubmit: 156  Solved: 63[Submit][Status ...

  7. BZOJ4590 自动刷题机

    Description 曾经发明了信号增幅仪的发明家SHTSC又公开了他的新发明:自动刷题机--一种可以自动AC题目的神秘装置.自动 刷题机刷题的方式非常简单:首先会瞬间得出题目的正确做法,然后开始写 ...

  8. 被「李笑来老师」拉黑之「JavaScript微博自动转发的脚本」

    故事的背景如下图,李笑来 老师于10月19日在 知乎Live 开设 一小时建立终生受用的阅读操作系统 的讲座,他老人家看到大家伙报名踊跃,便在微博上发起了一个 猜数量赢取iPhone7 的活动. 因为 ...

  9. 使用node自动刷房源并发送可入住房源到邮箱

    因为住的地方离公司太远,每天上下班都要坐很久的班车,所以最近想搬到公司旁边的皖水公寓住.去问了一下公寓的客服,客服说房源现在没有了,只能等到别人退房,才能在网站上申请到. 如果纯靠手动F5刷新浏览器, ...

  10. BZOJ_4590_[Shoi2015]自动刷题机_二分答案

    BZOJ_4590_[Shoi2015]自动刷题机_二分答案 Description 曾经发明了信号增幅仪的发明家SHTSC又公开了他的新发明:自动刷题机--一种可以自动AC题目的神秘装置.自动 刷题 ...

随机推荐

  1. nginx启停shell脚本

    #!/bin/bash # 编写 nginx 启动脚本 # 本脚本编写完成后,放置在/etc/init.d/目录下,就可以被 Linux 系统自动识别到该脚本 # 如果本脚本名为/etc/init.d ...

  2. numba jit加速python程序

    numba numba加速循环.numpy的一些运算,大概是将python和numpy的一些代码转化为机器代码,速度飞快! 加速耗时很长的循环时: from numba import jit # 在函 ...

  3. MVVM视图模型

  4. day03-2-拓展

    满汉楼03 5.拓展_多表查询 前面都是对单表进行操作 思考一个问题:如果多表查询怎么处理?例如,查看账单时,希望现实菜品名称 查询的结果从上图变为下图: 方案一 由多张表组合查询的的结果,我们仍然可 ...

  5. 三十一、kubernetes网络介绍

    Kubernetes 网络介绍 Service是Kubernetes的核心概念,通过创建Service,可以为一组具有相同功能的容器应用提供一个统一的入口地址,并且将请求负载分发到后端的各个容器应用上 ...

  6. docker容器化业务

    1.环境准备: 设备 IP地址 作用 系统版本 mysql-master 192.168.100.213 Nginx-Web服务器 Ubuntu2004 mysql-slave 192.168.100 ...

  7. CSS基础知识筑基

    01.CSS 简介 CSS 指层叠样式表 (Cascading Style Sheets),对HTML网页内容进行统一外观样式设计和管理,给网页进行各种装饰,让她变得美观,是HTML的化妆师.(Cas ...

  8. jvm双亲委派机制详解

    双亲委派机制 ​ 记录一下JVM的双亲委派机制学习记录. 类加载器种类 ​ 当我们运行某一个java类的main方法时,首先需要由java虚拟机的类加载器将我们要执行的main方法所在的class文件 ...

  9. dockerNginx代理本地目录

    dockerNginx代理本地目录 ssl_certificate cert/5900588_test.zk.limengkai.work.pem; ssl_certificate_key cert/ ...

  10. golang 简书

    https://www.jianshu.com/p/548adff0d10d Go 入门指南 https://github.com/wuxiaoxiaoshen/go-example-for-live ...