文章目录
前言
因为疫情缘故,学校搞了个每日限时打卡的系统,要求学生在每天0-9点完成当日体温和在京状况的打卡。就这样手动打卡了两个多月,北京还是迟迟不开学,目测开学已经要到5月底了。打卡期间忘过无数次,每次都被班长提醒,学校还往家长手机里发送作者没有打卡的短信,神烦。
于是乎,决定使用 定时开关机软件 + python 实现一个全自动定时打卡的脚本,省却我接下来一个月的劳神费力。
学校的打卡系统登录界面是这样的:
这里是填写界面(左侧菜单栏需要依次点击数据采集和学生每日上报才能出现表格,否则是空白页面。表格很长,注意有纵向和横向滚动条)
问题分析
手动操作时的步骤是这样的:
进入学生登录界面–>输入账号密码
–>进入填报界面–>点击左侧菜单栏–>点击数据采集–>点击学生每日上报
–>进入表格–>点击表格左下角“与昨日情况一致”
–>手动填写当日体温–>滑动横向滚动条–>手动填写当日在京情况(一直在京)
因此在使用selenium时,其大致步骤与上面描述的无二。
网页源码分析与代码实现
一、加载火狐驱动
selenium需要模拟打开浏览器,这里一般使用的浏览器驱动(Driver)是谷歌或者火狐,笔者先尝试了谷歌的驱动,发现在源码分析的时候效果不是很理想,于是用了火狐的。
(关于驱动的下载与安装,这里不做赘述,百度即可查看。)
驱动加载并进入后,脚本会自动根据当前的驱动打开对应的火狐浏览器,并链接到提供url的网页。
driver = webdriver.Firefox() # 利用火狐浏览器
# 填写疫情上报系统的url
url = r"http://tb.bucea.edu.cn:8075/WebReport/ReportServer?op\=fs_load&cmd=fs_signin&_=1586929099201"
driver.get(url)
# 最大化浏览器窗体
driver.maximize_window()
二、输入账号密码并提交
想要实现填写指定数据并自动填写到对应的位置,或者点击按钮提交到对应位置,就需要通过网页的各种标签的id
或者class
来定位相对应的元素,学过web编程的同学应该比较好理解这点。
例如下面这段用于填写用户名的代码
<div class="fs-login-input fs-login-input-username">
<input tabindex="1" class="fs-login-username" type="text" placeholder="用户名" title="用户名">
</div>
我们可以看到,这部分的核心在于<input>
标签,此标签没有指定id
,而是给了class
,因此可以使用selenium提供的driver.find_element_by_class_name()
方法,通过class
的值来定位元素,进而调用send_keys()
方法来实现数据填充。
类似的,对于“点击”操作,只需要获取<button>
或<radio>
或<a>
标签的class或者id,进而调用onclick()
方法达到点击目的。
关于如何定位元素,可以参考这篇文章《史上最全!Selenium元素定位的30种方式》
笔记本上使用Fn+F12可以启用源代码查看器,点击页面中某个部分,便能自动锁定相应代码,非常好用。我们的页面是下图这样的,因此可以通过这段代码来定位“输入用户名”这个元素并设置数据:
elem = driver.find_element_by_class_name('fs-login-username')
elem.send_keys(stu_number)
# 疫情打卡系统url
url = r"http://tb.bucea.edu.cn:8075/WebReport/ReportServer?op=fs_load&cmd=fs_signin&_=1586929099201"
driver.get(url)
driver.maximize_window()
# 填写用户名和密码
elem = driver.find_element_by_class_name('fs-login-username')
elem.send_keys(stu_number)
elem = driver.find_element_by_class_name('fs-login-password')
elem.send_keys(stu_password)
# 提交表单
driver.find_element_by_xpath("//*[@id='fs-login-btn']").click()
三、进入打卡界面并点击左侧菜单栏
点击提交且用户验证成功后,会跳转到另外一个url,这个界面用于填写当日身体情况。依次点击左侧菜单栏的数据采集->学生每日上报,可以进入表格界面,如图:
在第二节的分析中,笔者已经确定了定位元素的基本流程,按照selenium给定的方法,便可依次点开左侧菜单栏(这里每执行一步后,尽量使用
time.sleep()
让程序休息一小段时间,否则页面可能会卡住或者代码执行无效)。
但是! 随之而来的是困扰了我一天的地方。想要定位表格窗体的元素时,却怎么都定位不到,我试了好多地方,发现只有表格窗体中的元素是无法定位的。
1.iframe内元素的定位
学过web的同学知道,这种页面的header和菜单栏基本是固定的,通过内嵌<iframe>
或者<frame>
的方式,可以达到不同页面在相同url中切换、而指定部分(例如菜单栏 表头等)不变的目的。
于是我猜测元素定位不到可能与<iframe>
的引入有关,也就是说,菜单栏和表格窗体构成的页面并非一个整体,而是两个模块的拼接。遂检索相关文章,发现果然如此。
如果页面使用了<iframe>
,想要定位内嵌界面中的某个元素,在编写代码并调用selenium时,需要进入相应的<iframe>
内!
知道了这点,就很容易解决了:Fn+F12查看页面源代码,把鼠标放在滚动条上就能查看到内嵌页面的<iframe>
的id或者class,耐心网上翻一点,就能找到表格所在的iframe了:fs_tab_1587094181588
我兴冲冲的编写代码进入iframe并重启程序测试:
driver.switch_to.frame(driver.find_element_by_xpath("fs_tab_1587094181588"))
2.动态id/class的定位
what?找不到iframe?
不慌,盯着iframe的idfs_tab_1587094181588
仔细思考了一下,后面一长串的数字像是动态生成的,而这种id一般前面的头部是不变的,即fs_tab_
,于是我重新查看新页面的iframe的id,果然和上次打开的不一样了:fs_tab_1587094408952,表头也果然没变。
查看源码中以fs_tab_
开头的id,发现只有一个,妙哉,可以使用了。
于是,通过driver.find_element_by_xpath("//iframe[starts-with(@id, 'fs_tab_')]")
模糊匹配带有fs_tab_
开头的id,并使用driver.switch_to.frame()
进入相应的iframe,重新定位元素,成功了!
# 由于是动态的id和class,因此...
driver.switch_to.frame(driver.find_element_by_xpath("//iframe[starts-with(@id, 'fs_tab_')]"))
# 点击“与昨日情况一致”radio
driver.find_elements_by_class_name('fr-group-span')[0].click()
四、表格填写
经过上面这些分析后,下面的步骤就变得轻松多了。
由于每天填写的表格行数都是在动态变化的,比如今天的某个单元格id是D21-0-0,明天就变成了D-22-0-0,因此,对于获取当前日期应该填写的表格id,可以这样做:
根据日期差值与编写代码时的表格id相加,便得到了当日动态表格id
def getDateDiff():
# 2020-4-16编写,此时动态列数为21
retire_day = datetime(2020, 4, 16)
today = datetime.now()
left_days = (retire_day - today).days # 获取两个日期的天数差值
return abs(left_days)
curColID = str(getDateDiff() + 20) + r'-0-0'
inputCol = ['D', 'AB']
点击“与昨日情况一致”后,还需要手动填写两个表格,分别是当日体温36度和“一直在京”,不怕,写个循环就搞定了:
# 列ID形式为:列编号-0-0
curColID = str(getDateDiff() + 20) + r'-0-0'
inputCol = ['D', 'AB']
inputVal = ['36', '一直在京']
for i in range(len(inputCol)):
inputBox = driver.find_element_by_id(inputCol[i] + curColID)
driver.execute_script("arguments[0].scrollIntoView();", inputBox)
time.sleep(1)
# 开始模拟鼠标双击操作,不然无法锁定表格填数据
action_chains = ActionChains(driver)
action_chains.double_click(inputBox).perform()
time.sleep(1)
# 填写表格
driver.find_element_by_xpath("//input[contains(@class,'fr-texteditor')]").send_keys(inputVal[i])
time.sleep(1)
【注】
在第三版代码中,作者将通过日期差定位元素的方式改动为根据文本定位,因为系统开发者会定期删除几行表格,导致通过日期差定位的方式不准确。
获取表格ID,可以先通过文字定位元素,再使用driver.get_attribute(‘id’)获取所在单元格的id
五、提交表格
一行代码:
# css选择器选择button .x-emb-submit 这是关键
print(driver.find_element_by_css_selector('.x-emb-submit').click())
全部代码
奉上全部代码,有需要的同学可以参考
第三版(已稳定运行一个多月未中断)
# encoding=utf8
# from datetime import datetime
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
import time
stu_number = ['201706020115', '201706020218', '201706020108', '201706040143', '201706020239', '201706020221']
stu_password = ['10200014', '08258612', '0320001X', '1026283X', '0711181X', '02113537']
#stu_number = ['201706020221']
# 授权操作
def operationAuth(driver):
# 等待电脑亮屏
# time.sleep(10)
driver.implicitly_wait(30)
try:
# 疫情打卡系统url
url = r"http://tb.bucea.edu.cn:8075/WebReport/ReportServer?op=fs_load&cmd=fs_signin&_=1586929099201"
# url = r"http://tb.bucea.edu.cn/WebReport/ReportServer?op=fs"
for j in range(len(stu_number)):
driver.get(url)
driver.maximize_window()
# 填写用户名和密码
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CLASS_NAME, r'fs-login-username')))
driver.find_element_by_class_name('fs-login-username').send_keys(stu_number[j])
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CLASS_NAME, r'fs-login-password')))
driver.find_element_by_class_name('fs-login-password').send_keys(stu_password[j])
# 提交表单
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, r'fs-login-btn')))
driver.find_element_by_xpath("//*[@id='fs-login-btn']").click()
# 没有睡眠时间就不行switch_to.frame()
# 模拟点击进入填报表格
driver.find_element_by_link_text("数据采集").click()
driver.find_element_by_link_text("学生每日上报").click()
# 这地方写博客记录,由于是动态的id和class,因此...
# WebDriverWait(driver, 10).until(EC.frame_to_be_available_and_switch_to_it((By.XPATH,"//iframe[starts-with(@id, 'fs_tab_')]")))
driver.switch_to.frame(driver.find_element_by_xpath("//iframe[starts-with(@id, 'fs_tab_')]"))
# 点击“与昨日情况一致”radio
time.sleep(2)
driver.find_element_by_class_name('fr-group-span').click()
inputVal = ['36']
inputText = ['请填写体温度数']
for i in range(len(inputText)):
# 改动,根据文本定位,因为系统开发者会定期删除几行表格,导致日期差定位不准
inputBox = driver.find_element_by_xpath("//*[text()='请填写体温度数']")
# 填写"一直在京"的表格所对应的ID
remarkID = 'AB' + inputBox.get_attribute("id")[1:]
driver.execute_script("arguments[0].scrollIntoView();", inputBox)
# 开始模拟鼠标双击操作,不然无法锁定表格填数据
action_chains = ActionChains(driver)
action_chains.double_click(inputBox).perform()
# 填写体温度数
driver.find_element_by_xpath("//input[contains(@class,'fr-texteditor')]").send_keys(inputVal[i])
time.sleep(1)
if stu_number[j] == '201706020221':
remarkID = 'AE' + inputBox.get_attribute("id")[1:]
inputBox = driver.find_element_by_id(remarkID)
driver.execute_script("arguments[0].scrollIntoView();", inputBox)
action_chains = ActionChains(driver)
action_chains.double_click(inputBox).perform()
time.sleep(1)
driver.find_element_by_xpath("//input[contains(@class,'fr-texteditor')]").send_keys('不在北京')
time.sleep(1)
remarkID = 'H' + inputBox.get_attribute("id")[1:]
if stu_number[j] == '201706020221':
remarkID = 'H' + inputBox.get_attribute("id")[2:]
inputBox = driver.find_element_by_id(remarkID)
# 开始模拟鼠标双击操作,不然无法锁定表格填数据
time.sleep(1)
driver.execute_script("arguments[0].scrollIntoView();", inputBox)
#inputBox.click()
action_chains = ActionChains(driver)
action_chains.double_click(inputBox).perform()
# fr-trigger-btn-up
tp = driver.find_element_by_class_name("fr-trigger-btn-up")
tp.click()
# time.sleep(1)
inp_boxes = driver.find_elements_by_class_name("fr-combo-list-item")
inp_boxes[1].click()
# css选择器选择button .x-emb-submit 这是关键
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, '.x-emb-submit')))
print(driver.find_element_by_css_selector('.x-emb-submit').click())
time.sleep(1)
#driver.close()
except TimeoutException:
# 报错后就强制停止加载
# 这里是js控制
driver.execute_script('window.stop()')
#print(driver.page_source)
# driver.close()
# 方法主入口
if __name__ == '__main__':
# 加启动配置
driver = webdriver.Firefox() # 利用火狐浏览器
operationAuth(driver)
第二版:
(改动为根据文本定位,因为系统开发者会定期删除几行表格,导致通过日期差定位的方式不准确)
# encoding=utf8
from datetime import datetime
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.common.exceptions import TimeoutException
import time
stu_number = ['stu1_num', 'stu2_num']
stu_password = ['stu1_pwd', 'stu2_pwd']
# 授权操作
def operationAuth(driver):
time.sleep(5)
try:
# 疫情打卡系统url
url = r"http://tb.bucea.edu.cn:8075/WebReport/ReportServer?op=fs_load&cmd=fs_signin&_=1586929099201"
for j in range(len(stu_number)):
driver.get(url)
driver.maximize_window()
# 填写用户名和密码
elem = driver.find_element_by_class_name('fs-login-username')
elem.send_keys(stu_number[j])
elem = driver.find_element_by_class_name('fs-login-password')
elem.send_keys(stu_password[j])
# 提交表单
driver.find_element_by_xpath("//*[@id='fs-login-btn']").click()
# 没有睡眠时间就不行switch_to.frame()
time.sleep(2)
# 模拟点击进入填报表格
driver.find_element_by_link_text("数据采集").click()
driver.find_element_by_link_text("学生每日上报").click()
time.sleep(2)
# 这地方写博客记录,由于是动态的id和class,因此...
driver.switch_to.frame(driver.find_element_by_xpath("//iframe[starts-with(@id, 'fs_tab_')]"))
# 点击“与昨日情况一致”radio
driver.find_elements_by_class_name('fr-group-span')[0].click()
inputVal = ['36']
inputText = ['请填写体温度数']
for i in range(len(inputText)):
# 改动,根据文本定位,因为系统开发者会定期删除几行表格,导致日期差定位不准
inputBox = driver.find_element_by_xpath("//*[text()='请填写体温度数']")
# 下面是当前日期在京状态单元格所在的ID
# remarkID = 'AB' + inputBox.get_attribute("id")[1:]
time.sleep(1)
# 开始模拟鼠标双击操作,不然无法锁定表格填数据
action_chains = ActionChains(driver)
action_chains.double_click(inputBox).perform()
time.sleep(1)
# 填写体温度数
driver.find_element_by_xpath("//input[contains(@class,'fr-texteditor')]").send_keys(inputVal[i])
time.sleep(1)
# css选择器选择button .x-emb-submit 这是关键
print(driver.find_element_by_css_selector('.x-emb-submit').click())
time.sleep(1)
# driver.close()
except TimeoutException:
# 报错后就强制停止加载
# 这里是js控制
driver.execute_script('window.stop()')
print(driver.page_source)
# 方法主入口
if __name__ == '__main__':
# 加启动配置
driver = webdriver.Firefox() # 利用火狐浏览器
operationAuth(driver)
第一版
# encoding=utf8
import sys
import math
from datetime import datetime
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
import time
stu_number = ['stu1_number', 'stu2_number']
stu_password = ['stu1_pwd', 'stu2_pwd']
# 获得日期差值,由于表格数据是动态变化的,可根据编写日期的行号和日期差来确定当日行号
def getDateDiff():
# 2020-4-16编写,此时动态列数为21
retire_day = datetime(2020, 4, 16)
today = datetime.now()
left_days = (retire_day - today).days # 获取两个日期的天数差值
return abs(left_days)
# 授权操作
def operationAuth(driver):
# time.sleep(10)
try:
# 疫情打卡系统url
url = r"http://tb.bucea.edu.cn:8075/WebReport/ReportServer?op=fs_load&cmd=fs_signin&_=1586929099201"
for j in range(len(stu_number)):
driver.get(url)
driver.maximize_window()
# 填写用户名和密码
elem = driver.find_element_by_class_name('fs-login-username')
elem.send_keys(stu_number[j])
elem = driver.find_element_by_class_name('fs-login-password')
elem.send_keys(stu_password[j])
# 提交表单
driver.find_element_by_xpath("//*[@id='fs-login-btn']").click()
# 没有睡眠时间就不行switch_to.frame()
time.sleep(2)
# 模拟点击进入填报表格
driver.find_element_by_link_text("数据采集").click()
driver.find_element_by_link_text("学生每日上报").click()
time.sleep(2)
# 这地方写博客记录,由于是动态的id和class,因此...
driver.switch_to.frame(driver.find_element_by_xpath("//iframe[starts-with(@id, 'fs_tab_')]"))
# 点击“与昨日情况一致”radio
driver.find_elements_by_class_name('fr-group-span')[0].click()
# 列ID形式为:列编号-0-0
curColID = str(getDateDiff() + 20) + r'-0-0'
inputCol = ['D', 'AB']
inputVal = ['36', '一直在京']
for i in range(len(inputCol)):
inputBox = driver.find_element_by_id(inputCol[i] + curColID)
driver.execute_script("arguments[0].scrollIntoView();", inputBox)
time.sleep(1)
# 开始模拟鼠标双击操作,不然无法锁定表格填数据
action_chains = ActionChains(driver)
action_chains.double_click(inputBox).perform()
time.sleep(1)
# 填写体温度数
driver.find_element_by_xpath("//input[contains(@class,'fr-texteditor')]").send_keys(inputVal[i])
time.sleep(1)
# css选择器选择button .x-emb-submit 这是关键
print(driver.find_element_by_css_selector('.x-emb-submit').click())
time.sleep(1)
# driver.close()
except TimeoutException:
# 报错后就强制停止加载
# 这里是js控制
driver.execute_script('window.stop()')
print(driver.page_source)
# 方法主入口
if __name__ == '__main__':
# 加启动配置
driver = webdriver.Firefox() # 利用火狐浏览器
# driver.get("http://www.baidu.com") # 打开get到的网址
operationAuth(driver)