Windows 开机日志 → 月度考勤表
从 Windows 事件日志还原每日开关机时间,生成可交付的考勤表。
一、先诊断,再取数(必做)
不同机器的日志可用性差异极大,不要跳过诊断。执行:
$o = New-Object System.Collections.ArrayList
foreach($c in @('Microsoft-Windows-TerminalServices-LocalSessionManager/Operational',
'System','Security','Microsoft-Windows-Kernel-Boot/Operational',
'Microsoft-Windows-Diagnostics-Performance/Operational',
'Microsoft-Windows-Power-Troubleshooter/Operational',
'Microsoft-Windows-Winlogon/Operational')){
$r = & wevtutil qe $c /f:xml /rd:false /c:1 2>$null
$n = & wevtutil qe $c /f:xml /rd:true /c:1 2>$null
$s = ($r | Out-String); $sn = ($n | Out-String)
if($s -match '<Event '){
$a = [regex]::Match($s,"SystemTime='([^']*)'").Groups[1].Value
$b = [regex]::Match($sn,"SystemTime='([^']*)'").Groups[1].Value
[void]$o.Add(("{0,-55} OLDEST={1} NEWEST={2}" -f $c,$a,$b))
} else { [void]$o.Add(("{0,-55} NONE" -f $c)) }
}
Set-Content -Path "$env:TEMP\_evtdiag.txt" -Value $o -Encoding UTF8
同时查看日志配置(重点看 retention 和 maxSize):wevtutil gl System
常见故障:System 日志死锁
若 System 日志 NEWEST 停在很久以前,多半是 20MB 写满 + retention: true(禁止覆盖)→ 停止写入。
此时 6005/6006/12/13、Kernel-Power 全部不可用。
修复(需管理员,须先征得用户同意):
wevtutil sl System /ms:209715200
wevtutil sl System /rt:false
二、数据源优先级
> 核心原则:出勤必须看"登录",不能只看"开机"。
> 很多办公电脑设置了插电自启动(BIOS/电源设置),他人插电、市电恢复都会开机,
> 但没人登录。只按开机统计会把这些日子全算成出勤(且周末全变成"加班")。
| 用途 | 日志通道 | 事件 | 关键字段 | 可靠性 |
|---|---|---|---|---|
| 登录(主依据) | Microsoft-Windows-TerminalServices-LocalSessionManager/Operational |
ID 21 登录、ID 25 重连/解锁 | UserData 里 User、Address(本地 或客户端 IP) |
高,保留期长(实测 7 个月+) |
| 离岗(主依据) | 同上 | ID 23 注销、ID 24 断开/锁屏 | 同上 | 高,覆盖率远高于关机事件 |
| 开机(仅参考) | Microsoft-Windows-Kernel-Boot/Operational |
任意(每次冷启动写入固定一批) | TimeCreated,按 5 分钟聚类取簇首 |
高,但不用于判定出勤 |
| 关机(补充) | Microsoft-Windows-Diagnostics-Performance/Operational |
ID 200 / 203 | ShutdownStartTime |
中,仅关机慢时记录,约覆盖 60% |
| 开机(校验) | 同上 | ID 100 | BootStartTime |
中,仅开机慢时记录 |
| 登录(备选) | Security |
4624 / 4634 / 4800 / 4801 | LogonType(2=交互 7=解锁 10=远程) |
低,多数机器只保留 1 天 |
会话事件判定口径
到岗 = 当日最早一次 ID21 或 ID25
离岗 = 当日最晚一次 ID23 / ID24 / 关机事件ID200,三者取最大值
出勤 = 当日存在 ID21 或 ID25 (没有登录 -> 「未出勤」,哪怕开过机)
Address 字段可区分本地与远程会话:本地 = 人在机器前;IP = 网络会话(RDP / 向日葵 / 远程协助)。
远程会话照常计入出勤,但应在备注标出 IP,交给用户核对是否为本人操作。
> 注意:ID 24(断开)在远程会话网络波动时会频繁出现,与 ID 25 成对刷屏。
> 取"当日最晚"可规避大部分干扰,但还需配合下一条"在岗中"判定。
> 致命陷阱:Diagnostics-Performance 的 ID 101 是"应用程序启动性能"事件
> (含 SearchIndexer.exe 等字段),不是关机事件。误用会导致下班时间全变成早上 8 点。
> 关机是 ID 200(字段 ShutdownStartTime/ShutdownEndTime)。
三、wevtutil 取数:XPath 写法(极易踩坑)
# 单引号!多 ID 平铺,不要写 (A or B) 分组
$q = "*[System[(EventID=100 or EventID=200 or EventID=203) and TimeCreated[@SystemTime>='2026-07-31T16:00:00.000Z']]]"
& wevtutil qe Microsoft-Windows-Diagnostics-Performance/Operational "/q:$q" /f:xml
规则:
- 字符串常量只能用单引号,双引号报"语法错误"
- 不加分组括号:System[EventID=100 or EventID=200]
- 时间过滤:System[TimeCreated[@SystemTime>='...Z']]
- 按 Data 字段过滤:EventData[Data[@Name='LogonType']='2']
- /rd:true 最新优先,/rd:false 最旧优先
- 时间一律是 UTC,转北京时间需 +8
三·五、"在岗中"判定(易漏,必做)
若当日最后一次动作是登录/解锁(ID21/ID25),且晚于所有离岗事件,说明人还在用机器,
此时离岗时间必须留空、状态标「在岗中」。否则会把中途一次锁屏/远程断连误判成下班时间。
实测案例:某天序列 08:35 登录(远程IP) → 10:01 断开(远程IP) → 10:16 重连(本地),
人一直用到下午。若简单取"最晚离岗事件"会得出 10:01 下班,明显错误。
if off is None or (last_logon and last_logon > off):
off, still_in = None, True
四、其它高频陷阱
- wevtutil 输出跟随系统代码页(中文 Windows = GBK)。Python 里若固定
encoding='utf-8'
解码,本地这类中文会变乱码,导致"本地 vs 远程 IP"的判定失效。
正确做法:读 bytes 后依次尝试utf-8 / gbk / mbcs。 -
LSM 的
User/Address在<UserData>节点里(不是<EventData>的<Data Name=...>),
且节点带xmlns属性,提取正则要写成<(\w+)[^>]*>([^<]*)</\1>。 -
Get-WinEvent -MaxEvents N返回的是最旧的 N 条(与Get-EventLog相反),
容易误判"日志停在两年前"。要最新请| Sort-Object TimeCreated后取末尾,或用 wevtutil/rd:true。 - 大日志解析用
wevtutil输出 XML 再正则提取;Get-WinEvent+$e.ToXml()在几十万条时会超时。 - PowerShell 调用原生命令的 stdout 常抓不到 → 结果
Set-Content写文件再读取。 - bat 脚本里的中文会被 cmd 按 GBK 解析而乱码 → bat 内容保持纯 ASCII,
中文提示交给 Python 输出(配合chcp 65001)。cd /d在 Git Bash 下会被路径转换破坏,用pushd "%~dp0"。 - Windows 上 Git Bash 的
rm批量删除易被沙箱中断 → 改用 Pythonos.remove清理。
五、执行
脚本模板见 scripts/attendance.py(采集 → 聚类 → HTML + Excel)。
python attendance.py 2026-08 # 指定月份
python attendance.py # 默认上个月(当月未结束)
python attendance.py --no-open # 生成后不自动打开 Excel(定时任务/无人值守必加)
--no-open 也可通过环境变量 ATTENDANCE_NO_OPEN=1 控制。脚本生成完会 os.startfile 弹开 Excel,
不加这个开关的话定时任务跑起来会莫名其妙弹窗。
依赖 openpyxl。环境:
C:\Users\00008753\.workbuddy\binaries\python\envs\default\Scripts\python.exe
输出 HTML 用深色主题(IDE 为 dark 时),Excel 含"月度明细 + 汇总"两个 sheet。
六、结果解读(务必告知用户)
- 到岗 = 当日首次登录/解锁;离岗 = 当日最后一次注销/锁屏/关机。
- 「未出勤」= 开机了但全天没有登录:插电自启动、他人开机、市电恢复自启。
这是本方案相对"按开机统计"最关键的修正,通常能把虚高的周末加班天数砍掉一半。 - 离岗缺失 ≠ 早退:全程未锁屏且关机过快时 Windows 不写日志,应标注为"离岗未记录"。
- 只有关机没有开机的日子,通常前一天跨天未关机(取数窗口需前后各多留 1 天)。
- 备注里的「远程登录 IP」表示存在网络会话,照常计入出勤,但需用户自行核对是否本人。
- 该表是机器登录客观记录,不等于人事考勤,交付时须声明。
七、定时自动化(用户常要求"每月自动推给我")
用 automation_update 工具创建,不要手写 RRULE 给用户看。每月 1 号跑上月数据的配置:
scheduleType = recurring
rrule = FREQ=MONTHLY;BYMONTHDAY=1;BYHOUR=9;BYMINUTE=0
cwds = <脚本所在目录>
prompt 里必须写死这几件事(自动化运行时看不到当前对话):
- Python 绝对路径 + 脚本绝对路径 + --no-open
- "不带月份参数 = 自动统计上个月",所以每月 1 号跑正好
- 数据源口径与 ID 101 陷阱,避免后续执行时重蹈覆辙
- 要求用 present_files 展示 xlsx/html,并输出汇总(出勤/缺勤/迟到/早退/平均到岗离岗)