迫于学校对健康打卡的要求愈发严格,花半天简单写了个脚本,用于自动健康打卡,写完往服务器一扔,加个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
,通过value
或index
等来选择。
关于嵌套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')
Comments NOTHING