背景描述:
定时定点自动化预约指定日期的指定会议室,实现自动化会议室抢占预订功能。
技术栈:
python3 、 pyinstaller(exe打包工具)、 selenium、Html5等
实现思路:
公司OA办公系统使用SSO单点登录,所有相关子系统登录前需要先进行系统登录后, 才能跳转登录到需要的业务系统。
通过selenium+chromdriver实现用户行为模拟,定位相关界面元素后,输入指定人员信息后进行自动化相关操作(如点击登录按钮、点击提交按钮、选中复选框等)。
1、 分析界面DOM结构:
登录界面:
由于公司会做每日健康统计,当日首次登录后,会在登录环节中通过iframe页面嵌入来进行相关信息统计,需要在登录成功后做一次判断处理,否则无法进行后续的逻辑处理(程序中通过webdriver.Chrome().switch_to.frame() 进行iframe界面切换处理):
分析dom界面过程与登录界面相似,这里不做赘述、详细处理请见后续代码实现。
登录环节完成后,程序跳转会议室选择界面:
同样分析DOM获取界面的url访问链接,界面中部分dom元素有前后关联性,需要在自动填写界面信息时注意下。
提交申请单,完成申请。
项目结构:
核心代码结构:
会议预订处理代码(order_meeting.py文件)
import io
import json
import sys
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.support.select import Select
import selenium.webdriver.support.ui as ui
from selenium.webdriver import DesiredCapabilities
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf8')
cap = DesiredCapabilities().CHROME
cap["marionette"] = False
option = webdriver.ChromeOptions()
# option.add_argument('headless')
driver = webdriver.Chrome(executable_path="./chromedriver.exe", options=option)
# sso登录界面,请自行设置
LOGIN_URL = "https://sso.xxx.com/login?service=http%3A%2F%2Fmeeting.xxx.com.cn%2Fmeeting%2F"
def crawl_url(submit_meeting_url,
username, pwd,
date, begin_hour, begin_minus, end_hour, end_minus,
type, title, content,
meeting_space, meeting_room_option):
driver.get(LOGIN_URL)
wait = ui.WebDriverWait(driver, 10)
wait.until(lambda dr: dr.find_element_by_id('fm1').is_displayed())
driver.find_element_by_id("username").send_keys(username)
driver.find_element_by_id("password").send_keys(pwd)
driver.find_element_by_xpath(".//*[@type='submit']").submit()
# 处理每日健康报备界面
try:
driver.switch_to.frame('layui-layer-iframe1')
ui.WebDriverWait(driver, 2).until(lambda dr: dr.find_element_by_id('hei').is_displayed())
health_dom = driver.find_element_by_xpath('//span[@class="el-checkbox__input" and position()=1]')
health_dom.click()
continue_login_button = driver.find_element_by_xpath('//div[@class="mt-16 btnCheck"]/button')
continue_login_button.click()
except NoSuchElementException as e:
print('NoSuchElementException: ' + e.msg)
pass
except Exception as e:
print(e)
pass
# 提交会议室预约申请
driver.get(submit_meeting_url)
ui.WebDriverWait(driver, 30).until(lambda s: s.execute_script("return jQuery.active == 0"))
wait.until(lambda dr: dr.find_element_by_id('root').is_displayed()) # 登录成功
js = "var l = document.getElementById('layui-layer2'); l.style.display='None';" \
"var s = document.getElementById('layui-layer-shade2'); s.style.display='None';"
driver.execute_script(js)
ui.WebDriverWait(driver, 10).until(lambda dr: dr.find_element_by_id('meeting-info-form').is_displayed())
# 会议主题、会议内容、会议标题等内容设定
driver.find_element_by_name("type").send_keys(type)
driver.find_element_by_id("title").send_keys(title)
driver.find_element_by_id("content").send_keys(content)
# 会议日期设定
wait.until(lambda dr: dr.find_element_by_id('startDate').is_displayed())
driver.execute_script("document.getElementById('startDate').value='" + date + "'")
# 会议时间设定
meeting_time = driver.find_element_by_xpath('//span[@class="combodate" and position()=1]/select[position()=1]')
Select(meeting_time).select_by_visible_text(str(begin_hour))
meeting_time = driver.find_element_by_xpath('//span[@class="combodate" and position()=1]/select[position()=2]')
Select(meeting_time).select_by_visible_text(str(begin_minus))
meeting_time = driver.find_element_by_xpath('//span[@class="combodate" and position()=last()]/select[position()=1]')
Select(meeting_time).select_by_visible_text(str(end_hour))
meeting_time = driver.find_element_by_xpath('//span[@class="combodate" and position()=last()]/select[position()=2]')
Select(meeting_time).select_by_visible_text(str(end_minus))
# 会议地点选定
driver.find_element_by_xpath('//div[@class="radio-box" and position()=' + str(meeting_space) + ']').click()
# 会议地点会议室选定
ui.WebDriverWait(driver, 30).until(lambda s: s.execute_script("return jQuery.active == 0"))
meeting_room = driver.find_element_by_id("roomId")
Select(meeting_room).select_by_visible_text(meeting_room_option)
driver.execute_script("document.getElementById('submit-btn').click()")
ui.WebDriverWait(driver, 30).until(lambda s: s.execute_script("return jQuery.active == 0"))
driver.find_element_by_xpath('//a[@class="layui-layer-btn0"]').click()
ui.WebDriverWait(driver, 30).until(lambda s: s.execute_script("return jQuery.active == 0"))
driver.quit()
if __name__ == '__main__':
# 读取配置文件处理逻辑
with open("./config/app.json", encoding="utf-8") as json_file:
config = json.load(json_file)
crawl_url(config['meeting_url'],
config['username'], config['password'],
config['meeting_date'],
config['begin_hour'], config['begin_minus'], config['end_hour'], config['end_minus'],
config['meeting_type'], config['meeting_title'], config['meeting_content'],
config['meeting_space'], config['meeting_room_option'])
添加定时处理程序逻辑 schedule_runner.py,用于定点定时拉起程序处理:
#!/usr/bin/python
# -*- coding: utf-8 -*-
import json
import os
import datetime
import time
import psutil
# 运行exe文件
def run():
kill()
# os.chdir(r"C:\Users\Desktop\\")
os.chdir(r"./")
path = "会议预订.exe"
print("运行 会议预订.exe 进程")
os.system(path)
# 杀掉进程
def kill():
pids = psutil.pids()
for pid in pids:
p = psutil.Process(pid)
if p.name() == '会议预订.exe':
print("杀死 会议预订.exe 进程")
cmd = 'taskkill /F /IM 会议预订.exe'
os.system(cmd)
def main(begin_date='2020-12-15', begin_hour=23, begin_minus=58, begin_seconds=40, task_num=24, request_internal=15,
detect_internal=60):
count = task_num
while True:
now = datetime.datetime.now()
print(now)
# 2020-12-15 23:58分开始准备频繁执行任务
if now.date().strftime('%Y-%m-%d') == begin_date \
and now.hour == begin_hour and now.minute >= begin_minus and now.second >= begin_seconds:
while count >= 0:
run()
count -= 1
# request_internal 秒后重新发起请求
time.sleep(request_internal)
# 每隔 1min 检测一次
time.sleep(detect_internal)
if __name__ == '__main__':
with open("./config/schedule.json") as json_file:
config = json.load(json_file)
main(config['begin_date'],
config['begin_hour'],
config['begin_minus'],
config['begin_seconds'],
config['task_num'],
config['request_internal'],
config['detect_internal'])
通过pyinstaller打包python程序为exe可执行程序,方便使用。打包的配置文件分别为:
# -*- mode: python -*-
block_cipher = None
a = Analysis(['../order_meetting.py'],
pathex=['../Py-Job'],
hiddenimports=[],
hookspath=None,
runtime_hooks=None)
pyz = PYZ(a.pure)
exe = EXE(pyz,
a.scripts,
a.binaries+[],
a.zipfiles,
a.datas,
name='会议预订',
debug=True,
strip=None,
upx=True,
console=True)
# -*- mode: python -*-
block_cipher = None
a = Analysis(['../schedule_runner.py'],
pathex=['../Py-Job'],
hiddenimports=[],
hookspath=None,
runtime_hooks=None)
pyz = PYZ(a.pure)
exe = EXE(pyz,
a.scripts,
a.binaries+[],
a.zipfiles,
a.datas,
name='定时检测程序',
debug=True,
strip=None,
upx=True,
console=True)
打包命令: pyinstaller -F ./XXX.spec
最终效果:
app.json配置文件
schedule.json配置文件:
改进点:
1、会议选择以及配置文件可以做一个配置界面进行处理,方便用户使用;
2、核心预订功能可以做并发处理,提高命中抢到会议室的成功率。
分享以上,欢迎三连,感谢!