
我们公司有个规定:会议室要通过OA系统预约,每天早上8点开放第二天的预约。
这意味着什么?意味着每天早上7点59分,全公司三百多人都在盯着OA系统的预约页面,手指悬在鼠标上。8点一到,疯狂刷新。
我手速不行。连续两周,我想约周三下午的大会议室,每次8点准时点进去,都显示“该时段已被预约”。
后来我学聪明了,7点55分就开始刷新。结果还是抢不到。因为有人写了脚本。
那我也写一个呗。
用 Playwright 模拟浏览器操作。先登录OA系统,等到8点整就自动提交预约请求。如果失败了就重试。成功或失败都发微信通知。
为什么用Playwright不用Selenium?Playwright更快,API也更顺手。加上异步支持好,定时触发更精确。
先装依赖:
pip install playwright requestsplaywright install chromium
主程序:
import timeimport jsonimport osfrom datetime import datetime, timedeltafrom playwright.sync_api import sync_playwrightimport requests# ============ 配置区 ============# OA系统地址OA_URL = "https://oa.yourcompany.com"OA_LOGIN_URL = f"{OA_URL}/login"OA_BOOKING_URL = f"{OA_URL}/meeting-room/booking"# 登录凭证(请妥善保管)USERNAME = "your_username"PASSWORD = "your_password"# 预约规则BOOKING_CONFIG = {# 要预约的会议室名称(按优先级排列,抢不到第一个就试下一个)"rooms": ["8楼大会议室", "8楼小会议室", "6楼中会议室"],# 预约日期:明天"date_offset": 1,# 时间段"time_slot": "14:00-16:00",# 会议主题"meeting_title": "周例会",# 参会人(如果需要填)"attendees": ["张三", "李四"],}# 抢预约的时间点(8:00:00 开放)BOOKING_HOUR = 8BOOKING_MINUTE = 0BOOKING_SECOND = 0# 失败重试次数MAX_RETRIES = 5# 重试间隔(秒)RETRY_INTERVAL = 1# PushPlus推送(通知结果)PUSHPLUS_TOKEN = "YOUR_PUSHPLUS_TOKEN"# 无头模式(True=不显示浏览器窗口,False=显示)HEADLESS = False# ============ 推送通知 ============def notify(title, content):"""推送预约结果到微信"""if not PUSHPLUS_TOKENorPUSHPLUS_TOKEN == "YOUR_PUSHPLUS_TOKEN":print(f"[通知] {title}: {content}")returnurl = "http://www.pushplus.plus/send"data = {"token": PUSHPLUS_TOKEN,"title": title,"content": content,"template": "txt" }try:resp = requests.post(url, json=data, timeout=10)result = resp.json()if result.get("code") == 200:print(f" ✓ 已推送到微信")except Exception as e:print(f" 推送失败: {e}")# ============ 核心逻辑 ============def login(page):"""登录OA系统"""print(" 正在登录...")page.goto(OA_LOGIN_URL)# 等待登录页面加载page.wait_for_selector("input[name='username']", timeout=10000)# 填写用户名密码page.fill("input[name='username']", USERNAME)page.fill("input[name='password']", PASSWORD)# 点击登录按钮page.click("button[type='submit']")# 等待登录完成(页面跳转或出现特定元素)page.wait_for_url("**/dashboard**", timeout=15000)print(" ✓ 登录成功")def book_room(page, room_name, booking_date, time_slot, title):"""预约指定会议室"""print(f" 尝试预约: {room_name} | {booking_date} {time_slot}")page.goto(OA_BOOKING_URL)page.wait_for_selector(".room-list", timeout=10000)# 选择日期page.fill("input[name='booking_date']", booking_date)# 选择时间段page.select_option("select[name='time_slot']", time_slot)# 点击"查询可用"按钮page.click("button#search-rooms")page.wait_for_selector(".available-rooms", timeout=10000)# 在可用列表中找到目标会议室room_elements = page.query_selector_all(".room-item")target_room = Nonefor elem in room_elements:name = elem.inner_text()if room_nameinname:target_room = elembreakif target_room is None:print(f" ✗ {room_name} 不可用或已被预约")return False# 检查是否显示"可预约"status = target_room.query_selector(".status")if status and "已约" in status.inner_text():print(f" ✗ {room_name} 已被预约")return False# 点击预约book_btn = target_room.query_selector("button.book-btn")if book_btn:book_btn.click()else:target_room.click()# 填写会议信息page.wait_for_selector("input[name='meeting_title']", timeout=5000)page.fill("input[name='meeting_title']", title)# 提交预约page.click("button#confirm-booking")# 等待结果page.wait_for_selector(".booking-result", timeout=10000)result = page.query_selector(".booking-result")if result and "成功" in result.inner_text():print(f" ✓ 预约成功: {room_name}")return Trueelse:error_msg = page.query_selector(".error-message")msg = error_msg.inner_text() if error_msgelse"未知错误"print(f" ✗ 预约失败: {msg}")returnFalsedef run_booking():"""执行抢会议室流程"""booking_date = (datetime.now() +timedelta(days=BOOKING_CONFIG["date_offset"])).strftime("%Y-%m-%d")print(f"\n{'=' * 40}")print(f" 会议室预约 - {booking_date} {BOOKING_CONFIG['time_slot']}")print(f"{'=' * 40}")with sync_playwright() as p:browser = p.chromium.launch(headless=HEADLESS)context = browser.new_context(viewport={"width": 1280, "height": 800},locale="zh-CN", )page = context.new_page()try:# 登录login(page)# 按优先级依次尝试预约success = Falsefor room_name in BOOKING_CONFIG["rooms"]:for attempt in range(1, MAX_RETRIES+1):print(f"\n 第 {attempt} 次尝试 - {room_name}")try:if book_room(page, room_name, booking_date, BOOKING_CONFIG["time_slot"], BOOKING_CONFIG["meeting_title"]):success = Truenotify("会议室预约成功",f"会议室: {room_name}\n"f"日期: {booking_date}\n"f"时间: {BOOKING_CONFIG['time_slot']}\n"f"主题: {BOOKING_CONFIG['meeting_title']}" )breakexcept Exception as e:print(f" ✗ 异常: {e}")if attempt<MAX_RETRIES:print(f" {RETRY_INTERVAL}秒后重试...")time.sleep(RETRY_INTERVAL)if success:breakif not success:notify("会议室预约失败",f"所有会议室均未能预约成功\n"f"日期: {booking_date}\n"f"时间: {BOOKING_CONFIG['time_slot']}\n"f"尝试过的会议室: {', '.join(BOOKING_CONFIG['rooms'])}" )except Exception as e:print(f"\n✗ 脚本异常: {e}")notify("预约脚本报错", str(e))finally:browser.close()# ============ 定时触发 ============def wait_until_booking_time():"""等待到预约开放时间"""now = datetime.now()target = now.replace(hour=BOOKING_HOUR,minute=BOOKING_MINUTE,second=BOOKING_SECOND,microsecond=0, )# 如果目标时间已过,设为明天if now>= target:target += timedelta(days=1)wait_seconds = (target-now).total_seconds()if wait_seconds>60:print(f"预约开放时间: {target.strftime('%Y-%m-%d %H:%M:%S')}")print(f"还需等待 {wait_seconds / 60:.1f} 分钟")# 提前5分钟提醒if wait_seconds>300:time.sleep(wait_seconds-300)print("\n距离开放还有5分钟,准备登录...")# 最后5分钟,精确等待while True:now = datetime.now()remaining = (target-now).total_seconds()if remaining<= 0:break# 最后1秒用更短的间隔time.sleep(0.1 if remaining<1 else 0.5)print(f"\n⏰ 时间到: {datetime.now().strftime('%H:%M:%S.%f')}")def main():print("="*40)print(" 会议室自动预约脚本")print("="*40)print(f" OA地址: {OA_URL}")print(f" 会议室: {', '.join(BOOKING_CONFIG['rooms'])}")print(f" 时间段: {BOOKING_CONFIG['time_slot']}")print(f" 最大重试: {MAX_RETRIES} 次")print("="*40)# 等待到开放时间wait_until_booking_time()# 执行预约run_booking()if __name__ == "__main__":main()
有个细节:OA系统的前端结构每个公司不一样。上面的选择器(".room-list"、".room-item" 这些)需要你自己改。怎么找到正确的选择器?
打开浏览器,进入预约页面,按F12打开开发者工具,用“检查元素”功能找到对应的HTML标签。把选择器换成你公司的就行。
如果你不想等到8点再跑脚本,也可以改成手动触发模式:
def main_manual():"""手动模式:直接执行,不等时间"""print("手动模式 - 立即执行预约")run_booking()
另外,有些公司的OA不是Web系统而是客户端软件。这种情况可以改用 pyautogui 来模拟键鼠操作:
import pyautoguiimport timedef book_with_gui():"""用GUI自动化预约(适用于客户端软件)"""# 打开OA客户端(假设快捷方式在桌面)pyautogui.hotkey("win", "r")time.sleep(0.5)pyautogui.typewrite("oa_client.exe", interval=0.05)pyautogui.press("enter")time.sleep(5)# 点击会议室预约菜单(坐标需要自己用 pyautogui.position() 获取)pyautogui.click(200, 150)time.sleep(1)# 后续操作类似...print("GUI自动化需要根据实际界面调整坐标")
提前启动脚本(比如7点50分),控制台输出:
======================================== 会议室自动预约脚本======================================== OA地址: https://oa.yourcompany.com 会议室: 8楼大会议室, 8楼小会议室, 6楼中会议室 时间段: 14:00-16:00 最大重试: 5 次========================================预约开放时间: 2024-03-15 08:00:00还需等待 10.0 分钟距离开放还有5分钟,准备登录...⏰ 时间到: 08:00:00.012345======================================== 会议室预约 - 2024-03-16 14:00-16:00======================================== 正在登录... ✓ 登录成功 第 1 次尝试 - 8楼大会议室 尝试预约: 8楼大会议室 | 2024-03-16 14:00-16:00 ✓ 预约成功: 8楼大会议室 ✓ 已推送到微信
微信收到推送:“会议室预约成功 - 8楼大会议室,2024-03-16,14:00-16:00”。
如果8楼大会议室被别人抢了,脚本会自动尝试下一个:
第 1 次尝试 - 8楼大会议室 ✗ 8楼大会议室 已被预约 1秒后重试... 第 2 次尝试 - 8楼大会议室 ✗ 8楼大会议室 已被预约 ... 第 1 次尝试 - 8楼小会议室 ✓ 预约成功: 8楼小会议室
这个脚本我用了一个多月。基本流程是:前一天晚上改一下配置(会议室和时间),第二天早上7点50分启动脚本,然后该干嘛干嘛。8点过几秒,微信就收到通知了。
以前抢会议室得7点59分坐在电脑前盯着。现在不用了。
注意事项:
选择器需要根据你公司的OA系统调整,这是最花时间的部分
密码别写在代码里提交到Git,用环境变量或单独的配置文件
别设太短的重试间隔,有些OA系统会封IP
建议用无头模式(HEADLESS=True),省得抢的时候弹个浏览器窗口出来吓人
https://ima.qq.com/wiki/?shareId=f2628818f0874da17b71ffa0e5e8408114e7dbad46f1745bbd1cc1365277631c
