我平时写自动化脚本时,经常会遇到一种需求:某个目录里一出现新文件,就马上处理;某个配置文件一变化,就让脚本自动重载;报表目录生成了新 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 原始缓冲区里拆路径和动作。

第二,它支持过滤。比如只处理 txtcsvxlsxdocx,忽略临时文件和 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
}

 

声明:站内资源为整理优化好的代码上传分享与学习研究,如果是开源代码基本都会标明出处,方便大家扩展学习路径。请不要恶意搬运,破坏站长辛苦整理维护的劳动成果。本站为爱好者分享站点,所有内容不作为商业行为。如若本站上传内容侵犯了原著者的合法权益,请联系我们进行删除下架。