全自动上班打卡脚本思路分享|Windows + macOS 双系统实现方案

Python3.8

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

标签:Python、Selenium、AppleScript、自动化、Edge、Safari

一、写在前面

有没有想过,早上打开电脑时,它能自动为你打开打卡页面并点击“打卡”按钮

这篇文章,我将分享自己在 Windows 11 + Edge 浏览器 以及 macOS + Safari 上实现的完整方案。

不需要购买第三方自动化软件,全程只用原生脚本即可搞定!

二、需求背景

我有一个本地网页:上班打卡_单页html交互示例.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>上班打卡模拟</title>
  <style>
    :root{
      --bg:#0f172a;          /* slate-900 */
      --card:#111827cc;      /* slate-900 w/ alpha */
      --muted:#94a3b8;       /* slate-400 */
      --text:#e5e7eb;        /* slate-200 */
      --primary:#38bdf8;     /* sky-400 */
      --ok:#22c55e;          /* green-500 */
      --warn:#f59e0b;        /* amber-500 */
      --danger:#ef4444;      /* red-500 */
      --shadow:0 10px 30px rgba(0,0,0,.35);
    }
    html,body{height:100%}
    body{
      margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
      background: radial-gradient(1200px 600px at 50% -10%, #1e293b, var(--bg));
      color:var(--text); display:grid; place-items:center;
    }
    .wrap{ width:min(920px, 94vw); }
    .card{ background:linear-gradient(180deg,#0b1220, #0b1220cc); border:1px solid #1f2937; border-radius:20px; box-shadow:var(--shadow); padding:28px; }

    .header{ display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; }
    .title{ font-size:clamp(20px, 2vw + 12px, 28px); font-weight:700; letter-spacing:.3px; }

    .clock{ font-variant-numeric: tabular-nums; font-size: clamp(28px, 6vw, 48px); font-weight:800; }
    .tz { color:var(--muted); font-size:12px; margin-top:4px; }

    .main{ display:grid; grid-template-columns: 1fr auto; align-items:center; gap:24px; margin-top:20px; }
    @media (max-width: 720px){ .main{ grid-template-columns: 1fr; place-items:center; text-align:center; } }

    .status{
      display:flex; gap:12px; align-items:center; color:var(--muted);
    }
    .badge{ padding:4px 10px; border-radius:999px; font-size:12px; border:1px solid #334155; }

    .btn-area{ display:grid; place-items:center; }
    .punch{
      width:220px; height:220px; border-radius:9999px; border:none; cursor:pointer; outline:none;
      background: radial-gradient(120px 120px at 35% 30%, #7dd3fc, #0284c7);
      color:white; box-shadow: 0 20px 60px rgba(56,189,248,.35), inset 0 -20px 40px rgba(2,132,199,.45);
      display:grid; place-items:center; gap:8px; transition: transform .08s ease, filter .2s ease;
      position:relative; isolation:isolate;
    }
    .punch:before{
      content:""; position:absolute; inset:-18px; border-radius:inherit; z-index:-1;
      background: conic-gradient(from 0deg, rgba(56,189,248,.45), rgba(56,189,248,0) 40%, rgba(56,189,248,.45));
      filter: blur(18px); opacity:.65; animation: spin 6s linear infinite;
    }
    @keyframes spin{ to{ transform: rotate(360deg); } }
    .punch:hover{ filter:brightness(1.05); }
    .punch:active{ transform: translateY(2px) scale(.995); }
    .punch .big{ font-size:28px; font-weight:800; letter-spacing:1px; }
    .punch .small{ font-size:12px; opacity:.9 }
    .punch.disabled{ opacity:.6; cursor:not-allowed; filter:grayscale(.15); }

    .panel{ display:flex; gap:16px; flex-wrap:wrap; margin-top:18px; }
    .pill{ border:1px solid #334155; border-radius:14px; padding:8px 12px; color:var(--muted); font-size:13px; }

    .tools{ display:flex; gap:10px; flex-wrap:wrap; }
    .ghost{ background:transparent; color:var(--muted); border:1px solid #334155; border-radius:10px; padding:8px 12px; cursor:pointer; }
    .ghost:hover{ border-color:#475569; color:#cbd5e1 }

    .table-wrap{ margin-top:22px; border:1px solid #1f2937; border-radius:16px; overflow:auto; }
    table{ width:100%; border-collapse:collapse; min-width: 520px; }
    thead{ background:#0b1220cc; position:sticky; top:0; }
    th, td{ padding:12px 14px; border-bottom:1px solid #1f2937; font-size:14px; text-align:left; }
    tbody tr:hover{ background:#0b122044; }
    .tag{ padding:2px 8px; border-radius:999px; font-size:12px; border:1px solid #334155; }
    .tag.ok{ color:#bbf7d0; border-color:#14532d; background:#14532d66 }
    .tag.warn{ color:#fde68a; border-color:#78350f; background:#78350f66 }

    .toast{ position:fixed; inset:auto 0 28px 0; display:grid; place-items:center; pointer-events:none; }
    .toast > div{ background:#0b1220cc; border:1px solid #1f2937; color:var(--text); border-radius:12px; padding:10px 14px; box-shadow:var(--shadow); opacity:0; transform: translateY(10px); transition: all .25s ease; }
    .toast.show > div{ opacity:1; transform: translateY(0); }
  </style>
</head>
<body>
  <div class="wrap">
    <div class="card">
      <div class="header">
        <div>
          <div class="title">上班打卡 · 模拟页面</div>
          <div class="tz">时区:Asia/Taipei(可本地离线使用,记录保存在浏览器)</div>
        </div>
        <div>
          <div class="clock" id="clock">--:--:--</div>
        </div>
      </div>

      <div class="main">
        <div>
          <div class="status">
            <span class="badge" id="todayStatus">今天未打卡</span>
            <span class="badge" id="lastPunch">上次打卡:—</span>
          </div>
          <div class="panel">
            <span class="pill">上班时间:<strong id="shiftSpan">09:00</strong></span>
            <span class="pill">打卡规则:同一自然日仅允许一次“上班打卡”</span>
          </div>
          <div class="panel tools" style="margin-top:10px">
            <button class="ghost" id="changeShiftBtn" aria-label="设置上班时间">设置上班时间</button>
            <button class="ghost" id="exportBtn" aria-label="导出CSV">导出 CSV</button>
            <button class="ghost" id="resetBtn" aria-label="清空记录">清空记录</button>
          </div>
        </div>

        <div class="btn-area">
          <button id="punchBtn" class="punch" aria-label="打卡">
            <div class="big">打 卡</div>
            <div class="small">Click to Punch In</div>
          </button>
        </div>
      </div>

      <div class="table-wrap" aria-live="polite">
        <table>
          <thead>
            <tr>
              <th>#</th>
              <th>日期</th>
              <th>时间</th>
              <th>状态</th>
              <th>备注</th>
            </tr>
          </thead>
          <tbody id="logBody"></tbody>
        </table>
      </div>
    </div>
  </div>

  <div class="toast" id="toast" role="status" aria-live="polite"><div id="toastMsg">已打卡</div></div>

  <script>
    // ====== 基础配置 ======
    const TZ = 'Asia/Taipei';
    const LS_KEY = 'punchLogs_v1';
    const SHIFT_KEY = 'shiftTime_v1'; // e.g. '09:00'

    // 默认上班时间
    const DEFAULT_SHIFT = '09:00';

    // ====== 工具函数 ======
    const fmt = new Intl.DateTimeFormat('zh-CN', {
      timeZone: TZ, year:'numeric', month:'2-digit', day:'2-digit',
      hour:'2-digit', minute:'2-digit', second:'2-digit', hour12:false
    });

    function now(){ return new Date(); }

    function formatDate(d){
      const dt = new Date(d);
      const opt = { timeZone: TZ, year:'numeric', month:'2-digit', day:'2-digit' };
      const {year, month, day} = new Intl.DateTimeFormat('zh-CN', opt)
        .formatToParts(dt)
        .reduce((acc,p)=> (acc[p.type]=p.value, acc),{});
      return `${year}-${month}-${day}`;
    }

    function formatTime(d){
      const dt = new Date(d);
      const opt = { timeZone: TZ, hour:'2-digit', minute:'2-digit', second:'2-digit', hour12:false };
      const {hour, minute, second} = new Intl.DateTimeFormat('zh-CN', opt)
        .formatToParts(dt)
        .reduce((acc,p)=> (acc[p.type]=p.value, acc),{});
      return `${hour}:${minute}:${second}`;
    }

    function getShift(){ return localStorage.getItem(SHIFT_KEY) || DEFAULT_SHIFT; }
    function setShift(v){ localStorage.setItem(SHIFT_KEY, v); }

    function loadLogs(){
      try{ return JSON.parse(localStorage.getItem(LS_KEY) || '[]'); }
      catch{ return []; }
    }
    function saveLogs(arr){ localStorage.setItem(LS_KEY, JSON.stringify(arr)); }

    function isToday(str){ return str === formatDate(now()); }

    function compareHHMM(a,b){
      // 返回 a 与 b(均为"HH:MM")的时间差(分钟): a-b
      const [ah,am] = a.split(':').map(Number);
      const [bh,bm] = b.split(':').map(Number);
      return (ah*60+am) - (bh*60+bm);
    }

    function toast(msg){
      const box = document.getElementById('toast');
      document.getElementById('toastMsg').textContent = msg;
      box.classList.add('show');
      setTimeout(()=> box.classList.remove('show'), 1600);
    }

    // ====== 核心逻辑 ======
    const clockEl = document.getElementById('clock');
    const punchBtn = document.getElementById('punchBtn');
    const logBody = document.getElementById('logBody');
    const todayStatus = document.getElementById('todayStatus');
    const lastPunch = document.getElementById('lastPunch');
    const shiftSpan = document.getElementById('shiftSpan');

    function tick(){
      const d = now();
      // 仅显示时分秒(台北时区)
      clockEl.textContent = formatTime(d);
    }

    function refresh(){
      const logs = loadLogs();
      const today = formatDate(now());
      const shift = getShift();
      shiftSpan.textContent = shift;

      // 渲染表格
      logBody.innerHTML = '';
      logs.slice().reverse().forEach((row, idx)=>{
        const tr = document.createElement('tr');
        tr.innerHTML = `
          <td>${logs.length - idx}</td>
          <td>${row.date}</td>
          <td>${row.time}</td>
          <td><span class="tag ${row.status==='准时'?'ok':'warn'}">${row.status}</span></td>
          <td>${row.note || ''}</td>
        `;
        logBody.appendChild(tr);
      });

      // 状态与按钮
      const todayLog = logs.find(r=> r.date === today);
      if(todayLog){
        todayStatus.textContent = `今天已打卡(${todayLog.time} · ${todayLog.status}`;
        punchBtn.classList.add('disabled');
        punchBtn.disabled = true;
      }else{
        todayStatus.textContent = '今天未打卡';
        punchBtn.classList.remove('disabled');
        punchBtn.disabled = false;
      }

      const last = logs[logs.length-1];
      lastPunch.textContent = last ? `上次打卡:${last.date} ${last.time}` : '上次打卡:—';
    }

    function doPunch(){
      const d = now();
      const dateStr = formatDate(d);
      const timeStr = formatTime(d).slice(0,5); // HH:MM
      const shift = getShift();

      // 同日仅一次
      const logs = loadLogs();
      if(logs.some(r => r.date === dateStr)){
        toast('今天已打卡');
        return;
      }

      const delta = compareHHMM(timeStr, shift); // 分钟差
      const status = delta <= 0 ? '准时' : '迟到';
      const note = delta <= 0 ? `提前${Math.abs(delta)}分钟` : `晚到${delta}分钟`;
      const row = { date: dateStr, time: timeStr, status, note };

      logs.push(row);
      saveLogs(logs);
      toast('打卡成功');
      refresh();
    }

    // ====== 事件绑定 ======
    punchBtn.addEventListener('click', doPunch);

    document.getElementById('changeShiftBtn').addEventListener('click', ()=>{
      const current = getShift();
      const v = prompt('设置上班时间(24小时制 HH:MM)', current);
      if(!v) return;
      if(!/^([01]\\d|2[0-3]):[0-5]\\d$/.test(v)){
        alert('格式不正确,请输入形如 09:00 的时间。');
        return;
      }
      setShift(v);
      refresh();
      toast('上班时间已更新');
    });

    document.getElementById('exportBtn').addEventListener('click', ()=>{
      const rows = loadLogs();
      if(rows.length === 0){ toast('暂无记录'); return; }
      const header = ['序号','日期','时间','状态','备注'];
      const body = rows.map((r,i)=>[i+1, r.date, r.time, r.status, r.note||'']);
      const csv = [header, ...body].map(line => line.map(cell => `"${String(cell).replace(/"/g,'""')}"`).join(',')).join('\n');
      const blob = new Blob(["\uFEFF"+csv], {type:'text/csv;charset=utf-8;'});
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url; a.download = '打卡记录.csv'; a.click();
      URL.revokeObjectURL(url);
    });

    document.getElementById('resetBtn').addEventListener('click', ()=>{
      if(confirm('确定要清空所有打卡记录吗?')){
        localStorage.removeItem(LS_KEY);
        refresh();
        toast('记录已清空');
      }
    });

    // ====== 初始化 ======
    if(!localStorage.getItem(SHIFT_KEY)) setShift(DEFAULT_SHIFT);
    refresh();
    tick();
    setInterval(tick, 1000);
  </script>
</body>
</html>

其中包含一个 id=“punchBtn” 的按钮,用于模拟“上班打卡”。

本文的目标是让系统在每天开机时或定时自动打开这个模拟网页,并点击“打卡”按钮。

三、Windows 方案:Python + Selenium + Edge

在 Windows 11 上,我们使用 Python + Selenium 控制 Edge 浏览器实现自动化操作。

1️⃣ 安装依赖

pip install -U selenium
pip install webdriver-manager

说明:新版 Selenium 已可自动管理 EdgeDriver,如报错则加装 webdriver-manager。

2️⃣ 完整脚本 auto_punch_edge.py

import time
from pathlib import Path

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# ---------- 配置区域 ----------
HTML_PATH = Path(r"Z:\Desktop\上班打卡_单页html交互示例.html")  # 本地页面路径
TIMEOUT = 15
HEADLESS = False
CLICK_ID = "punchBtn"
# ----------------------------

def build_edge(headless: bool = False):
    from selenium.webdriver.edge.options import Options
    options = Options()
    if headless:
        options.add_argument("--headless=new")
    options.add_argument("--disable-gpu")
    options.add_argument("--window-size=1280,900")
    options.add_experimental_option("detach", True)  # ⭐关键:脚本结束后浏览器保持打开

    try:
        driver = webdriver.Edge(options=options)
        return driver
    except Exception:
        from selenium.webdriver.edge.service import Service
        from webdriver_manager.microsoft import EdgeChromiumDriverManager
        service = Service(EdgeChromiumDriverManager().install())
        driver = webdriver.Edge(service=service, options=options)
        return driver

def click_punch(driver):
    try:
        btn = WebDriverWait(driver, 3).until(
            EC.element_to_be_clickable((By.ID, CLICK_ID))
        )
        btn.click()
        return True
    except Exception:
        pass

    try:
        driver.execute_script("""
            (function(){
                var el = document.getElementById(arguments[0])
                    || Array.from(document.querySelectorAll('button'))
                    .find(b => (b.innerText||'').includes('打卡'));
                if (el) el.click();
            })();
        """, CLICK_ID)
        return True
    except Exception:
        return False

def main():
    if not HTML_PATH.exists():
        raise FileNotFoundError(f"找不到文件:{HTML_PATH}")

    url = HTML_PATH.resolve().as_uri()
    driver = build_edge(HEADLESS)
    try:
        driver.get(url)
        WebDriverWait(driver, TIMEOUT).until(
            lambda d: d.execute_script("return document.readyState") == "complete"
        )
        click_punch(driver)
        print("✅ 打卡完成,浏览器窗口保持打开。")
        input("按回车键退出脚本…")  # 阻止程序自动关闭
    except Exception as e:
        driver.save_screenshot("punch_fail_screenshot.png")
        print("❌ 出错:", e)
    finally:
        pass  # 不关闭浏览器

if __name__ == "__main__":
    main()

3️⃣ 使用方法

  1. 双击运行脚本,Edge 会自动打开网页并点击“打卡”按钮;
  2. 页面加载后不会被关闭;
  3. 可在任务计划程序中设置每天定时运行:
powershell -ExecutionPolicy Bypass -File "Z:\Desktop\auto_punch_edge.py"

四、macOS 方案:Automator + AppleScript + Safari

macOS 自带的 Automator + AppleScript 是天然的自动化组合。

可以轻松控制 Safari 打开网页并执行 JavaScript。

🔹 AppleScript 完整代码

on run {input, parameters}
    -- 1) 用 open -a Safari 打开本地文件(能稳妥处理中文/空格路径)
    set thePath to "/Users/jnlk/Desktop/上班打卡_单页html交互示例.html"
    do shell script "/usr/bin/open -a Safari " & quoted form of thePath

    -- 2) 等 Safari 打开并加载文档
    delay 1
    tell application "Safari"
        -- 尝试轮询 document.readyState,最多等 6 秒
        repeat 30 times
            try
                set readyState to do JavaScript "document.readyState" in document 1
                if readyState is "complete" then exit repeat
            end try
            delay 0.2
        end repeat

        -- 3) 点击“打卡”按钮(id=punchBtn)
        do JavaScript "document.getElementById('punchBtn')?.click();" in document 1
    end tell
    return input
end run

🔹 使用步骤

  1. 打开 Automator → 新建「应用程序」;
  2. 添加「运行 AppleScript」;
  3. 粘贴上面的代码;
  4. 保存为 自动打卡.app;
  5. 双击运行即可。

⚠️ 第一次运行前请到 Safari → 开发菜单中勾选:

允许来自 Apple 事件的 JavaScript

五、功能演示与结果

  • ✅ 页面自动打开
  • ✅ 自动点击按钮
  • ✅ 保留浏览器窗口
  • ✅ 可跨平台运行
  • ✅ 无第三方依赖(mac 端)

六、后记与扩展思路

这个项目虽然是个“上班打卡”的小工具,但其实是浏览器自动化与本地任务编排的完美入门案例。

你可以把它扩展成:

  • 自动登录网页系统;
  • 自动抓取日报/考勤数据;
  • 自动生成截图并推送微信机器人。

自动化的价值在于:重复动作交给机器,时间留给思考。

📦 资源与下载

  • 完整 Python 脚本:见上文代码块
  • macOS AppleScript 版本:见上文代码块
  • 兼容系统:Windows 10/11、macOS 13+

✍️ 作者寄语

我是一个热爱教育与编程的开发者。

写代码、做项目、自动化生活,是我的日常。

如果你也喜欢这种“用脚本改善工作流”的实践,欢迎留言、收藏、或关注我的 CSDN。

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值