Python 查找大文件:用 os.walk 和 heapq 列出前 N 名
日志、下载目录和构建产物越积越多,想知道“到底哪几个文件最大”,却不想把文件内容全部读一遍。本文用 Python 标准库写一个只读命令行工具:按大小列出前 N 个普通文件,支持最小大小过滤,跳过目录内的符号链接,并在扫描不完整时返回失败。
适合会运行终端命令的初级到中级读者。先在小型测试目录验证,再用于自己的日志或 Hugo 输出目录;如果已经遇到磁盘写入失败,先按 Linux 磁盘空间与 inode 排查教程确定问题所在的文件系统。
1. 方法:只查元数据,保留前 N 个结果
- 用
os.walk()遍历目录,设置onerror记录无法列出的目录,followlinks=False避免递归进入目录符号链接。 - 对文件条目用
lstat()读取链接本身的信息,再用stat.S_ISREG()只接收普通文件。文件符号链接、断链和 FIFO 都不会进入排名。 - 用
heapq维护最多 N 条候选记录。堆满后,仅在新记录更大时替换最小项,结束后再按降序输出。
这里统计的是逻辑大小 st_size,不是可释放磁盘空间。 单位 1 MiB = 1,048,576 bytes;输出同时保留精确字节数。同样大小按相对路径的字符串降序排列,使前 N 条的选择稳定。
2. 准备环境和测试数据
需要 Linux、Python 3.9 或更新版本和 bash,无第三方依赖。本文示例已在 Linux、Python 3.11.4 上测试;直接使用已有解释器,不需要安装 pip 包。
- 在同一个 bash 终端创建独立演示目录。重复练习时换一个目录名,避免覆盖自己的文件:
python3 --version
mkdir largest-files-demo
cd largest-files-demo
python3 - <<'PY_DATA'
from pathlib import Path
root = Path("sample")
(root / "nested").mkdir(parents=True)
for name, mib in [("small.bin", 1), ("nested/big.bin", 3), ("space name.bin", 2)]:
with (root / name).open("wb") as stream:
for _ in range(mib):
stream.write(b"x" * (1024 * 1024))
(root / "big-link").symlink_to("nested/big.bin")
(root / "dir-link").symlink_to("nested", target_is_directory=True)
(root / "broken-link").symlink_to("missing.bin")
PY_DATA
- 在当前目录保存下一节代码为
largest_files.py,也可下载完整脚本。测试数据只有三个普通文件,共 6 MiB;三个符号链接用于验证排除行为。
3. 完整实现:按逻辑大小排名
"""List the largest regular files without reading their contents. Python 3.9+."""
import argparse
import heapq
import json
import os
from pathlib import Path
import stat
import sys
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument("directory", type=Path)
parser.add_argument("--top", type=int, default=10, help="results to keep, 1-1000")
parser.add_argument("--min-mib", type=int, default=0, help="minimum size in whole MiB")
args = parser.parse_args(argv)
if not 1 <= args.top <= 1000 or args.min_mib < 0:
parser.error("top must be 1-1000 and min-mib must be nonnegative")
try:
root = args.directory.resolve(strict=True)
if not stat.S_ISDIR(root.stat().st_mode):
parser.error("directory must refer to a directory")
except (OSError, RuntimeError) as exc:
parser.error(str(exc))
errors = scanned = matched = 0
largest = []
minimum = args.min_mib * 1024 * 1024
def report_error(exc):
nonlocal errors
errors += 1
print(f"ERROR: {ascii(str(exc))}", file=sys.stderr)
for folder, _dirs, names in os.walk(root, onerror=report_error, followlinks=False):
for name in names:
path = Path(folder) / name
try:
info = path.lstat()
except OSError as exc:
report_error(exc)
continue
if not stat.S_ISREG(info.st_mode):
continue
scanned += 1
if info.st_size < minimum:
continue
matched += 1
item = (info.st_size, str(path.relative_to(root)))
if len(largest) < args.top:
heapq.heappush(largest, item)
elif item > largest[0]:
heapq.heapreplace(largest, item)
print("MiB\tbytes\tpath_json")
for size, relative in sorted(largest, reverse=True):
print(f"{size / (1024 * 1024):.2f}\t{size}\t{json.dumps(relative)}")
print(
f"scanned={scanned} matched={matched} shown={len(largest)} errors={errors}",
file=sys.stderr,
)
return 1 if errors else 0
if __name__ == "__main__":
raise SystemExit(main())
起始目录先用 resolve(strict=True) 转成真实路径,因此你显式传入的起始路径可以是目录链接;目录内部的链接不再跟随。脚本读取目录和元数据,不打开文件内容,也不执行删除或重命名。
scanned 是成功取得元数据的普通文件条目数,matched 是达到大小阈值的条目数,shown 是实际输出数。错误写到 stderr,报告写到 stdout;路径使用 JSON 字符串编码,文件名里的制表符和换行不会破坏一行一条的 TSV 格式。
退出码约定:0 表示没有记录到扫描错误,1 表示发生扫描错误、报告可能不完整,2 表示参数或起始目录无效。无匹配文件也返回 0,此时只有表头。
4. 运行:列出至少 2 MiB 的前两个文件
python3 largest_files.py --help
python3 largest_files.py sample --top 2 --min-mib 2
printf 'exit=%s\n' "$?"
预期内容如下。统计行来自 stderr,与 stdout 在终端中的先后顺序可能不同:
MiB bytes path_json
3.00 3145728 "nested/big.bin"
2.00 2097152 "space name.bin"
scanned=3 matched=2 shown=2 errors=0
exit=0
big-link 不会重复贡献 3 MiB,dir-link 不会让 nested 被扫描两遍,断链也不会被当成读取失败。--min-mib 接受非负整数;--top 限制为 1–1000。
扫描自己的项目时,将 sample 换成实际目录,含空格的路径要加引号。Hugo 项目可以在仓库根目录运行 python3 largest_files.py public --top 20 --min-mib 1,前提是脚本也放在该目录。
5. 验证:断言排名,再检查权限错误
- 分别保存报告和统计,并验证大小、顺序、阈值及链接排除:
python3 largest_files.py sample --top 2 --min-mib 2 > report.tsv 2> scan.log
python3 - <<'PY_CHECK'
import csv
import json
from pathlib import Path
with Path("report.tsv").open(newline="", encoding="utf-8") as stream:
rows = list(csv.DictReader(stream, delimiter="\t", quoting=csv.QUOTE_NONE))
assert [int(row["bytes"]) for row in rows] == [3145728, 2097152]
assert [json.loads(row["path_json"]) for row in rows] == [
"nested/big.bin", "space name.bin"
]
assert Path("scan.log").read_text().strip() == "scanned=3 matched=2 shown=2 errors=0"
print("PASS: ranking, threshold, and symlink exclusions")
PY_CHECK
应输出 PASS: ranking, threshold, and symlink exclusions。如果只有表头,先检查阈值:--min-mib 100 在这组数据上没有匹配项。
- 使用普通用户测试无法列出的子目录,并在命令结束后恢复权限:
mkdir sample/locked
chmod 000 sample/locked
if python3 largest_files.py sample > partial.tsv 2> partial.log; then
scan_status=0
else
scan_status=$?
fi
chmod 700 sample/locked
cat partial.log
printf 'exit=%s\n' "$scan_status"
普通用户应看到 Permission denied、errors=1 和 exit=1,其他可访问文件仍会被列出。root 或具备绕过权限能力的账户可能仍能读取该目录,此时不会复现;不要为了这个测试切换成 root。若中途打断实验,执行 chmod 700 sample/locked 恢复权限。
- 自动化时检查退出码,并把报告放在扫描目录之外:
if python3 largest_files.py sample --top 20 --min-mib 2 > latest.tsv 2> latest.log; then
printf 'Complete report: latest.tsv\n'
else
scan_status=$?
cat latest.log >&2
printf 'Incomplete or invalid scan, exit=%s\n' "$scan_status" >&2
fi
这里报告目录是 sample 的父目录,避免把自己的输出加入排名。失败时的 latest.tsv 只能作为部分线索,不能当成完整清单;正式任务应在失败分支告警或返回非零状态。定时运行的解释器和工作目录问题,可继续参考 cron 中 Python 的 PATH 与工作目录排障。
6. 常见问题与边界
| 现象 | 原因与处理 |
|---|---|
| 报告为空但退出码为 0 | 降低 --min-mib,查看 scanned、matched,确认起始目录。 |
出现 Permission denied |
核对运行账户和具体目录的访问权限;保留错误记录,不要把部分报告当成完整扫描。 |
出现 No such file or directory |
文件可能在扫描期间被移动或删除;暂停相关写入任务后重试。 |
排名与 du 不一致 |
本例使用逻辑大小;稀疏文件、硬链接和目录自身的空间使两者口径不同。 |
| 日志一直占前几名 | 确认保留周期,再为后续日志配置大小轮转与备份数量。 |
os.stat_result 文档区分 st_size 与已分配的 st_blocks:稀疏文件的逻辑大小可能远大于实际占用。本例按路径计数,多个硬链接可分别出现,不能把排名相加当作唯一数据量。
脚本也会遍历真实的挂载子目录,没有实现 du -x 那样的文件系统边界过滤。请选择范围明确的目录,不要直接对 / 做日常扫描。
候选堆只保留 --top 条,但 os.walk() 仍需保存目录条目和遍历状态,不能宣称总内存恒定;超宽或超深目录还会有额外开销。扫描是逐项观察,并非快照;链接或目录被并发替换时,普通路径遍历不能充当安全隔离边界。对自己管理、结构稳定的目录使用即可。
7. 总结
先对明确目录列出大文件,再结合磁盘统计判断空间问题,能减少盲目清理。这个脚本只读元数据,用固定数量的候选记录生成排名,并通过 stderr 和退出码暴露扫描错误。把报告保存在扫描范围之外,确认完整性后再决定日志轮转或归档措施。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-largest-files-heapq.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。