我平时写自动化脚本时,经常会遇到一种需求:某个目录里一出现新文件,就马上处理;某个配置文件一变化,就让脚本自动重载;报表目录生成了新 Excel,就自动归档或上传。
这类需求如果一直用 Loop 扫目录,能做,但不够舒服。扫得太勤,浪费资源;扫得太慢,又错过及时处理的感觉。所以我整理了这个 WatchdogAHK,给 AHK v1 用的文件系统变化监听库。
它的底层思路不是轮询扫描,而是基于 Windows 的 ReadDirectoryChangesW。简单说,就是让系统告诉脚本“目录里发生了什么”,脚本再按事件去处理。
它适合解决什么问题
这个库适合做下载目录整理、投递目录处理、日志监控、配置热加载、截图目录 OCR、报表生成后自动归档这类工作。
初学者可以先把它理解成一个“目录监听器”:你指定一个文件夹,文件创建、删除、修改、重命名时,它会把事件对象交给你的回调函数。
最小上手示例
#Requires AutoHotkey v1.1
#Include <WatchdogAHK>
opts := {"Recursive": true, "DebounceMS": 500}
watch := WatchdogAHK_Watch("C:\Temp\WatchMe", "OnFileEvent", opts)
return
OnFileEvent(event) {
MsgBox, 64, WatchdogAHK, % event.type "`n" event.path
}
这段代码做的事情很直白:监听 C:\Temp\WatchMe,有变化就弹出事件类型和路径。实际项目里,把 MsgBox 换成移动文件、读取文本、压缩图片、上传报表就可以了。
我觉得好用的几个点
第一,它返回的是整理过的事件对象,不需要自己从 Windows 原始缓冲区里拆路径和动作。
第二,它支持过滤。比如只处理 txt、csv、xlsx、docx,忽略临时文件和 Office 的 ~$ 文件。
rules := {"IncludeExt": ["txt", "csv", "xlsx", "docx"]
, "ExcludeWildcard": ["~$*", "*.tmp", "*.part", "*.crdownload"]}
watch := WatchdogAHK_Watch(folder, "OnFileEvent", {"Recursive": true, "FilterRules": rules, "DebounceMS": 800})
第三,它有去抖和等待文件稳定的处理。很多新手会踩一个坑:文件刚出现时,其实还没写完,脚本马上去读就会失败。这个库里留了 WatchdogAHK_WaitFileReady(),适合处理大文件、下载文件、网络投递文件。
ready := WatchdogAHK_WaitFileReady(event.path, 15000, 200, 3)
if (ready.ready) {
; 文件已经稳定,可以开始处理
} else {
; 可以记录日志,稍后再处理
}
demo 里有什么
资源包里的 demo 做了一个小 GUI,可以选择监听目录、开启递归、只显示常见办公文件,并把创建、删除、修改、重命名事件显示在列表里。刚接触这类库的用户,建议先跑 demo,再看自己的业务该接在哪个事件上。

WatchdogAHK.ahk【文章底部有完整示例】
Gui监控demo代码片段展示:
#NoEnv
#SingleInstance Force
SetBatchLines -1
#Include <WatchdogAHK>
global gWatch := ""
global gLogLines := 0
global gWatchFolder := A_ScriptDir
global gGuiHwnd := 0
Gui, WatchdogDemo:New, +Resize +MinSize820x540 +HwndgGuiHwnd, WatchdogAHK 目录监控示例
Gui, WatchdogDemo:Margin, 12, 12
Gui, WatchdogDemo:Font, s9, Microsoft YaHei UI
Gui, WatchdogDemo:Add, Text, xm ym+2, 监控目录
Gui, WatchdogDemo:Add, Edit, x+8 yp-3 w560 vWatchFolder, %gWatchFolder%
Gui, WatchdogDemo:Add, Button, x+8 yp-1 w80 gChooseFolder, 选择
Gui, WatchdogDemo:Add, Checkbox, x+12 yp+2 vRecursive Checked, 递归
Gui, WatchdogDemo:Add, Checkbox, xm y+10 vOnlyOfficeDocs gFilterModeChanged, 只处理 txt/csv/xlsx/docx
Gui, WatchdogDemo:Add, Button, xm y+10 w80 gStartWatch, 开始
Gui, WatchdogDemo:Add, Button, x+8 yp w80 gPauseWatch, 暂停
Gui, WatchdogDemo:Add, Button, x+8 yp w80 gResumeWatch, 恢复
Gui, WatchdogDemo:Add, Button, x+8 yp w80 gStopWatch, 停止
Gui, WatchdogDemo:Add, Button, x+8 yp w80 gClearLog, 清空
Gui, WatchdogDemo:Add, Button, x+8 yp w110 gOpenFolder, 打开目录
Gui, WatchdogDemo:Add, Text, xm y+10 vStatusText w790, 状态:准备监听
Gui, WatchdogDemo:Add, ListView, xm y+6 w790 h390 vEventLV Grid, 时间|类型|文件名|扩展名|大小|所在目录|路径|旧路径|是否目录
Gui, WatchdogDemo:Show, w820 h540
WatchdogDemo_SetColumnWidths()
GoSub, StartWatch
return
ChooseFolder:
FileSelectFolder, picked, %gWatchFolder%, 3, 选择要监听的文件夹
if (picked != "") {
gWatchFolder := picked
GuiControl,, WatchFolder, %picked%
}
return
FilterModeChanged:
if (gWatch)
Gosub, StartWatch
return
StartWatch:
Gui, WatchdogDemo:Submit, NoHide
if (gWatch)
WatchdogAHK_Stop(gWatch)
rules := ""
if (OnlyOfficeDocs) {
rules := {"IncludeExt": ["txt", "csv", "xlsx", "docx"]
, "ExcludeWildcard": ["~$*", "*.tmp", "*.part", "*.crdownload"]}
}
opts := {"Recursive": !!Recursive, "Filter": 0x1 | 0x2 | 0x8 | 0x10, "BufferSize": 65536, "FilterRules": rules, "DebounceMS": 300}
gWatch := WatchdogAHK_Watch(WatchFolder, "WatchdogDemo_OnEvent", opts)
if (!gWatch) {
MsgBox, 16, WatchdogAHK, % "启动失败:`n" . WatchdogAHK_LastError()
return
}
modeText := OnlyOfficeDocs ? ",仅显示 txt/csv/xlsx/docx" : ""
GuiControl, WatchdogDemo:, StatusText, % "状态:正在监听 " . WatchFolder . modeText . ",请在这个目录中新建、删除或改名文件"
WatchdogDemo_Log({"timestamp": A_Now, "type": "info", "name": "", "ext": "", "size": "", "path": "开始监听:" . WatchFolder . modeText, "oldPath": "", "isDir": 0})
return
PauseWatch:
if (gWatch)
WatchdogAHK_Pause(gWatch)
GuiControl, WatchdogDemo:, StatusText, 状态:已暂停
WatchdogDemo_Log({"timestamp": A_Now, "type": "info", "path": "已暂停"})
return
ResumeWatch:
if (gWatch)
WatchdogAHK_Resume(gWatch)
GuiControl, WatchdogDemo:, StatusText, 状态:正在监听
WatchdogDemo_Log({"timestamp": A_Now, "type": "info", "path": "已恢复"})
return
StopWatch:
if (gWatch) {
WatchdogAHK_Stop(gWatch)
gWatch := ""
GuiControl, WatchdogDemo:, StatusText, 状态:已停止
WatchdogDemo_Log({"timestamp": A_Now, "type": "info", "path": "已停止"})
}
return
ClearLog:
Gui, WatchdogDemo:Default
Gui, WatchdogDemo:ListView, EventLV
LV_Delete()
gLogLines := 0
WatchdogDemo_SetColumnWidths()
return
OpenFolder:
Gui, WatchdogDemo:Submit, NoHide
Run, %WatchFolder%
return
WatchdogDemoGuiSize:
if (A_EventInfo = 1)
return
GuiControl, WatchdogDemo:Move, StatusText, % "w" (A_GuiWidth - 24)
GuiControl, WatchdogDemo:Move, EventLV, % "w" (A_GuiWidth - 24) " h" (A_GuiHeight - 165)
return
WatchdogDemoGuiClose:
if (gWatch)
WatchdogAHK_Stop(gWatch)
ExitApp
WatchdogDemo_OnEvent(event) {
if (!WatchdogDemo_ShouldShowEvent(event))
return
if (event.type = "error") {
WatchdogDemo_Log(event)
return
}
WatchdogDemo_Log(event)
}
WatchdogDemo_ShouldShowEvent(event) {
GuiControlGet, onlyDocs, WatchdogDemo:, OnlyOfficeDocs
OnlyOfficeDocs := onlyDocs
if (!OnlyOfficeDocs)
return true
if (!IsObject(event))
return true
ext := event.ext
if (ext = "")
return false
if (ext != "txt" && ext != "csv" && ext != "xlsx" && ext != "docx")
return false
name := event.name
if (name != "" && (SubStr(name, 1, 2) = "~$" || RegExMatch(name, "i)\.(tmp|part|crdownload)$")))
return false
return true
}
WatchdogDemo_Log(event) {
global gLogLines
Gui, WatchdogDemo:Default
Gui, WatchdogDemo:ListView, EventLV
if (!IsObject(event))
event := {"timestamp": A_Now, "type": "info", "path": event}
time := event.HasKey("timestamp") ? event.timestamp : A_Now
FormatTime, timeText, %time%, yyyy-MM-dd HH:mm:ss
typeText := WatchdogDemo_TranslateType(event.type)
LV_Add("", timeText, typeText, event.name, event.ext, event.size, event.dir, event.path, event.oldPath, event.isDir)
gLogLines += 1
LV_Modify(gLogLines, "Vis")
}
WatchdogDemo_SetColumnWidths() {
Gui, WatchdogDemo:Default
Gui, WatchdogDemo:ListView, EventLV
LV_ModifyCol(1, 140)
LV_ModifyCol(2, 50)
LV_ModifyCol(3, 120)
LV_ModifyCol(4, 56)
LV_ModifyCol(5, 70)
LV_ModifyCol(6, 150)
LV_ModifyCol(7, 170)
LV_ModifyCol(8, 170)
LV_ModifyCol(9, 60)
}
WatchdogDemo_TranslateType(type) {
type := type ""
if (type = "created")
return "创建"
if (type = "deleted")
return "删除"
if (type = "modified")
return "修改"
if (type = "moved")
return "重命名"
if (type = "error")
return "错误"
if (type = "info")
return "提示"
return type
}

评论(0)