批量视频按创建时间区间筛选:Python脚本生成Excel清理标注全流程 项目编号857是最近在处理一批视频素材库时记下的任务代号。任务听上去很朴素把创建时间落在指定区间的视频文件批量筛出来提取每个文件的创建时间在Excel里生成一份清单并把需要删除的文件逐一标注清楚。真正动手之后你会发现这一步没有想象中那么简单——资源管理器只能手动排序Excel不会替你去判断哪些文件该删几千个视频堆在文件夹里文件名又是“录屏_20240101_153520_001”这种毫无规律的东西清理起来特别折磨人。这篇内容就完整拆解这个任务从需求识别、方案选择、脚本实现到Excel标注、人工复核、安全删除的完整链路。无论你是做内容运营需要清理历史素材还是做监控回放整理、直播录屏归档只要有“批量视频 按时间筛选 留档清理”的需求都可以直接参考这套流程。1. 先把需求说清楚标题里的“修改”到底改什么1.1 这类需求通常出现在什么场景我接到这个任务时第一反应不是写代码而是先搞明白对方到底要什么。项目背景挺典型一个素材库盘里堆了大几千个视频有直播录屏、监控回放、拍摄废片、缓存切片占了两百多G空间。运维同事只给了一句话“把3月到5月创建的视频清一批空间不够了。”问题在于没人敢直接删。文件名完全看不出内容时间排序在资源管理器里只能一页一页翻真要精确到“某天某点到某天某点之间创建的文件”手动操作基本不可行而且删错了没人能负责。这种场景在内容运营、素材管理、监控存档这类岗位上很常见视频数量大、命名乱、时间跨度长但清理动作必须有据可查。1.2 “修改”的对象是筛选状态不是文件时间戳项目标题里“创建时间在指定区间修改”这句话第一次看到的人很容易理解成“批量修改文件的创建时间到某个区间”。我一开始也差点跑偏后来和需求方确认才发现大家真正要的是另一件事把创建时间落在指定区间内的视频批量识别出来然后把它们的状态“修改”为待删除并留下Excel标注。也就是说核心动作是“筛选 提取 标注”而不是“篡改元数据”。文件本身的创建时间不应该动动了反倒麻烦——素材的时间属性一旦变更后续复盘、审计、追溯都会出问题。真正想改时间戳的需求也存在比如测试环境需要统一时间轴但那属于另一个场景我在第5.4节单独说。1.3 为什么方案必须落到Excel而不是直接删很多人的第一直觉是既然规则这么明确脚本筛完直接删不就行了我见过不少清理事故基本都是这个思路埋的雷。直接删有三个致命问题第一删除不可逆误删后找回来的成本极高第二创建时间只是参考维度不排除某些文件虽然落在区间内但业务上还需要用必须有人能复核第三清理动作需要留痕删了哪些文件、依据什么规则、谁确认的Excel清单就是那个证据。所以项目的落地方案被我设计成三级结构脚本扫描生成Excel标注清单 → 人工复核修改标注 → 脚本读取标注执行移动或删除。这套链路里Excel是中间那个最重要的“人机交互层”而不是可有可无的副产品。2. 方案选型手动、PowerShell、Python三条路线怎么选2.1 三条路线对比在正式写脚本之前我把可行的路线过了一遍。手动操作最快但能力最低PowerShell是Windows自带方案胜在零依赖Python生态则更适合做“扫描 Excel 复核删除”的完整体。三条路线的差异我用表格列一下方案区间筛选能力Excel生成可处理量级学习成本复用性资源管理器手动只能排序分组无法精确筛选区间手动复制粘贴百级以内零差PowerShell脚本支持CreationTime属性比较导出CSV再转xlsx需处理编码万级中中Python openpyxl支持且时间解析灵活直接生成xlsx并带样式十万级中高手动操作就不用多说了文件一多直接崩溃。PowerShell的优点是Windows机器开箱即用写几行筛选语句也不难但输出到Excel这一步很尴尬Export-Csv导出的CSV要用Excel打开才能转格式中文编码稍不注意就乱后续做“按标注删除”还得再写一套解析逻辑链条拉得很长。2.2 项目857最终为什么选Python我做这个项目还有个前提清理不是一次性的每个季度可能都要跑一遍而且需要对Excel复核结果做二次处理。这种情况下Python脚本的优势就很明显了代码可以直接演进成“生成报告”和“执行删除”两个独立命令Excel写入用openpyxl能精确控制颜色、列宽、冻结窗格中文路径和Unicode处理也省心。当然如果只是某台Windows机器上跑一次、以后用不上了PowerShell极简版也完全够用我3.4节会给一个可以直接抄的版本。但凡是准备长期用的我建议一步到位用Python。2.3 环境准备需要Python 3.9或以上版本安装openpyxl库。命令很简单pip install openpyxl后面如果要实现“移到回收站而不是直接删除”的安全删除逻辑可以再装一个send2trashpip install send2trash装完之后直接在命令行跑脚本就行。没有Python环境的Windows用户可以先跳到3.4节用PowerShell方案应急。3. 核心脚本实现扫描、提取、筛选、标注一步到位3.1 脚本设计思路这个脚本的关键点有三处一是遍历目录时要能拿到文件的创建时间同时不能太慢二是时间区间要支持精确到秒的起止比较三是Excel输出要有标注逻辑命中的行要一眼能看出来。字段我设计成序号、文件名、完整路径、大小(KB)、创建时间、修改时间、是否建议删除、备注。“修改时间”一列是顺带输出的很多场景下也有参考价值而且脚本写起来几乎不增加成本。标注规则很简单创建时间落在[start, end]闭区间内判定为“是”并把整行标红落在区间外的文件默认不输出也可以加--full参数输出全量便于整体核对。3.2 主脚本代码下面这个脚本我直接贴完整版注释也写好了保存为video_scan.py即可使用# -*- coding: utf-8 -*- 项目857批量视频创建时间区间筛选 Excel标注工具 用法示例 python video_scan.py --dir D:\素材库 --start 2024-03-01 00:00:00 --end 2024-05-31 23:59:59 --out 清理报告.xlsx python video_scan.py --dir D:\素材库 --start 2024-03-01 00:00:00 --end 2024-05-31 23:59:59 --full --out 全量报告.xlsx import argparse import datetime import os import sys from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment from openpyxl.utils import get_column_letter VIDEO_EXTS {.mp4, .mkv, .mov, .avi, .flv, .wmv, .ts, .m4v, .webm, .mpeg, .mpg} HEADERS [序号, 文件名, 完整路径, 大小(KB), 创建时间, 修改时间, 是否建议删除, 备注] def parse_time(text): return datetime.datetime.strptime(text, %Y-%m-%d %H:%M:%S).timestamp() def fmt_time(ts): if ts 0: return N/A return datetime.datetime.fromtimestamp(ts).strftime(%Y-%m-%d %H:%M:%S) def scan_video_files(root, recursive): files [] stack [root] while stack: current stack.pop() try: with os.scandir(current) as it: for entry in it: try: if entry.is_dir(follow_symlinksFalse): if recursive: stack.append(entry.path) else: ext os.path.splitext(entry.name)[1].lower() if ext in VIDEO_EXTS: st entry.stat(follow_symlinksFalse) files.append((entry.path, st)) except OSError as err: print(f[WARN] 文件访问失败: {entry.path} - {err}, filesys.stderr) except PermissionError: print(f[WARN] 无权限访问目录: {current}, filesys.stderr) except OSError as err: print(f[WARN] 目录扫描失败: {current} - {err}, filesys.stderr) return files def export_excel(rows, out_path, full_mode): if full_mode: wb Workbook(write_onlyTrue) ws wb.create_sheet(视频清理清单) else: wb Workbook() ws wb.active ws.title 视频清理清单 # 表头样式 header_font Font(boldTrue, colorFFFFFF) header_fill PatternFill(solid, fgColor4F81BD) for col_idx, _ in enumerate(HEADERS, start1): cell ws.cell(row1, columncol_idx) cell.font header_font cell.fill header_fill cell.alignment Alignment(horizontalcenter, verticalcenter) ws.append(HEADERS) danger_fill PatternFill(solid, fgColorFFC7CE) for idx, row in enumerate(rows, start1): excel_row [idx] list(row) ws.append(excel_row) if not full_mode and row[5] 是: for col_idx in range(1, len(HEADERS) 1): ws.cell(rowidx 1, columncol_idx).fill danger_fill if not full_mode: ws.freeze_panes A2 col_widths [6, 32, 80, 12, 20, 20, 16, 30] for col_idx, width in enumerate(col_widths, start1): ws.column_dimensions[get_column_letter(col_idx)].width width wb.save(out_path) def main(): parser argparse.ArgumentParser(description扫描视频文件提取创建时间按指定区间筛选并生成Excel标注清单) parser.add_argument(--dir, requiredTrue, help要扫描的根目录) parser.add_argument(--start, requiredTrue, help区间开始时间如 2024-03-01 00:00:00) parser.add_argument(--end, requiredTrue, help区间结束时间如 2024-05-31 23:59:59) parser.add_argument(--out, defaultvideo_clean_report.xlsx, help输出Excel文件路径) parser.add_argument(--full, actionstore_true, help输出全部视频文件并在是否建议删除列标注命中区间的文件) parser.add_argument(--no-recursive, actionstore_true, help不递归扫描子目录) args parser.parse_args() start_ts parse_time(args.start) end_ts parse_time(args.end) if end_ts start_ts: parser.error(--end 必须晚于或等于 --start) print(f[1/3] 开始扫描目录: {args.dir}) files scan_video_files(args.dir, recursivenot args.no_recursive) total len(files) print(f[2/3] 扫描完成共发现 {total} 个视频文件) rows_hit [] rows_all [] for path, st in files: ctime st.st_ctime mtime st.st_mtime size_kb round(st.st_size / 1024.0, 1) hit start_ts ctime end_ts flag 是 if hit else 否 remark 落在指定创建时间区间 if hit else row (os.path.basename(path), path, size_kb, fmt_time(ctime), fmt_time(mtime), flag, remark) rows_all.append(row) if hit: rows_hit.append(row) export_rows rows_all if args.full else rows_hit print(f[3/3] 命中区间文件 {len(rows_hit)} 个正在写入Excel: {args.out}) export_excel(export_rows, args.out, full_modeargs.full) print(完成。Excel已生成建议打开后人工复核“是否建议删除”列。) if __name__ __main__: main()脚本里有一个细节我想特别说明遍历用的是os.scandir而不是os.listdir加重复os.path.getctime。因为scandir在拿到目录项时能直接带上文件状态信息一次系统调用就把路径和时间都取回来了文件量大时省下的时间非常可观。3.3 命令用法与输出效果假设素材在D:\素材库要筛出2024年3月1日0点到5月31日23点59分59秒之间创建的视频并生成默认的命中清单python video_scan.py --dir D:\素材库 --start 2024-03-01 00:00:00 --end 2024-05-31 23:59:59 --out 五月清理报告.xlsx如果想输出全量文件并标出命中区间加--full参数即可python video_scan.py --dir D:\素材库 --start 2024-03-01 00:00:00 --end 2024-05-31 23:59:59 --full --out 素材全量清单.xlsx生成的Excel效果是命中区间的行整行标红“是否建议删除”列显示“是”没命中的显示“否”。第一行冻结方便上下翻看。文件名、完整路径、大小、创建时间、修改时间都列清楚了人工复核时一眼就能看到关键信息。3.4 PowerShell极简替代版如果只是临时用一次机器上也没装PythonPowerShell可以快速顶上去。注意-Include配合-Recurse时-Path后面必须以\*结尾否则会返回空结果这个坑我踩过$start Get-Date 2024-03-01 00:00:00 $end Get-Date 2024-05-31 23:59:59 Get-ChildItem -Path D:\素材库\* -Recurse -File -Include *.mp4,*.ts,*.mov,*.mkv | Where-Object { $_.CreationTime -ge $start -and $_.CreationTime -le $end } | Select-Object Name, FullName, {nSizeKB;e{[math]::Round($_.Length/1KB,1)}}, CreationTime | Export-Csv -Path 清理报告.csv -NoTypeInformation -Encoding UTF8这版只负责筛选和导出CSV没有红色标注和全量报告功能。想要xlsx还是得用Excel打开CSV再另存。编码方面Windows PowerShell 5.1的-Encoding UTF8是带BOM的UTF-8Excel能正常识别PowerShell 7里不带BOM中文容易乱建议先用Python方案。这也是我最终选择Pythonopenpyxl的原因之一直接从源头绕开CSV编码问题。4. 实测中踩过的坑时间口径、编码、路径、性能4.1 创建时间和修改时间用混整张表全废项目857第一次出报告的时候我差点把“创建时间”和“最后修改时间”混着用。在Windows上os.path.getctime()取到的是文件创建时间对应资源管理器显示的“创建日期”os.path.getmtime()取到的是最后修改时间对应“修改日期”。视频文件一旦转码、重封装、改名后再保存修改时间会变创建时间通常保留原始值。如果拿修改时间去做区间筛选整个报告的判定结果都可能偏移尤其那些经过转码的视频修改时间往往比创建时间晚一大截一批本来不该删的文件会被标成“是”。这个错误非常隐蔽因为Excel里的时间列看起来都很正常只有对照资源管理器的“创建日期”才能发现口径对不上。我在脚本里把两个时间都输出了原因就在这里方便复核时肉眼对照。Linux和macOS上的情况还不一样。Linux的st_ctime是inode变更时间不是创建时间换平台跑脚本时要特别小心macOS可以用st_birthtime拿到创建时间但也不是所有文件系统都支持。如果哪天要把这套脚本迁到Linux服务器上建议先小范围抽查结果再全量跑。4.2 中文路径、编码与Excel兼容性用Pythonopenpyxl写xlsx中文路径和中文内容基本不用操心字符串就是Unicode写入Excel不会乱。真正踩坑的是CSV方案PowerShell导出的CSV在中英文混杂的路径下很容易出现乱码尤其是Excel直接双击打开CSV时默认按ANSI解析UTF-8无BOM内容直接变“锟斤拷”。另一个烦人的点是Excel里的时间列显示。openpyxl写入datetime对象后如果不显式设置单元格的数字格式Excel可能显示成一串数字或默认日期格式连时分秒都看不到。脚本里我已经把时间转成了%Y-%m-%d %H:%M:%S字符串写入读取时保持原样即可这是最省事、最不容易出错的做法。4.3 长路径和网络盘Windows上路径超过260字符是老问题。素材库存在网络共享盘或子目录特别深的时候某些文件会扫描失败报“文件名或扩展名太长”。Python 3.6以后的os.scandir在部分情况下会自动加\\?\前缀来规避但并非全兼容网络盘尤其不稳定。我在脚本里加了异常捕获和警告输出扫不全时至少知道哪些文件漏了。如果真的遇到大量长路径文件可以先用robocopy /L做一个只读清单确认文件是否存在再决定是否把素材库整体挪到浅层目录。日常管理里我强烈建议素材目录不要建得太深二级目录就够用省得给自己找麻烦。4.4 文件量大时的性能优化第一次跑全量扫描素材库大概有两万多个文件脚本跑了不到一分钟还算能接受。但如果到十万级就有几个优化点要注意。第一遍历用os.scandir不要在循环里反复调os.path.getctime每调一次就是一次系统调用第二写Excel时不要一行一存攒在内存里最后一次性wb.save()这个脚本已经是这样实现的第三几万行的Excel如果还要逐格设置红色填充速度会明显下降因为openpyxl的样式写入比较重。我的处理是默认只输出命中区间的行这样文件数量通常控制在几百到几千填色压力不大。真要输出全量可以用--full但接受速度变慢。5. 从Excel标注到安全删除我推荐的“三遍确认”5.1 为什么扫描之后不能马上删脚本跑完后Excel里已经有了一堆标红的文件看起来非常“值得删”但我不建议你立刻写个循环全部删除。原因有两个。第一区间筛选中没有业务判断。创建时间是机器记录的元数据但它不代表文件内容真的没用。比如某个视频虽然创建于3月但4月被剪辑师引用过一次文件名又看不出关联这种只能靠人工过目。第二创建时间可能因为系统行为变得不准确。我实际遇到过某次服务器迁移大量文件的创建时间被重置为复制当天的日期如果只看时间区间可能把本该保留的旧素材全筛出来。所以扫描自动完成的部分止步于“建议”真正的决定权必须留给人。5.2 三遍确认的操作细节我最终用的流程是“三遍确认”具体操作如下。第一遍脚本扫描生成Excel命中区间标红、标“是”未命中的要么不输出要么用--full输出并标“否”。第二遍人工打开Excel逐行检查把误判的“是”改成“否”也可以在备注列补一句原因比如“该文件已引用”“不确定先不删”。第三遍脚本读取Excel只处理仍然标记为“是”的行但删除动作我先做成“移动到待删除目录”而不是永久删除。保留一周确认无误后再清空。这套流程看起来比“扫完就删”多花几天时间但能挡住绝大多数误删事故。素材清理这类事情一次误删的代价远超那几天等待成本。5.3 复核移动脚本代码下面这个脚本读取第3章生成的Excel把“是否建议删除”列仍为“是”的文件移动到指定目录。移动比删除安全而且能留出观察期import argparse import os import shutil from openpyxl import load_workbook def main(): parser argparse.ArgumentParser(description读取Excel标注清单移动待删除文件到指定目录) parser.add_argument(--xlsx, requiredTrue, helpvideo_scan.py生成的Excel文件) parser.add_argument(--target-dir, requiredTrue, help移动到哪个目录例如 D:\\_待删除_) args parser.parse_args() wb load_workbook(args.xlsx, read_onlyTrue, data_onlyTrue) ws wb.active header [cell.value for cell in next(ws.iter_rows(min_row1, max_row1))] path_col header.index(完整路径) flag_col header.index(是否建议删除) os.makedirs(args.target_dir, exist_okTrue) moved 0 for row in ws.iter_rows(min_row2, values_onlyTrue): if row and row[flag_col] 是 and row[path_col]: src row[path_col] if not os.path.exists(src): print(f[SKIP] 文件不存在: {src}) continue basename os.path.basename(src) dst os.path.join(args.target_dir, basename) if os.path.exists(dst): name, ext os.path.splitext(basename) dst os.path.join(args.target_dir, f{name}_{moved}{ext}) shutil.move(src, dst) moved 1 print(f完成共移动 {moved} 个文件到 {args.target_dir}) if __name__ __main__: main()执行示例python video_move.py --xlsx 清理报告.xlsx --target-dir D:\_待删除_一个实际经验移动操作尽量在同一个磁盘分区内进行shutil.move在本分区内只改目录项瞬间完成如果目标目录在另一个盘实际是复制加删除文件多时会慢很多还可能因为磁盘空间不足中断。5.4 如果你真的想批量修改创建时间标题里“修改创建时间”这个说法确实也有一部分人是字面意思。这个需求通常出现在两类场景一类是想把素材库里的历史文件时间轴统一到某个固定区间便于测试或演示另一类是归档规范化希望文件时间看起来整齐。如果真的要做Windows下PowerShell可以直接改单条命令就能把指定视频的创建时间改成某个固定值Get-ChildItem D:\视频 -Filter *.mp4 | ForEach-Object { $_.CreationTime Get-Date 2024-03-01 00:00:00 }如果希望时间随机落在某个区间内可以这样写$start Get-Date 2024-03-01 00:00:00 $minutesRange 90 * 24 * 60 Get-ChildItem D:\视频 -Filter *.mp4 | ForEach-Object { $randomTime $start.AddMinutes((Get-Random -Maximum $minutesRange)) $_.CreationTime $randomTime $_.LastWriteTime $randomTime }但我必须提醒一句批量修改文件时间会改变素材的真实元数据在需要留档审计的场景里极不推荐。真要动手也要先对整个目录做一次完整备份并且只在副本上操作。这类操作本身不复杂复杂的是后续追溯——一旦时间属性被改过原始信息就找不回来了。所以我在项目857里始终把“提取时间并标注”作为主方案把“修改时间”作为可选的旁路功能两件事不该混在一起。6. 扩展空间与个人体会6.1 这个工具稍加改动还能管什么项目857的脚本表面上是给视频用的核心逻辑其实是“按创建时间区间筛选文件 输出Excel标注”。把VIDEO_EXTS换成图片、音频、日志、压缩包的扩展名集合这个工具立刻就能管住完全不同类型的文件。比如素材库里堆积了大量缓存图片想找出某年某季度之前生成的旧图并归档改一下扩展名集合就能跑。再比如日志服务器上几千个历史压缩包按创建时间筛选出超过90天的待清理项同一套流程也能覆盖。我后来把脚本稍微封装了一下加了个参数指定扩展名列表复用性高了很多。6.2 定时执行和报告推送这个流程还有个很自然的扩展方向定期自动生成清理报告。可以在Windows计划任务里配一个每周任务定时执行扫描命令让Excel报告自动落到指定文件夹再由管理员定期打开复核。不过有一点要注意自动扫描可以自动移动尽量别碰。计划任务里跑的脚本如果配置错了时间区间可能把正在使用中的素材批量挪走等到发现时已经晚了一步。我把移动脚本做成手动执行就是不想让删除动作在没有人工确认的情况下自动发生。6.3 我主要学到的一点做批量清理这件事最大的风险从来不是技术而是“不敢删、删错了、删了没记录”。项目857跑通以后我最大的体会是把扫描、标注、删除三个动作拆开中间留出人工确认的环节比任何高明的脚本都管用。后来团队再遇到类似清理需求流程基本固定成了模板先生成Excel标注清单再人工复核最后移动待删除目录。简单、稳妥、可追溯。素材库清了小两百G过程里最花时间的反而是人工翻Excel那两步但这正好说明技术能做的是把“不确定”变成“一目了然”把最终决定权留给该负责的人。