春江暮客

春江暮客的个人学习分享网站

Python 查找大文件:用 os.walk 和 heapq 列出前 N 名

2026-10-15 技术
Python 查找大文件:用 os.walk 和 heapq 列出前 N 名

日志、下载目录和构建产物越积越多,想知道“到底哪几个文件最大”,却不想把文件内容全部读一遍。本文用 Python 标准库写一个只读命令行工具:按大小列出前 N 个普通文件,支持最小大小过滤,跳过目录内的符号链接,并在扫描不完整时返回失败。

适合会运行终端命令的初级到中级读者。先在小型测试目录验证,再用于自己的日志或 Hugo 输出目录;如果已经遇到磁盘写入失败,先按 Linux 磁盘空间与 inode 排查教程确定问题所在的文件系统。

1. 方法:只查元数据,保留前 N 个结果

  1. 用 os.walk() 遍历目录,设置 onerror 记录无法列出的目录,followlinks=False 避免递归进入目录符号链接。
  2. 对文件条目用 lstat() 读取链接本身的信息,再用 stat.S_ISREG() 只接收普通文件。文件符号链接、断链和 FIFO 都不会进入排名。
  3. 用 heapq 维护最多 N 条候选记录。堆满后,仅在新记录更大时替换最小项,结束后再按降序输出。

这里统计的是逻辑大小 st_size,不是可释放磁盘空间。 单位 1 MiB = 1,048,576 bytes;输出同时保留精确字节数。同样大小按相对路径的字符串降序排列,使前 N 条的选择稳定。

2. 准备环境和测试数据

需要 Linux、Python 3.9 或更新版本和 bash,无第三方依赖。本文示例已在 Linux、Python 3.11.4 上测试;直接使用已有解释器,不需要安装 pip 包。

  1. 在同一个 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
  1. 在当前目录保存下一节代码为 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. 验证:断言排名,再检查权限错误

  1. 分别保存报告和统计,并验证大小、顺序、阈值及链接排除:
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 在这组数据上没有匹配项。

  1. 使用普通用户测试无法列出的子目录,并在命令结束后恢复权限:
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 恢复权限。

  1. 自动化时检查退出码,并把报告放在扫描目录之外:
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 和退出码暴露扫描错误。把报告保存在扫描范围之外,确认完整性后再决定日志轮转或归档措施。

标签

1024 12306 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena copy cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging deployment desktop-app devtools disk-space disown docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pdb pep-723 phaser pillow pip postfix preprocessing print protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 signals sklearn solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tensorflow tkinter toml tron tronpy troubleshooting turtle typesafe-ai ubuntu usdt uv vhhbert vite webp wordcloud wordpress workflow yaml zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它