我的第一个自动刷作业脚本(大起大落的selenium经验分享)

起因


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

但是!!!

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

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

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

emoji

可行性分析


题目性质

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

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

工具

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

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

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平台的登陆界面
log in
可以看到,两个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的主页面,里面有你注册的课程main
检查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

选择题

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

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

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条件来防止误判

填空题

filling

对于填空题,我们看到有一个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)

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

video

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

经过一段时间的听音乐,观察,还有头脑风暴。我发现,当动画结束(也可以说是没开始)时,按钮的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()

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

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

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

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

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

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

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

结尾


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

思考


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


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

posted @ 2023-01-07 19:43  杏仁。君  阅读(816)  评论(1编辑  收藏  举报