selenium 自动打卡

发布于 2022-01-18  60 次阅读


迫于学校对健康打卡的要求愈发严格,花半天简单写了个脚本,用于自动健康打卡,写完往服务器一扔,加个crontab任务,告别枯燥打卡。
本文记述下此过程中遇到的一些坑以及解决方法,以备查阅。
以下代码均使用Chrome作为webdriver。

关于抓包获取真实URL

日常打卡是通过企业微信中的链接跳转至打卡页面,并自动完成登录和身份信息填充。复制该页面链接并使用浏览器打开后,会提示“请在微信客户端打开链接”。 这里就要用到Fiddler来获取可在浏览器上打开的真实URL了。本次仅使用Fiddler中最基本的功能,故不多做介绍,有需要可以自行搜索

关于页面跳转

很多时候我们最终需要页面的URL,不好通过结构分析来找到规律,比较简单的思路是从主页模拟点击,一路跳转到最终需要的页面。此法虽然费时(相对而言),但个人使用足矣。

关于获取图片验证码

本校登录系统中使用了随机图片验证码,利用验证码URL保存的图片,与当前显示并不一致,比较明显的思路是截图获取。截图获取又可分为两类,一类是先给整个页面截图(driver.get_screenshot_as_file(savePath)),再通过图片元素的坐标、尺寸来进行截取(Pillow库);另一种是直接截取元素(element.screenshot(savePath))。在条件允许的情况下,显然后者更为便利。

关于验证码识别

获取到验证码图片后,还需要OCR识别。百度、腾讯等云服务提供商,基本都有自己的OCR服务,每月的免费额度供个人使用足矣。而Python本身也有一个第三方库——Ddddocr(带带弟弟OCR)。本校系统的验证码并不复杂,使用Ddddocr测试多次,几乎没有识别错误的情况,所以不再花时间去调用别厂的OCR服务,直接安装使用Ddddocr了。使用Ddddocr识别图片的参考代码如下:

import ddddocr

def verCodeReco(picPath):
    # verification code recognition
    ocr = ddddocr.DdddOcr()
    with open(picPath, 'rb') as f:
        imgBytes = f.read()
    return ocr.classification(imgBytes)

picPath为需要识别的图片路径,函数以字符串返回识别结果

关于地理位置

健康打卡中有一项是获取当前地理位置,可以使用以下代码来给浏览器设置地理位置:

driver.execute_cdp_cmd("Emulation.setGeolocationOverride", {
            "latitude": latitude,
            "longitude": longitude,
            "accuracy": 100
    })

其中latitude为纬度,longtitude为经度。

仅在无头模式下报错

大部分情况应该可以参考这个回答
但我遇到的情况可能更特殊一些。当仅在无头模式下报错后,我在代码中插入了截图语句,通过查看截图来确定问题所在。结果发现问题在获取定位权限上。
无头模式下默认是不给定位权限的,所以即使我们如上节所述设置了经纬度也没用。我在中文网络搜不到相关问题和解决方法,最后在国外论坛找到如下应对措施:

driver.execute_cdp_cmd('Browser.grantPermissions', {
    'origin': mainPage,
    'permissions': ['geolocation']
    })

其中mainPage是访问网站的域名。

关于无法点击

有些元素用element.click()方法点击不到,可以换一种方法:

from selenium.webdriver.common.keys import Keys

element.send_keys(Keys.SPACE)

关于动态加载的下拉列表

可以先点击框内其他元素,比如一般都有个正三角,点击后会变倒三角,且显示下拉选项,然后再使用Select,通过valueindex等来选择。

关于嵌套iframe

页面中有些元素是在嵌套iframe中的,无法直接定位,需要先定位到iframe元素,然后driver.switch_to_frame(iframeElement),之后正常定位元素即可。

弹出密码保存提示

禁用这一功能:

prefs = {"":""}
prefs["credentials_enable_service"] = False
prefs["profile.password_manager_enabled"] = False
options.add_experimental_option("prefs", prefs)

设定隐性等待

手动sleep()过于麻烦,大部分情况下设置全局的隐性等待即可,同时追求稳定和性能,需要单独设置元素等待。
全局隐形等待的设置:

driver.implicitly_wait(timeout)

其中timeout是超时时间,整个driver的持续时间内,所有操作都会等待页面全部加载完毕,直到超过timeout值。

处理复合class元素

有些元素仅有复合class属性,而xpath也不好用的时候,可以使用driver.find_element_by_css_selector("[class='class值']")来定位。因为driver.find_element_by_class_name的参数,是不支持复合class属性的。

代码

最后贴上俩自动打卡脚本,具体信息隐去。

健康打卡

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.select import Select
from selenium.webdriver.common.keys import Keys
from diyLib.verCodeReco import verCodeReco
from time import sleep
import time

# define options
options = Options()
options.add_argument('--headless')
options.add_argument("--window-size=1920,1080")
options.add_experimental_option('excludeSwitches', ['enable-logging'])
# options.add_experimental_option("detach", True) # 不自动关闭浏览器
mainPage = 'https://***'
verCodePath = '***'

timeout = 10
maxTry = 3
# 判断登录是否成功
def logSuc(driver):
        errMsg = driver.find_elements_by_class_name('auth_error')
        if len(errMsg) == 0:
                print('登录成功')
                return True
        else:
                print('登录失败')
                return False

def healthCheck(stuNum, stuPasswd, latitude, longitude, phone, temperature, resiType):
    # health check
    driver = webdriver.Chrome(options=options)
    driver.implicitly_wait(timeout)
    # set the location
    driver.execute_cdp_cmd("Emulation.setGeolocationOverride", {
            "latitude": latitude,
            "longitude": longitude,
            "accuracy": 100
    })

    # driver.execute_cdp_cmd("Page.setGeolocationOverride", {
        #     "latitude": latitude,
        #     "longitude": longitude,
        #     "accuracy": 100
    # })

    driver.execute_cdp_cmd('Browser.grantPermissions', {
    'origin': mainPage,
    'permissions': ['geolocation']
    })

    login = False
    tryNum = maxTry
    print('开始登陆')
    while (login == False and tryNum > 0):
        #go to the main page and login
        driver.get(mainPage)
        # name and password input
        driver.find_element_by_xpath('//*[(@id = "username")]').send_keys(stuNum)
        driver.find_element_by_xpath('//*[(@id = "password")]').send_keys(stuPasswd)
        # get the verification code img
        driver.find_element_by_xpath('//*[(@id = "captchaImg")]').screenshot(verCodePath)
        # recognize verification code and input
        verCode = verCodeReco(verCodePath)
        driver.find_element_by_xpath('//*[(@id = "captchaResponse")]').send_keys(verCode)
        driver.find_element_by_xpath('//*[contains(concat( " ", @class, " " ), concat( " ", "full_width", " " ))]').click()
        sleep(3)
        login = logSuc(driver)
        tryNum-=1

    if login == False:
        print('密码错误')
        return False
    sleep(1)

    uiNav = driver.find_element_by_id('ui_nav')
    navList = uiNav.find_elements_by_tag_name('a')
    navList[1].click()

    # now we are in the homepage of all work service
    servFav = driver.find_element_by_class_name('fuwutab')
    servList = servFav.find_elements_by_tag_name('li')
    servList[3].click()
    # now we are in the favorait service
    healthServ = driver.find_element_by_class_name('kuai').find_element_by_tag_name('a')
    healthUrl = healthServ.get_attribute('href')
    driver.get(healthUrl)

    # now we are in the check page
    formIframe = driver.find_element_by_id('pageFrame')
    driver.switch_to_frame(formIframe)
    driver.find_element_by_id('STUDENT_PHONE').send_keys(phone)
    driver.find_element_by_id('STZK_0').send_keys(Keys.SPACE)
    driver.find_element_by_id('TW').send_keys(temperature)
    # to select residence type, 1 for Shanghai, 3 for other
    driver.find_element_by_class_name('select2-selection__arrow').click()
    driver.find_element_by_class_name('select2-selection__arrow').click()
    selectResi = Select(driver.find_element_by_id('DQJZD'))
    selectResi.select_by_index(resiType)
    # select 未途径中高风险地区
    driver.find_element_by_id('SFTJGFXDQ_1').send_keys(Keys.SPACE)
    driver.find_element_by_id('BTN_SAVE').click()
    # screenshot for headless test
    # driver.get_screenshot_as_file('CrawlResult/screen.png')
    driver.quit()
    print(time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime()), 'task' , stuNum , 'done')
    sleep(1)
    return True

# ***打卡
healthCheck('123', '123', 11, 11, 111, 36, 3)

某站日常签到

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from time import sleep
import time
from retrying import retry

# define options
options = Options()
options.add_argument('--headless')
options.add_argument("--window-size=1920,1080")
options.add_experimental_option('excludeSwitches', ['enable-logging'])
# 取消自控提示
options.add_experimental_option('useAutomationExtension',False)
options.add_experimental_option("excludeSwitches",['enable-automation'])

# 防止弹出密码保存框
prefs = {"":""}
prefs["credentials_enable_service"] = False
prefs["profile.password_manager_enabled"] = False
options.add_experimental_option("prefs", prefs)

# options.add_experimental_option("detach", True) # 不自动关闭浏览器
mainPage = 'https://***'
timeout = 30

@retry(stop_max_attempt_number=5)
def isCheck(driver):
        # 检查是否已经签到
        checkBox = driver.find_elements_by_css_selector("[class='click-qiandao btn btn-qiandao']")
        if len(checkBox) == 0: return True
        else: return False

@retry(stop_max_attempt_number=5)
def bbsCheck(userName, passwd):
        # 签到程序
        driver = webdriver.Chrome(options=options)
        driver.implicitly_wait(timeout)
        driver.get(mainPage)
        driver.find_element_by_xpath('//*[contains(concat( " ", @class, " " ), concat( " ", "swal2-close", " " ))]').click()
        # 使用CSS选择器处理复合class的div元素
        driver.find_element_by_css_selector("[class='login-btn navbar-button']").click()
        driver.find_element_by_name('username').send_keys(userName)
        driver.find_element_by_name('password').send_keys(passwd)
        driver.find_element_by_css_selector("[class='go-login btn btn--primary btn--block']").click()
        sleep(10)
        burger = driver.find_element_by_class_name('burger')
        driver.execute_script("arguments[0].click();", burger)
        alCheck = isCheck(driver)
        # driver.get(mainPage)
        authorField = driver.find_element_by_class_name('author-fields')
        numList = authorField.find_elements_by_class_name('num')
        # 获取积分余额
        integral = float(numList[0].get_attribute('textContent'))
        if alCheck:
                print(time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime()), 'account:', userName, 'already check, remain:', integral)
                driver.quit()
                return True
        else:
                driver.find_element_by_css_selector("[class='click-qiandao btn btn-qiandao']").click()
                sleep(1)
                print(time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime()), 'account:', userName, 'check success, remain:', integral + 5)
                driver.quit()
                return True

bbsCheck(***, ***)

print(time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime()), 'task done')