资讯详情

Caveman:用Python标准库实现的极简文件检索与归档工具

📅 2026/10/29 21:50:57 | 华诺云谱 👁 阅读
Caveman:用Python标准库实现的极简文件检索与归档工具
1. 为什么我会把一个工具叫做 Caveman1.1 从一次凌晨三点找不到配置文件开始凌晨三点我蹲在服务器前面找一份三个月前生成的配置文件。文件名我记不全只记得是 caveman 开头后面跟了一串日期放在哪台机器、哪个目录也完全没印象。当时手边没有任何像样的检索工具我只能一层一层ls从/opt翻到/var翻了几分钟才在一堆历史备份里把它捞出来。那种感觉很糟糕。问题不是文件丢了而是没有一套顺手、统一、离线也能跑的检索和归档方式。这件事之后我开始琢磨与其继续依赖记忆不如花一个晚上写个能一直用下去的小工具。于是有了 Caveman。Caveman 这个名字一语双关。第一层意思是“原始人”它刻意保持原始、粗暴、只做最基本的事第二层意思是它像洞穴人手里的石斧笨但真的能干活。我把它定位成一个只依赖 Python 标准库的本地命令行小工具包含三个子命令find按文件名关键词定位文件pack一键打包目录index把全目录文件信息写入 SQLite 索引。它解决的痛点是本地和服务器上文件越来越多记不清路径、忘了归档、目录结构乱到无法检索。这篇文章不是广告也不涉及什么高深架构而是把我从头写这个工具过程中的设计逻辑、代码骨架、实测踩坑和定时任务接入经验完整复盘一遍。适合这几类人阅读本地文件量大但不想装全家桶工具的普通用户需要在不允许随便装第三方库的服务器上做维护的运维和开发或者想学 Python 标准库argparse、os.walk、sqlite3实操的初学者。1.2 为数不多但真实存在的使用场景我先说清楚Caveman 不是一个“生产级服务”也不是要取代任何大型检索系统。它解决的是一个小而固定范围的问题当你明确知道“我要找的文件在哪里附近”但记不住准确文件名和扩展名时它能用一个命令把候选文件全部列出来。真实场景包括某台没有图形界面的 Linux 服务器上同事上传了一批乱序命名的日志你想根据关键词把所有.log找出来。个人电脑上Downloads目录堆积了上千个文件你想按扩展名和关键词筛选后统一打包备份。想给目录里的所有文件建一个最小索引供其他脚本二次查询又不想引入庞大的商业检索软件。Caveman 正是围绕这三个场景设计的。它不监听、不驻留、不自动更新每次执行都显式由用户触发逻辑透明到连我这种记性不好的人都能一眼看穿。2. Caveman 的核心功能与设计取舍2.1 三个子命令各自只干一件事很多工具做着做着就变成了瑞士军刀什么都沾一点什么都不好用。Caveman 在设计上给自己划了清晰的边界只做find、pack、index三件事。我把它们的分工整理成一张表这样你也好对照自己的需求。子命令输入核心输出核心典型用法find关键词、扩展名、根目录、跳过目录匹配文件的完整路径忘记文件名时快速定位pack目录路径、输出路径、跳过目录一个 tar.gz 压缩包备份某个项目或目录index目录路径、数据库路径SQLite 数据库文件给后续脚本做离线查询find和pack看起来只是很平凡的功能但组合起来非常有用。比如我先用find -q 配置 -e .conf定位到一批文件确认无误后再用pack把整个项目目录打包归档整个过程不需要打开任何图形界面也不依赖文件管理器。index则更像是给“记性不好”准备的保险。它遍历一个目录把文件名、完整路径、大小、最后修改时间写入 SQLite 数据库。有人问这不就是locate吗确实有点像但locate依赖系统定期跑updatedb而我这个索引完全由你自己掌控想什么时候扫就什么时候扫扫哪些目录也由自己决定。2.2 用“原始人”哲学对抗工具膨胀我曾认真考虑过用现成方案来替代比如everything、fd、ripgrep、locate甚至干脆装一个全文检索引擎。但这些方案各有各的别扭everything在 Windows 上确实好可它需要常驻后台而且只能跑在 Windows 上。fd和ripgrep很快但它们是编译好的二进制在部分服务器上还得考虑内核和发行版兼容性。locate日常够用可它的数据库更新时机是系统控制的想立刻搜刚下载的文件时经常落空。我的核心需求很原始只要有一个 Python 解释器就能跑。服务器上不联网、不让随便装新包是常有的事而 Python 3 自带标准库几乎可以说是“开局自带石斧”。这让我决定不引入任何第三方依赖。有人会觉得“不用第三方库”很刻意但这不是技术洁癖而是运维现实。一台生产服务器上装一个 PyPI 包意味着要处理依赖树、权限、网络源甚至可能影响系统中已运行的 Python 应用。Caveman 整个项目只有几百行代码单文件就能运行拷过去就能用这种可控性和低侵入性是“原始人哲学”最大的价值。代价我也认了没有高亮显示、没有交互式搜索、没有模糊匹配、没有图形界面。可话说回来凌晨三点找配置文件的时候我需要的不是炫酷界面而是“输入关键词出来路径复制走人”。Caveman 正好做到这一点。3. 用 Python 标准库写稳这三个子命令3.1 argparse 子命令解析别急着上 Click命令行工具的第一步是把参数解析处理好。我用的是argparse自带的subparsers它能天然生成caveman find、caveman pack、caveman index三段式命令结构。#! /usr/bin/env python3 caveman: 原始人级别的本地文件小工具。 import argparse import fnmatch import os import sqlite3 import sys import tarfile import time from pathlib import Path def _build_parser(): parser argparse.ArgumentParser( progcaveman, description按文件名关键词查找、一键打包、生成轻量索引的极简工具。, epilog示例caveman find -q 配置 -e .conf -d ., ) sub parser.add_subparsers(destcommand, requiredTrue) p_find sub.add_parser(find, help按文件名关键词定位文件) p_find.add_argument(-q, --query, default, help关键词不区分大小写) p_find.add_argument(-e, --ext, nargs*, default[*], help扩展名列表例如 .md .conf) p_find.add_argument(-d, --dir, default., help搜索根目录) p_find.add_argument(--skip, nargs*, default[.git, __pycache__, node_modules], help跳过目录名) p_find.add_argument(--follow, actionstore_true, help是否跟随符号链接) p_find.set_defaults(funccmd_find) p_pack sub.add_parser(pack, help把目录打成 tar.gz) p_pack.add_argument(-d, --dir, default., help需要打包的目录) p_pack.add_argument(-o, --out, requiredTrue, help输出的 tar.gz 路径) p_pack.add_argument(--skip, nargs*, default[.git, __pycache__, node_modules], help过滤目录名) p_pack.set_defaults(funccmd_pack) p_index sub.add_parser(index, help把文件信息写入 sqlite 索引) p_index.add_argument(-d, --dir, default., help扫描根目录) p_index.add_argument(-o, --out, default~/.cache/caveman.db, help索引数据库路径) p_index.add_argument(--skip, nargs*, default[.git, __pycache__, node_modules], help过滤目录名) p_index.set_defaults(funccmd_index) return parser def main(): _setup_output() args _build_parser().parse_args() sys.exit(args.func(args))为什么不用click或typer因为这两个库确实好用但它们不是标准库。在能联网开发的个人电脑上无所谓可一旦把脚本拷到离线服务器安装依赖就成了大问题。argparse生成的--help虽然朴素但信息完整对命令行工具来说已经足够。3.2 findos.walk 与 fnmatch 的配合find的核心逻辑是遍历目录树然后按文件名做两层过滤先看扩展名是否符合再看关键词是否在文件名里。之所以不用pathlib.glob一步到位是因为我想对遍历过程有精细控制尤其是跳过哪些目录、是否跟随符号链接。def _walk_with_notice(root, followFalse): for cur, dirs, files in os.walk( root, followlinksfollow, onerrorlambda e: print(f跳过无法访问的目录: {e}, filesys.stderr), ): yield cur, dirs, files def cmd_find(args): root Path(args.dir).expanduser() if not root.is_dir(): print(f目录不存在或不可读: {root}, filesys.stderr) return 1 skip set(args.skip or []) for cur, dirs, files in _walk_with_notice(root, args.follow): dirs[:] [d for d in dirs if d not in skip] for name in files: if args.ext and not any(fnmatch.fnmatch(name, pat) for pat in args.ext): continue if args.query and args.query.lower() not in name.lower(): continue print(Path(cur) / name) return 0一个细节很关键fnmatch.fnmatch在匹配扩展名时用的是文件全名比如report.conf.bak如果按*.conf匹配会失败因为它不是以.conf结尾。因此我给默认扩展名列表传了[*]这样只要不指定扩展名所有文件都会被关键词过滤。如果你只想找.conf结尾的文件可以用-e *.conf而不仅是.conf这个习惯在写匹配规则时能少踩很多坑。3.3 packtarfile 归档的安全做法pack做的事情是把一个目录打平成tar.gz。需要注意的点有两个一是遍历时同样要跳过虚拟环境和版本库目录二是在把文件加入压缩包时要使用相对于根目录的arcname避免把绝对路径写进包里。def cmd_pack(args): src Path(args.dir).expanduser() if not src.is_dir(): print(f目录不存在: {src}, filesys.stderr) return 1 out Path(args.out).expanduser() out.parent.mkdir(parentsTrue, exist_okTrue) skip set(args.skip or []) with tarfile.open(out, w:gz) as tar: for cur, dirs, files in _walk_with_notice(src): dirs[:] [d for d in dirs if d not in skip] for name in files: full Path(cur) / name arc full.relative_to(src) tar.add(full, arcnamearc) size_mb out.stat().st_size / 1024 / 1024 print(f完成{out}大小 {size_mb:.1f} MB) return 0这里我故意不写“空目录也打包”。技术上说空目录在一些场景下值得保留但 Caveman 的定位是“最原始的文件备份”多数情况下用户关心的是文件内容空目录反而容易在解压后制造干扰。如果你需要保留空目录可以在os.walk循环里检查files为空时用tar.add(cur, arcname...)补一条目录记录。3.4 indexsqlite3 轻量索引index子命令负责把目录里的文件信息写进 SQLite 数据库。核心价值是让后续程序快速查询不必每次都重新遍历磁盘。def cmd_index(args): root Path(args.dir).expanduser() db_path Path(args.out).expanduser() db_path.parent.mkdir(parentsTrue, exist_okTrue) skip set(args.skip or []) conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS files ( path TEXT PRIMARY KEY, name TEXT, size INTEGER, mtime REAL ) ) count 0 conn.execute(BEGIN) try: for cur, dirs, files in _walk_with_notice(root): dirs[:] [d for d in dirs if d not in skip] for name in files: full Path(cur) / name try: st full.stat() size, mtime st.st_size, st.st_mtime except OSError: size, mtime -1, 0.0 conn.execute( INSERT OR REPLACE INTO files(path, name, size, mtime) VALUES (?, ?, ?, ?), (str(full), name, size, mtime), ) count 1 conn.commit() except Exception: conn.rollback() raise finally: conn.close() print(f索引完成{count} 个文件 - {db_path}) return 0SQLite 是我特意选的因为它零配置、单文件、Python 内置支持。索引库在大量文件时会变成几十 MB但每次检索都不需要加载整个库SQL 查询速度依然很快。我一般再配合一条sqlite3命令行来做查询sqlite3 ~/.cache/caveman.db \ SELECT path FROM files WHERE name LIKE %配置% AND path LIKE %.conf;这就是把 Caveman 当作“数据采集器”真正的灵活的查询留给 SQL。工具边界在这里非常清楚Caveman 负责把文件快照落库SQL 负责按任意条件查找。4. 实测中比功能更值得说的边界问题功能写出来是一回事真正跑起来又是另一回事。下面是实际测试中让我调整代码的几个边界问题比功能本身更有价值。4.1 权限不足和符号链接别让遍历半路崩掉第一次写find时我以为os.walk会自动跳过没权限的目录。实测后的确没有直接崩溃但会在某些系统上通过onerror把PermissionError传给回调。如果不处理遍历会直接停下来一个 10 万文件的目录树可能只扫完前两层就中断。我的解决方式是给os.walk传入onerror回调打印一条警告到标准错误然后继续扫其他可访问的目录。这样至少不会因为某个目录权限异常把整条命令卡死。符号链接是另一个坑。默认情况下os.walk不跟随符号链接这是正确且安全的行为否则目录树里存在循环链接时你可能会陷入无限递归。但如果确实需要跟随可以在find子命令里加--follow参数显式打开followlinks。日常使用建议保持关闭因为你通常不希望搜索结果里出现一堆带着奇怪名字的符号链接。4.2 中文文件名与终端编码中文文件名在 Linux 服务器上多数情况没问题但在 Windows 的某些终端环境下print一个包含中文的路径可能直接抛出UnicodeEncodeError。这个问题在 Python 2 时代特别普遍Python 3 虽然默认使用 UTF-8但在中文 Windows 控制台仍然是 GBK 编码体系。为了避免这种“路径还没输出程序先崩”的尴尬我在入口处加了_setup_outputdef _setup_output(): if sys.stdout and hasattr(sys.stdout, reconfigure): sys.stdout.reconfigure(encodingutf-8, errorsreplace)errorsreplace的作用是遇到无法编码的字符时用?替代而不是抛异常。这样即使名称里有特殊符号工具也能依旧完整跑完至少不会因为输出环节导致临时中断。4.3 性能12 万文件测出来的数字我拿自己那台旧笔记本简单测了一轮环境是 Windows 11 Python 3.11目录是本地磁盘里一个接近 12 万个文件的混合项目包含大量.png、.log、.md和无数小文件。操作数据量实测耗时备注caveman find -q 配置 -e .md12 万文件约 1.2 秒第一次扫描后无缓存caveman pack12 万文件约 38 秒压缩比较依赖文件类型caveman index12 万文件约 11 秒SQLite 写入单事务对索引库执行 SQL 查询12 万记录几毫秒依赖path主键这个性能足够覆盖日常使用。如果你要扫描的是百万级文件建议把index的 SQLite 事务改成每 5000 条提交一次避免单事务内占用过大的 WAL 空间。我之所以在代码里用BEGIN包住全部写入是为了在文件量不大时获得最快速度文件量大时可以自行拆成批次。5. 接入定时任务让 Caveman 每天自己跑把index做成手动命令有一个问题你可能会忘了跑等真要用时索引还是旧的。定时任务是最自然的补充既不需要常驻进程又能保证索引每天更新一次。5.1 Linux 下用 crontab 建每日索引在 Linux 服务器上我习惯把定时任务写进自己的用户 crontab。先执行crontab -e然后加一行30 2 * * * /usr/bin/python3 /opt/caveman/caveman.py index -d /data -o /var/cache/caveman.db这里两个细节非常关键所有路径都用绝对路径。cron 的环境变量非常精简不会自动带上你交互 shell 的PATH相对路径很容易失效。Python 解释器路径写明确不要只写python3。某些系统上 cron 可能找不到/usr/local/bin/python3导致任务没有任何报错却根本没能运行。如果你还要定期打包可以写成一行两个命令用连接30 2 * * * /usr/bin/python3 /opt/caveman/caveman.py index -d /data -o /var/cache/caveman.db /usr/bin/python3 /opt/caveman/caveman.py pack -d /data -o /backup/data-$(date \%F).tar.gz注意 crontab 里百分号%需要转义成\%否则会被当作换行符处理。5.2 Windows 下用计划任务做一个“伪服务”Windows 上没有 crontab但有schtasks。用管理员权限打开 PowerShell执行schtasks /Create /SC DAILY /TN CavemanIndex /TR python D:\tools\caveman.py index -d D:\data -o D:\cache\caveman.db /ST 02:30这条命令会在每天凌晨 2:30 创建一个名为CavemanIndex的计划任务。删除它也很简单schtasks /Delete /TN CavemanIndex /F如果你希望失败时能看到日志可以在命令后面加 D:\logs\caveman.log 21但要注意schtasks的/TR参数在引号内处理重定向有时会被 Windows 安全策略限制。更稳妥的做法是写一个.bat文件把重定向放到批处理内部再让计划任务调用它。5.3 自动化之后要盯住的三个状态接入定时任务不算完自动化意味着你不再每天手动观察输出所以更要关注三个状态近一次任务是否真的执行了。Linux 下可以在 crontab 里加21 /var/log/caveman-index.logWindows 计划任务界面也能查看“上次运行结果”。索引库是否每天更新。看数据库文件的修改时间就能判断如果连续几天没变多半是扫描目录路径写错或磁盘挂载点发生变化。磁盘空间是否足够。pack如果每天打包备份目录会持续增长顺手写一个检查脚本只保留最近 N 天的包比盲目堆积要理智得多。6. 回看踩过的坑经验、教训和下一步6.1 我给项目加“自动删除”功能后立刻删掉了开发过程中我一度想加一个clean子命令专门把超过 90 天没修改的文件自动删除。逻辑不难但我在测试时差点把一条重要的配置目录清空。原因是我把“按 mtime 判定太久没改”用错了字段某些系统文件很久没更新但依然重要。最后我把自动删除功能整体移除。Caveman 现在的定位是只读检索和归档工具不主动修改任何源文件。这个决定让它的安全性提升了一个量级。如果你也想做清理工具建议单独写一个 dry-run 模式先打印所有“将被删除”的文件清单人工确认后再执行千万不要把删除动作和检索工具混在一起。6.2 命令名撞车与退出码“Caveman”并不是一个很少见的名字我在一台机器上实际执行时发现系统里已经有一个同名命令。解决方法是先which caveman检查如果冲突就把项目文件改名成caveman.py或者用alias cavemanpython /opt/caveman/caveman.py在 shell 层解决。退出码是另一个容易被忽略的细节。脚本在自动化中被调用时调用方通常只关心退出码0 代表成功非 0 代表失败。所以我在每个cmd_xxx里对目录不存在、参数错误等情况都明确返回1或2而不是让 Python 抛出堆栈后返回异常码。这样在 shell 脚本里就能直接用if caveman index ...; then ...; else ...; fi判断任务状态。6.3 如果你也想抄作业怎么改最稳整套代码加在一起不到 300 行直接抄完全没问题但更值得做的是先想清楚它在你环境里的定位。我的建议是保留单文件结构方便拷贝到任意机器。如果你要处理的是大量云盘同步目录把.cloud、.sync这些隐藏目录加入默认--skip列表。如果你需要匹配文件内容而不只是文件名Caveman 不适合应该换rg或全文检索方案别硬把 JSON 塞进 SQLite。增加颜色输出很容易但请放在独立函数里避免在纯文本终端上打印控制字符。最后说一句我自己的体会工具不在于功能多而在于边界清楚。Caveman 这个名字就像一个提醒原始人手里的石斧虽然不够精致但当你站在山洞外面对一片荒野时它比一堆不知道怎么启动的复杂机械更有用。越简单的工具越不容易坏也越让人敢在凌晨三点信任它。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑