“数文件”这三个字听起来像是入门级运维才会干的活但真正在服务器堆里打过滚的人都知道它最磨人的不是“数”而是“定期”和“汇总”这两个词。我这边维护着几台业务服务器上面有备份目录、文件交换目录、日志归档目录每天都要确认这些目录是否还在正常产生文件。刚开始我都是手动登录服务器find一下再用wc -l数个数三五台机器还凑合越往后越乱——今天忘看这台明天漏掉那个目录后来连着出了几次备份静默失败的事故我才下定决心写一套工具把“定期统计文件数量并推送至钉钉”这件事彻底自动化。现在这套工具已经稳定跑了半年多每天早高峰前把关键目录的文件总数、最近24小时新增文件数、空间占用情况整理成一份简报直接推进钉钉运维群谁都能一眼看出系统有没有异常。这篇文章不搞虚的把整套实现思路、核心代码、调度配置和踩过的坑从头到尾拆开讲清楚给同样有文件数量统计和钉钉推送需求的朋友一个可以直接照着抄的参考。1. 这个工具究竟在解决什么问题1.1 三个典型场景让我动了写工具的念头先说第一个场景备份目录。我这边每天凌晨会有一批增量备份和归档任务跑起来正常情况下备份目录里的文件数量是稳步上涨的某些按策略清理的任务则是“有增有减”。如果连续两天文件数一点变化都没有基本可以断定备份任务在某个环节静默失败了。这时候看磁盘空间往往很正常因为文件数量不变、体积也没涨但业务数据已经处于“裸奔”状态等真出问题再去查就晚了。第二个场景是文件交换目录。我们跟外部系统之间有条数据对接链路对方会定时往指定目录推送Excel、图片、合同文件我们再定时取走。这类目录如果某个时间段文件数突然暴涨十有八九是上游程序在重放数据或有人用脚本批量刷文件反过来如果一整天都没有新增文件说明对接链路可能已经断了。这两个信号都靠“文件数量”来捕捉而不是靠看目录大小——因为几百个空文件就能把数量撑起来但体积几乎没有变化。第三个场景更隐蔽应用日志目录。某些老业务系统会突然出现疯狂刷日志的bug一晚上生成几万个小日志文件把inode耗光。磁盘空间告警通常要滞后一两天但文件数量可能一夜之间就翻了十几倍。如果每天固定看一眼数量曲线这种异常在萌芽期就会被发现。这三个场景的共同点很明显文件数量的变化比磁盘空间变化更敏感比人工巡检更可靠。所以我要的不是一个“数数脚本”而是一个带固定调度、能产出结构化汇报、能推送到群的工具。1.2 为什么是“文件数量”而不是“磁盘空间”很多人第一反应是要看服务器异常直接监控磁盘空间不就够了但实际运维里“磁盘空间”是结果指标“文件数量”是过程指标。任何写文件、删文件、移文件的操作都会先体现在数量变化上空间占用只是这些操作的累计效果。用过程指标做异常检测天然比结果指标更早发现问题。举个真实例子有个备份任务因为挂载点异常退出了但磁盘空间没有任何明显变化空间监控一切正常。直到三天后有人手动查看备份目录才发现文件数量连续三天没动过。这就是只看空间的盲区。反过来日志刷屏的场景里文件数量一夜暴涨几千个但空间可能还有几十G富余磁盘告警不触发数量指标却早就拉红了。所以我在这套工具里把核心统计指标定为“文件数量”而不是简单执行一遍du -sh。数量变化能回答“系统还在不在正常干活”这个问题空间占用反而只是辅助参考。1.3 技术选型为什么是Python Cron 钉钉机器人这个方案组合不是拍脑袋定的我对比过好几套。先从脚本语言说起。用Shell写统计逻辑确实能跑但一旦要区分多个目录、按文件类型汇总、生成好看的文本报告shell的代码会迅速变成一堆难以维护的“管道串”。Python在这里的优势是标准库足够强大os.walk遍历目录、stat拿元数据、字符串格式化生成文本都很顺手中途要接异常处理也写得更清晰。调度方面我选了系统自带的cron没有引入Jenkins、Airflow这类重组件。原因很简单这个任务的依赖只有“一台服务器一个Python解释器”用cron在合适的时间拉起脚本就行不需要分布式调度、不需要任务编排。真要上了消息队列和调度平台反而是拿大炮打蚊子。推送渠道选钉钉自定义机器人是因为它实在太轻了。不需要申请正式的钉钉应用、不需要走OA审批流程在群设置里添加一个“自定义机器人”拿到一个webhook地址然后往这个地址POST一段JSON消息就出现在群里了。整个过程不到两分钟而且任何语言都能调——本质上就是一个HTTP接口。相比之下邮件推送可读性差时间长会被忽略企业微信机器人也不错但当时团队主要用钉钉没必要两套都接。2. 文件数量统计模块核心逻辑与实现细节2.1 统计维度一份简报里至少要包含哪些指标“统计文件数量”听起来就是数个数但真正到了日报场景一个数字撑不起一份汇报。我把统计维度拆成了下面几项每一项对应一个明确的业务含义指标含义用于观察什么文件总数目录下所有文件的数量目录是否“死掉”目录总数子目录数量项目结构是否失控最近24小时新增/变更数mtime在时间窗口内的文件数业务链路是否正常运转总大小所有文件占用空间空间和数量是否匹配扩展名分布按后缀聚合文件数是否有异常类型文件混入最大文件TopN体积最大的前N个文件异常大文件定位实际使用中最看重的是“最近24小时新增/变更数”和“文件总数”这两个。“文件总数”不变说明系统的写入链路停了“新增数量”异常暴涨说明可能有异常任务在刷目录。这里有个容易踩的认知偏差很多人以为“新增文件”就是文件的“创建时间”在今天。但Linux下获取一个文件的创建时间并不总是可靠而且很多文件是从其他服务器拷贝过来或由程序生成的真正对业务有意义的往往是这个文件“出现在这个目录里”的时间。所以我在实现中默认用mtime内容修改时间做时间窗口判断把“新增/变更”统称为“活跃文件”语义更准确实现也更简单。2.2 遍历目录的三道坎用os.walk遍历目录看起来简单实际统计时一定会遇到下面几个坑第一符号链接循环。如果一个目录里有软链接指向上级目录漫不经心的递归遍历会陷入死循环。os.walk的followlinks参数默认是False也就是不跟随软链接这个默认值一定要保留。不要为了“统计得更全”去把它改成True一旦目录结构里有环脚本会跑不完。第二没有权限的目录。有些系统目录或别的用户创建的目录当前用户根本没有读权限os.walk走到那里会抛PermissionError如果不处理整个统计可能在跑到一半时中断。我的做法是在取文件stat时把try/except包上跳过所有无法访问的目录和文件最后在报告里单独记一项“不可读文件数”这样至少要保证整轮统计能完整跑完。第三超大目录的性能问题。目录里有几十万、上百万个文件时os.walk会稍显吃力但也没到不可用的程度。我这边单目录最多十几万文件跑一次全量统计在一秒到几秒之间完全够用。只有文件量级到几百万以上才需要考虑用os.scandir手写广度优先遍历或者加多线程。优先保证正确性别一上来就优化性能。2.3 核心代码实现一个可复用的统计函数我最初写的是随意堆在一块的脚本后来重构成了一个干净的函数目录、时间窗口、忽略列表都做成参数方便在不同的服务器上复用。import os import time from collections import defaultdict def scan_directory(root_dir, ignore_dirsNone, recent_hours24): ignore_dirs ignore_dirs or [] total_files 0 total_dirs 0 total_size 0 recent_files 0 unreadable 0 ext_stats defaultdict(int) largest_files [] cutoff time.time() - recent_hours * 3600 for dirpath, dirnames, filenames in os.walk(root_dir, followlinksFalse): # 过滤掉需要忽略的子目录 dirnames[:] [ d for d in dirnames if os.path.join(dirpath, d) not in ignore_dirs ] total_dirs len(dirnames) for name in filenames: full_path os.path.join(dirpath, name) try: st os.stat(full_path) except OSError: unreadable 1 continue total_files 1 total_size st.st_size if st.st_mtime cutoff: recent_files 1 ext os.path.splitext(name)[1].lower() ext_stats[ext] 1 # 维护Top20最大文件列表 largest_files.append((st.st_size, full_path)) largest_files.sort(keylambda x: x[0], reverseTrue) if len(largest_files) 20: largest_files.pop() return { root: root_dir, total_files: total_files, total_dirs: total_dirs, total_size: total_size, recent_files: recent_files, unreadable: unreadable, ext_stats: dict(ext_stats), largest_files: largest_files, }有几个细节想说一下。dirnames[:] 这种写法是os.walk官方推荐的原地修改方式用来控制遍历时进入哪些子目录如果你写成dirnames new_listwalker还是按原来的列表往下走过滤根本不会生效。统计最大文件时我图省事直接在列表里插入再排序再截断Top20的数量级根本不需要引入堆结构简单反而可靠。os.stat外面那层try/except则是血的教训之前没有它一个没权限的目录直接让整个脚本崩溃。2.4 多目录分组统计让报告真正贴合业务单目录统计解决不了“一台服务器上多个业务目录”的问题。我把待统计的目录做成一个配置列表循环调用上面的函数再把结果汇总def build_report(scan_targets, recent_hours24): lines [] total_files 0 total_recent 0 for target in scan_targets: result scan_directory(target, recent_hoursrecent_hours) total_files result[total_files] total_recent result[recent_files] lines.append( f- {target}: 文件 {result[total_files]} 个, f近{recent_hours}h活跃 {result[recent_files]} 个, f大小 {result[total_size] / 1024 / 1024:.1f} MB ) summary f**服务器文件巡检报告**\n\n summary f检查目录数: {len(scan_targets)}\n summary f文件总数: {total_files}\n summary f近{recent_hours}h活跃文件: {total_recent}\n\n summary \n.join(lines) return summary实际部署时我还会把扫描结果存一份JSON到本地方便事后追溯推送到钉钉只是展示层本地落了盘的原始数据才是排查问题的依据。这个习惯帮我解决过好几次“今天数据对不上”的纠纷。3. 钉钉机器人推送把统计结果送进群3.1 创建自定义机器人的完整流程钉钉自定义机器人的创建路径是进入目标群聊 - 群设置 - 智能群助手 - 添加机器人 - 自定义。创建过程中会让填机器人名称选好后会生成一个webhook地址类似这样https://oapi.dingtalk.com/robot/send?access_token你的token如果创建时选择了“加签”安全设置还会额外得到一个secret密钥。这两段信息要分开保存webhook负责标识“推送到哪个群”secret负责签名认证。我把webhook和密钥放在独立的配置文件中和代码分开。原因是后续如果要换群、重置机器人只改配置文件就行不用去动代码逻辑。配置文件还可以根据部署环境区分开发、测试、生产避免误推。3.2 三种安全设置怎么选钉钉创建自定义机器人时提供三种安全设置自定义关键词、加签、IP白名单。可以同时启用多个。我把它们的区别整理成一张表安全方式原理优点缺点自定义关键词消息内容中必须包含指定关键词否则拒绝配置简单关键词可能被业务文本意外触发或绕过加签请求URL必须带上timestamp和签名安全性最高secret不泄露他人无法伪造实现时需要写签名逻辑IP白名单只允许特定公网IP调用简单粗暴服务器出口IP变化或走代理时会失效我的建议是至少开启“加签”如果生产环境出口IP固定可以再加一个“IP白名单”作为双保险。“自定义关键词”我一般不开因为推送内容本身是动态生成的统计文本硬塞一个固定关键词进去会破坏模板的可读性。3.3 加签算法的Python实现“加签”其实是目前最推荐的安全设置它的核心逻辑是把当前时间戳、换行符、密钥拼成一个字符串用HMAC-SHA256算法计算签名再做Base64编码和URL编码最终把时间戳和签名拼到webhook地址上发送。import time import hmac import hashlib import base64 import urllib.parse import requests def push_dingtalk(webhook, secret, content, msgtypetext): timestamp str(round(time.time() * 1000)) if secret: secret_enc secret.encode(utf-8) string_to_sign f{timestamp}\n{secret} string_to_enc string_to_sign.encode(utf-8) hmac_code hmac.new( secret_enc, string_to_enc, digestmodhashlib.sha256 ).digest() sign urllib.parse.quote_plus(base64.b64encode(hmac_code)) url f{webhook}timestamp{timestamp}sign{sign} else: url webhook payload { msgtype: msgtype, text: {content: content}, } resp requests.post(url, jsonpayload, timeout10) return resp.json()有几个细节必须提醒时间戳必须是毫秒级字符串不能用秒签名串的顺序是“时间戳 换行 密钥”不要把密钥写在前面quote_plus这层URL编码不能省否则签名里的特殊字符在URL传递时会被错误解析。我见过不少人在时间戳和URL编码这两点上翻车排查半天最后发现签名本身没错只是编码格式不对。3.4 text和markdown两种消息怎么选钉钉机器人的消息类型有好几种日常统计推送最常用的就是text和markdown两个。text类型适合极简提醒。比如“某目录文件数为0请检查”一句话说清楚就够不需要排版。markdown类型则适合刚才那种结构化报告可以让数字和结论部分更醒目。钉钉的markdown支持的语法有限表格表现很差折行和列表也会有奇奇怪怪的兼容问题。我踩过几次坑之后现在的模板以标题、加粗、无序列表为主坚决不用表格排版反而特别干净。一个典型的markdown推送模板长这样markdown_content ## 文件巡检报告 **检查时间**: 2025-01-15 09:00:00 **检查状态**: 全部正常 ### 目录摘要 - /data/backup: 文件 12840 个, 近24h活跃 320 个, 大小 1024.3 MB - /data/exchange: 文件 356 个, 近24h活跃 87 个, 大小 15.2 MB - /data/logs: 文件 2041 个, 近24h活跃 1560 个, 大小 802.7 MB ### 异常提醒 - 近24h无新增文件的目录: 无 - 最大文件: 20250114_backup.tar.gz (450.3 MB) --- _本消息由自动巡检工具发送_ 推送时把msgtype换成markdownpayload里的结构也改成{title: ..., text: markdown_content}。注意markdown类型的文本即使很大也不会被折叠所以不要塞得太长控制在三四十行以内阅读体验最好。3.5 推送频率和消息大小的边界钉钉自定义机器人有两个硬限制必须知道一是每个机器人每分钟最多发送20条消息超出会被限流二是单条消息内容大小有限制文本过长会被拒绝。实际使用中很少会触达20条的频率上限但如果你把脚本同时在多个服务器上部署且全部指向同一个机器人就要小心了——比如10台服务器同时推送一分钟内就可能打满20条后面的消息直接被丢弃。我的做法是设计一个“汇总推送模式”每台服务器先把统计结果上报到一台中央机由中央机统一汇总后发一条大消息。这样既避免限流也避免一群刷屏。如果只是个人用、三五台服务器那么各推各的也没问题把cron时间错开几分钟就行。至于消息大小我习惯把每条消息的正文控制在1KB左右既保险又清爽。4. 定期调度让工具真正“定期”跑起来4.1 Linux下crontab配置详解统计和推送的代码写完只完成了一半剩下的一半是让它在固定时间自动执行。Linux下最直接的方式就是crontab。我用Linux服务器做主部署配置如下0 9 * * * cd /opt/scripts /usr/bin/python3 file_stat_report.py /var/log/file_stat.log 21这个表达式的含义是每天9点整进入/opt/scripts目录用系统Python解释器运行file_stat_report.py脚本标准输出和标准错误都追加写到/var/log/file_stat.log。为什么要额外指定/usr/bin/python3而不是直接写python3因为cron执行时的环境变量和登录shell不一样PATH经常不包含Python安装路径裸写python3可能导致“命令找不到”。同理脚本内部如果有相对路径引用最好cd到脚本目录之后再执行保证工作目录正确。cron的五个时间字段从左到右分别是“分 时 日 月 周”0 9 * * *就是“每天9点0分”。如果要“每个小时跑一次”就是0 * * * *如果工作日早上8点和晚上20点各一次就是0 8,20 * * 1-5。这个语法没有太多要背的常用几个就够了但注意别把顺序记反我第一次随手写了9 0 * * *结果变成“上午0点9分”执行折腾了好久才发现。4.2 环境变量和日志文件的坑cron环境坑很大我单独把它拎出来讲。用crontab跑Python脚本时至少有三个环境层面的问题第一是PATH问题上面提到过。第二是虚拟环境问题。如果Python依赖要用venv管理cron里不能直接调用虚拟环境里的python要么在crontab里写全路径/opt/venv/bin/python要么在脚本第一行写清楚shebang并给执行权限。第三是日志文件权限问题。如果/var/log/file_stat.log由root创建cron任务以root跑没问题但如果你用普通用户的crontab写日志时可能权限不足脚本会在推送前静默失败。我的经验是把日志文件路径也放到脚本所在目录下用logs/子目录统一管理并添加一个简单的日志轮转逻辑超过10MB就把旧日志改名或直接清理。工具是跑长线的日志不轮转迟早会把磁盘吃满那就成了一个自己给自己添麻烦的“自动巡检工具”。4.3 Windows下的定时任务配置虽然主力部署是Linux但我也遇到过同事拿着Windows服务器想跑这套工具的情况。Windows下的方案是“计划任务”可以用带界面的“任务计划程序”创建也可以用命令行schtasksschtasks /create /tn FileStatReport /tr D:\scripts\file_stat_report.bat /sc daily /st 09:00这条命令创建一个名为FileStatReport的计划任务每天9点运行一个批处理脚本。Windows下路径分隔符、Python解释器路径、中文编码都可能成为问题我的建议是写一个file_stat_report.bat批处理文件在里面设置好Python路径并执行脚本然后让计划任务调这个bat而不是直接调Python这样遇到路径或环境问题只改bat就够了不需要反复修改计划任务设置。4.4 失败重试与告警闭环推送动作依赖网络网络抖动在所难免。我在推送函数中加了简单的重试逻辑第一次失败后隔5秒重试最多重试3次。如果三次都失败就把本次统计结果保存到本地一个pending/目录里第二天推送之前先检查有没有尚未送达的报告如果存在就顺带补推。这个设计让我安心很多。人工巡检最怕的不是工具坏了而是工具坏了之后自己还不知道。定时任务不产生消息可能只是网络抖动也可能是脚本报错了。所以我额外设置了一个“心跳”如果连续两天没有任何推送成功脚本会在第三天主动推送一条“最近可能存在问题”的提醒。虽然这个提醒仍然依赖同一个推送渠道但在大多数场景下已经足够暴露问题了。5. 常见问题与排查技巧实录5.1 加签一直提示“sign not match”这是我被问得最多的问题排查步骤基本固定。先检查系统时间是否准确加签依赖的timestamp是毫秒级当前时间如果服务器时间与真实时间偏差超过一分钟签名肯定校验不过这种情况执行一下date -u看一眼时间就知道了时间不对就同步。再检查代码中拼接的签名串格式必须是时间戳 换行符 密钥注意密钥是完整的SEC开头的字符串不能截断。最后检查URL编码base64结果里可能带、/不经过quote_plus传到钉钉那边就变成了空格和斜杠的转义问题签名自然不匹配。5.2 webhook能访问但推送没反应如果请求已经发出、返回值也是{errcode:0,errmsg:ok}但群里就是看不到消息优先怀疑机器人和群的状态。机器人在群设置里被移除或者群被解散、群主调整了机器人权限都会导致消息“发送成功”但实际不展示。另外如果同时启用了“自定义关键词”安全设置推送内容里不包含设定的关键词接口也报ok但消息会被静默拦下。这类问题排查时先看一眼钉钉开放平台的返回码再用最简单的一条text消息去试就能快速缩小范围。5.3 统计数字和别人手数的不一样这个现象出现过好几次。最典型的原因是统计口径不一致脚本默认统计最近N小时的“活跃文件”用的是文件的mtime内容修改时间而人工在文件管理器里看到的“创建时间”可能完全不同。尤其是一些程序生成的临时文件内容在创建后很快被覆盖mtime会往后跳但创建时间没变。还有一个隐蔽原因是目录里挂载了网络盘或绑定了其他文件系统os.walk可能会跨越挂载点把别的存储也统计进来肉眼看不出来。解决方法是把统计口径写进推送消息里——例如明确标注“近24h活跃文件 内容修改时间在24小时内的文件”让看的人都明白这个数字是怎么算出来的。5.4 推送限流与重复告警钉钉机器人每分钟20条的限制在汇总推送模式下很少触达但如果有多个脚本指向同一个机器人就容易出现部分消息被丢弃。我的排查方法是在出现“推送成功但群里只有一部分消息”的情况时先看脚本日志里有没有Flood相关的返回码再统计一下那一分钟内的总推送条数。如果确实超了就把各脚本的cron时间错开或者改成中央汇总推送。另外失败重试和补推逻辑一定要有“去重”概念同一份报告不要因为重试失败就反复入列否则网络恢复后会出现连续几条一模一样的消息刷屏。5.5 定时任务根本没执行cron任务没跑最常见的几个原因按出现频率排序cron服务没启动、脚本没有执行权限、crontab里命令路径写错、脚本本身抛异常但被重定向到某个没人看的日志。我最推荐的做法是给cron任务加一个硬标记脚本运行开始和结束时都向本地日志写入一条记录这样能看到任务是否被拉起、执行到哪一步。另一个容易被忽视的点是北京时间问题——服务器默认时区如果不是北京时间0 9 * * *执行的时间点会跟着系统时区走导致推送时间完全不对。最后分享一点我的个人习惯这套工具用了大半年最大的收获不是“每天有自动消息推送”这件事本身而是我把“数文件”从一项需要惦记的体力活变成了一条可追踪、可告警、可回溯的数据链路。现在新接一台服务器我第一件事就是配置巡检目录和钉钉webhook让它第二天早上自己“报到”。如果哪台机器没消息反而是最值得关注的信号。如果你现在还在手动数文件想用最低成本起步我建议不要一开始就追求“多目录汇总”“历史对比”先用最简单的方式跑通“一个目录统计一次推送”等它稳定了再慢慢往上加模块很多复杂功能其实都是靠坑堆出来的需求一次性全做完反而容易把自己劝退。