最近一个亲戚请我有时间帮她刷一下某网站的学习积分,因为是单位硬性要求必须刷到一定分数,他自己又要上班没有时间,所以请我帮忙。我登陆到那个网站仔细研究了一下,分数主要通过学习和登陆以及做题来获得,学习必须满一定时间才能获得积分,每天能得到分数有最高限制,所以最少也要花 10 天左右的时间,每天刷一两个小时才能刷好,想起来正好最近在学 Python,于是想用 Python 来写个刷分脚本试试。
首先本来想用 Requests 库来解决,但是用 F12 开发者工具研究了一下网站后放弃了,网站有很多 Javascript 代码,一些按钮都是调用 Javascript 函数实现的,在我的认知里用 Http 库是没办法解决的。Google 了一下,发现用 Selenium 这个库可以很容易解决,代码写起来也十分简单。

Selenium 是一个 Web 程序自动化测试框架,它可以模拟真实浏览器的浏览行为,其底层使用 Javascript 实现,可以用于任何支持 Javascript 的浏览器上。

使用Selenium就相当与一个真实的人在操作浏览器一样,但是一切都是自动化的,只要运行脚本,浏览器就会自动运行,省去了人工测试的麻烦,同时像这种无聊的刷分任务也可以解决了。但是该网站的登陆需要验证码,所以还要使用 Tesseract 这个开源 OCR 引擎,Python 有对应的 pytesseract 库,试了一下其识别率并不高,尝试对验证码图像做了简单的二值化处理降低噪点,识别率有所提升,但是仍然很差,想要提升识别率的话可以结合机器学习来使用大量的验证码图像做为数据进行训练,但是为了一个小脚本来这么玩就有点夸张了。最后的解决办法就是识别失败的话就更换验证码再次识别,反正总有识别成功的时候(好暴力的解决办法...)。
最后直接上代码,因为不方便透漏是哪个网站,所以网站链接被我去掉了:

from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException, TimeoutException
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.remote.webelement import WebElement
from PIL import Image
import pytesseract
import time
import re
import random


def login(url):
    """登陆网站,获得当前积分"""

    global driver

    user_account = "your_account"
    user_password = "your_password"

    driver.get(url)
    driver.set_page_load_timeout(15)

    # 填写帐号密码
    driver.find_element_by_id('userAccount').send_keys(user_account)
    driver.find_element_by_id('userPassword').send_keys(user_password)
    time.sleep(3)

    # 识别验证码并填写
    write_checkcode()
    time.sleep(1)

    try:
        # 提交表单
        try:
            driver.find_element_by_class_name("btn_denglu").click()
        except TimeoutException:  # 页面加载超时则停止加载
            driver.execute_script('window.stop()')

        # 检测验证码是否识别正确,不正确则更换验证码再次登陆
        while isinstance(driver.find_element_by_id('popwinConfirm'), WebElement):
            print("验证码错误,更换验证码重新识别...")
            confirm_elem = driver.find_element_by_id('popwinConfirm')
            # 模拟鼠标点击
            mouse_click(confirm_elem)
            driver.find_element_by_link_text('换一换').click()
            write_checkcode()

            try:
                driver.find_element_by_class_name('btn_denglu').click()
            except TimeoutException:
                driver.execute_script('window.stop()')
    # 抛出异常则登陆成功
    except NoSuchElementException:
        ignore_elem = driver.find_element_by_link_text('暂不绑定')
        mouse_click(ignore_elem)
        score = str(driver.find_element_by_id('todaytpoint').text)
        score = score.split(':')[1].strip().lstrip()
        print("登陆成功,当前积分:" + score)
        driver.find_element_by_link_text('我的学习').click()


def study_courses():
    """开始学习课程"""

    global driver

    chapter_num, course_index = open_course_page()
    # 遍历所有章节
    for _ in range(chapter_num - 1):
        time.sleep(3)
        driver.find_element_by_id('nextCourse').click()

    time.sleep(10 * 60 + random.randint(0, 9))

    exit_course_study(course_index)


def exit_course_study(course_index):
    """退出课程页面,跳转到课程列表页面"""

    global driver

    # 学习结束点击退出学习
    study_time = driver.find_element_by_id('ware_time_num').text
    if int(study_time[3:5]) >= 10:
        driver.find_element_by_id('exitCourse').click()
        time.sleep(1)
        mouse_click(driver.find_element_by_id('popwinConfirm'))
    else:
        time.sleep(10)
        study_time = driver.find_element_by_id('ware_time_num').text
        driver.find_element_by_id('exitCourse').click()
        time.sleep(1)
        mouse_click(driver.find_element_by_id('popwinConfirm'))
    write_index_from_file(course_index)
    print("当前课程学习结束,总学习时间:" + study_time)
    windows = driver.window_handles
    driver.switch_to.window(windows[-1])


def open_course_page():
    """打开要学习的课程页面"""

    global driver

    # 获得当前需要学习的课程索引,跳到其页面
    course_index = read_index_from_file() + 1
    page = course_index // 8
    page_num = course_index % 8
    if page == 1:
        open_page_by_page_num(2)
    elif page == 2:
        open_page_by_page_num(3)

    # 通过xpath找到所有课程的</a>的标签
    courses_link_elem = driver.find_elements_by_xpath("//a[@target='_blank']")
    courses_link_elem = courses_link_elem[6::2]
    # 点击课程链接开始学习
    course_title = courses_link_elem[page_num].text
    print(course_title)
    try:
        courses_link_elem[page_num].click()
    except TimeoutException:
        driver.execute_script("window.stop()")
    time.sleep(1)
    # 切换到新打开的窗口
    windows = driver.window_handles
    driver.switch_to.window(windows[-1])
    time.sleep(1)
    driver.find_element_by_link_text('关闭提示').click()

    try:
        # 计算课程有多少个章节
        chapter_num = len(driver.find_elements_by_link_text('开始学习'))
        if chapter_num == 0:
            chapter_num = len(driver.find_elements_by_link_text('继续学习'))
            driver.find_element_by_link_text('继续学习').click()
        else:
            driver.find_element_by_link_text('开始学习').click()
    except TimeoutException:
        driver.execute_script("window.stop()")

    print("当前正在学习课程:" + course_title)
    return chapter_num, course_index


def open_page_by_page_num(page_num):
    """跳转到指定的课程列表页面"""

    driver.find_element_by_id('tz').clear()
    driver.find_element_by_id('tz').send_keys(str(page_num))
    driver.find_element_by_class_name('ok').click()
    print(f"跳转到第{page_num}页...")


def image_to_string(image_path):
    """验证码图片识别为字符串"""

    image = Image.open(image_path)
    x_y = round(image.width / image.height, 2)
    image = image.resize((int(200 * x_y), 200), Image.ANTIALIAS)

    # 处理二维码图像
    image = image.convert('L')
    # 这个是二值化阈值
    threshold = 150
    table = []
    for i in range(256):
        if i < threshold:
            table.append(0)
        else:
            table.append(1)
    # 通过表格转换成二进制图片,1的作用是白色,0就是黑色
    image = image.point(table, "1")

    # 识别图像取出特殊字符
    checkcode = pytesseract.image_to_string(image)
    checkcode = re.sub(r"[\s@#$&%*?']", '', checkcode)
    return checkcode


def mouse_click(elem):
    """模拟鼠标左键点击"""

    ac = ActionChains(driver)
    ac.click(elem).perform()
    time.sleep(1)


def write_checkcode():
    """将验证码保存到本地并识别写入网页输入框"""

    global driver
    check_code_elem = driver.find_element_by_id("checkcode")
    check_code_elem.screenshot(png_file_path)
    check_code = image_to_string(png_file_path)
    driver.find_element_by_id('usercheckcode').clear()
    driver.find_element_by_id('usercheckcode').send_keys(check_code)
    print("识别验证码:" + check_code)
    time.sleep(1)


def read_index_from_file():
    """从txt文件中读取上次学习的课程索引"""

    with open(index_file_path) as file:
        index = int(file.read())
    return index


def write_index_from_file(index):
    """写入本次学习的课程索引到txt文件"""

    if index == 20:
        index = -1  # 如果写入的是最后一节课索引,则重置索引为第一节课

    with open(index_file_path, 'w') as file:
        file.write(str(index))


if __name__ == '__main__':
    png_file_path = "/home/god/PycharmProjects/faxuanyun/images/checkcode.png"
    index_file_path = "/home/god/PycharmProjects/faxuanyun/doc/index.txt"
    webUrl = "website_url"

    driver = webdriver.Firefox()
    driver.maximize_window()
    login(webUrl)
    for i in range(2):
        time.sleep(3)
        study_courses()
    driver.close()

    driver = webdriver.Firefox()
    driver.maximize_window()
    login(webUrl)
    study_courses()
    print("今天的学习结束")
    driver.close()

脚本写好后,每天只要运行这个脚本,浏览器就会自动打开网站进行学习获得积分,每天的积分刷满后就自动退出,完全不用人为干预了,验证码识别如果运气好一次就能成功,运气差就要换十多次验证码才能识别正确,反正也是自动运行的,无所谓啦。