距离上次练习已经过去了大半年,现在终于有时间接着进行一些其他方面的练习了。上次的弹幕练习属于为爱发电,这回更注重实用性,在练习的同时顺便解决一下生活中的实际问题,一举两得~
这次主要有两个内容,分别是学校的每日健康打卡以及外出报备的自动化处理,通过selenium和request库两种不同的方法实现,省去了每次都要手动填表的麻烦。如果有时间,还会涉及到UI以及exe打包相关内容。
话不多说,开始!
报备出校流程为:登录-填表-提交-得到结果,其中表格部分内容为自动生成,其余通过selenium配合xpath即可完成。
虽然也可以用requests实现,但相对要麻烦很多(登录就要麻烦不少,headers的选择,密码也经过MD5处理过。。。)
要使用selenium,必须配置好系统环境变量以及对应浏览器的webdriver,且版本要相匹配。
例如要使用Chrome浏览器则需要对应的driver,国内镜像地址为:下载地址
需要注意的是,如果电脑上有多个Chrome核心的浏览器,在使用以下代码时可能会出现调用失败的情况:
driver = webdriver.Chrome()
此时指定webdriver所在路径即可解决:
driver = webdriver.Chrome(r'X:\xxxxxxx\chromedriver.exe')
正常运行时,浏览器页面会一直出现,便于观察代码执行过程,这在测试阶段是十分必要的。当程序测试完毕后,就不再需要监测过程,得到结果便可。此时可以使浏览器在后台静默运行,不再出现浏览器窗口,代码如下:
option = webdriver.ChromeOptions() option.add_argument('headless') # 设置静默执行 option.add_argument("window-size=1920x1080") # 最大化,便于截图 driver = webdriver.Chrome(r'X:\xxxxxxx\chromedriver.exe', options=option)
当使用静默模式运行时,最大化窗口不能使用driver.maximize_window()
,而要用option.add_argument("window-size=Width x Height")
,如1920x1080。
至此,driver相关设置完成。
通过find_element_by_xpath()
可以很方便的实现元素的定位,只需找出对应元素的xpath即可进行操作。通过浏览器查看网页源码即可获取目标元素的xpath:
得到xpath如下:
uid_xpath = './/input[@name="username"]' pwd_xpath = './/input[@name="password"]' button1_xpath = './/span[@class="password_arrows"]' button2_xpath = './/a[@id="preview_start_button"]' phonenum_xpath = './/input[@name="fieldSQlxdh"]' reason_xpath = './/textarea[@name="fieldSQqjsy"]' schedule_xpath = './/textarea[@name="fieldSQxcap"]' button3_xpath = './/label[@for="V1_CTRL69"]' timestart_xpath = './/input[@name="fieldQJqjkssj"]' timeend_xpath = './/input[@name="fieldQJqjjssj"]' dayend_xpath = './/input[@name="fieldQJqjrqTo"]' button4_xpath = './/a[@class="command_button_content"]' button5_xpath = './/button[@class="dialog_button default fr"]'
以上为需要填入数据或选择类型以及提交按钮的xpath。
通过find_element_by_xpath()
定位元素后,即可进行相关操作。本次主要涉及点击,清空内容以及填写数据这三种操作,分别对应:
driver.find_element_by_xpath().click() driver.find_element_by_xpath().clear() driver.find_element_by_xpath().send_keys()
需要进行填写的数据有:账号,密码,手机号,出校原因,出校安排,开始时间,结束时间以及结束日期,离校类型为点选。
需要注意的是,若操作速度过快可能会因为元素未加载完成无法定位而导致失败,因此需要进行延时处理。
最简单的办法是通过time.sleep()
进行等待,每次执行语句前后强制等待,确保相应元素有足够的时间完成加载:
time.sleep(1) driver.find_element_by_xpath(uid_xpath).clear() #清空已存在内容 driver.find_element_by_xpath(uid_xpath).send_keys(username) #传入用户名 time.sleep(1) driver.find_element_by_xpath(pwd_xpath).clear() driver.find_element_by_xpath(pwd_xpath).send_keys(pwd) #传入密码 time.sleep(1) driver.find_element_by_xpath(button1_xpath).click() #登录 time.sleep(5) driver.find_element_by_xpath(button2_xpath).click() #开始办理 time.sleep(2) driver.find_element_by_xpath(phonenum_xpath).clear() #手机号 driver.find_element_by_xpath(phonenum_xpath).send_keys(phonenum) time.sleep(1) driver.find_element_by_xpath(reason_xpath).clear() #原因 driver.find_element_by_xpath(reason_xpath).send_keys(reason) time.sleep(1) driver.find_element_by_xpath(schedule_xpath).clear() #行程 driver.find_element_by_xpath(schedule_xpath).send_keys(schedule) time.sleep(1) driver.find_element_by_xpath(button3_xpath).click() #离校类型 time.sleep(1) driver.find_element_by_xpath(timestart_xpath).clear() #起始时间 driver.find_element_by_xpath(timestart_xpath).send_keys(timestart) time.sleep(1) driver.find_element_by_xpath(timeend_xpath).clear() #结束时间 driver.find_element_by_xpath(timeend_xpath).send_keys(timeend) time.sleep(1) driver.find_element_by_xpath(dayend_xpath).clear() #结束日期 driver.find_element_by_xpath(dayend_xpath).send_keys(dayend) time.sleep(1) driver.find_element_by_xpath(button4_xpath).click() #提交 time.sleep(1) driver.find_element_by_xpath(button5_xpath).click() #最终确定 time.sleep(1) driver.find_element_by_xpath(button6_xpath).click()
可以发现,这种方式缺点很明显:
selenium的显式等待可以准确判断等待时间,既能达到确定元素的目的,也能缩短延时时间,提高效率,但其用法较为复杂,还需要导入相关的库:
from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC
使用显式等待方式的同时,为精简代码,可以把不同元素及其对应的xpath存为dict,待填写数据存为list:
xpath_dict = { 'uid_xpath': './/input[@name="username"]', # 账号 'pwd_xpath': './/input[@name="password"]', # 密码 'button1_xpath': './/span[@class="password_arrows"]', # 登录按钮 'button2_xpath': './/a[@id="preview_start_button"]', # 开始办理 'phone_num_xpath': './/input[@name="fieldSQlxdh"]', # 电话号码 'reason_xpath': './/textarea[@name="fieldSQqjsy"]', # 出校事由 'schedule_xpath': './/textarea[@name="fieldSQxcap"]', # 出校日程 'button3_xpath': './/label[@for="V1_CTRL69"]', # 离校类型 'time_start_xpath': './/input[@name="fieldQJqjkssj"]', # 开始时间 'time_end_xpath': './/input[@name="fieldQJqjjssj"]', # 结束时间 'day_end_xpath': './/input[@name="fieldQJqjrqTo"]', # 结束日期 'button4_xpath': './/a[@class="command_button_content"]', # 提交 'button5_xpath': './/button[@class="dialog_button default fr"]' # 最终确定 } data_list = [ username, pwd, phone_num, reason, schedule, time_start, time_end, day_end ]
此处利用for循环即可自动判断操作类型为点击或填写数据,并按顺序自动完成。
count = 0 for i in xpath_dict: if 'button' in i: # 判断操作类型 try: WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, xpath_dict[i])) ) finally: driver.find_element_by_xpath(xpath_dict[i]).click() else: try: WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, xpath_dict[i])) ) finally: driver.find_element_by_xpath(xpath_dict[i]).clear() driver.find_element_by_xpath(xpath_dict[i]).send_keys(data_list[count]) count += 1
填报完成后,可根据需要通过截图(driver.get_screenshot_as_file()
)等方式留存并发送报备结果,或继续填加自动运行等功能。
施工中。。。
这个项目利用requests库来进行处理,之前的练习主要使用了get()
,本次则主要涉及post()
,具体流程与上文相同,区别在于不使用selenium模拟操作,直接进行数据的获取与提交。
还有一个不使用selenium的原因是,打卡需要获取定位,然而pc端总是无法定位,只能换用requests的方法。
系统使用的是腾讯的api接口,通过抓包发现确实可以获取正确定位,但是系统却无法读取,原因未知。
位置都出来了,为什么不用呢。。。
通过多次抓包试验,最终确定整个打卡过程共存在三次post行为,分别为登录,请求日期以及提交,对应三个不同的url。其中登录和提交带有数据,请求日期为单纯的post
和get的用法差不多,以登陆系统为例,本次需要注意的有请求的headers以及data:
json.dumps()
将python对象转换为json格式。(顺便一提,这系统登录时提交的密码是明文的,都不处理下吗。。。)登录时提交账号密码等数据即可:
headers = { 'User-Agent': 'xxx', 'Content-Type': 'application/json' } data_origin = {user_account, user_password} login_url = 'xxxxxxxx' login_res = requests.post(login_url, headers=headers, data=json.dumps(data_origin)) login_res_json = login_res.json()
通过返回的response可以判断登录是否成功,200为成功,其他则为不同情况的错误:
若不传入cookie,则会返回:{'code': 405, 'msg': '凭据已经过期,请重新认证', 'datas': ''}
。这会导致请求日期和提交无法进行,因此需要在登陆后获取cookie并在接下来的post中传入。(尝试过单独使用session()
而不传入cookie,无效,原因未知。)
cookies = login_res.cookies # 获取登陆后cookie cookie = requests.utils.dict_from_cookiejar(cookies) # 转换为可传入参数格式
可以看出,相关数据都储存在datas的hunch_list中,其中date1为打卡当天日期,state为打卡状态,1为已打卡,0为未打卡。
对日期进行请求:
date_res = requests.post(date_url, headers=headers, cookies=cookie) date_res_json = date_res.json()
将date1提取出来,提交时要用到:
today = date_res_json['datas']['hunch_list'][0]['date1']
接下来根据state的值判断是否继续打卡:
statue = 0 # 是否继续执行,0为继续 if date_res_json['datas']['hunch_list'][0]['state'] == 1: statue += 1 print('今日已打卡!') else: print('今日未打卡,将自动打卡。。。')
提交时,数据主要包括两部分:date和punch_form。其中date的值就是之前提取过的date1,punch_form为需要进行填报的内容,包括所在地,联系方式,体温等。最后将这两部分数据打包提交即可:
if statue == 0: res_submit = requests.post(submit_url, headers=headers, data=json.dumps(data_punch), cookies=cookie) res_submit_json = res_submit.json() if res_submit_json['code'] == 200: print('打卡成功!') else: print('打卡失败!') print('错误!:%s' % (res_submit_json['msg']))
施工中。。。