春江暮客

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

Linux 报 Argument list too long:用 find 和 Python 处理大量文件

2026-10-20 技术
Linux 报 Argument list too long:用 find 和 Python 处理大量文件

目录里的文本文件越来越多,原来能运行的 wc -c data/*.txt 突然报 Argument list too long。给路径加引号可以保留空格,但不能解决展开后的参数总量超限。

本文面向初中级 Linux/Python 用户,用两个只读方法统计 .txt 文件的字节数:GNU 工具通过标准输入接收文件名,Python 则只接收目录路径,再在进程内部遍历文件。两种方法都能处理带空格和换行的文件名。

1. 方法选择:把文件列表移出命令参数

Python 的 errno 文档把 E2BIG 定义为参数列表过长。常见原因是 shell 在启动程序之前把通配符展开为大量路径;参数与环境占用的空间受到系统限制,单个参数也可能过长。不要把某台机器的 ARG_MAX 当成固定文件数量上限。

需求 方法
临时统计,系统有 GNU find 和 GNU wc find -print0 配合 wc --files0-from=-
输出 JSON、接入定时任务或增加业务逻辑 Python 内部遍历目录

subprocess.run(["wc", "-c", *paths]) 虽然没有使用 shell,仍把所有路径放进一次进程启动的参数列表,也会遇到长度限制。已有命令的超时与错误处理可参考外部命令执行教程。

2. 准备环境与测试文件

需要 Linux、Bash、Python 3.9 或更新版本。方法一另外需要 GNU findutils 和 GNU coreutils;本文在 Python 3.11.4、GNU wc 9.7 下验证,无第三方 Python 依赖。若系统没有 GNU 工具,直接使用方法二,无需为此全局安装软件。

  1. 新建演示目录,重复运行时换一个目录名:
python3 --version
find --version
wc --version
mkdir argument-list-demo
cd argument-list-demo
python3 - <<'PY_DATA'
from pathlib import Path
root = Path("data")
root.mkdir()
for name, content in [("normal.txt", b"abc\n"),
                      ("two words.txt", b"12\n"),
                      ("line\nbreak.txt", b"xy\n")]:
    (root / name).write_bytes(content)
(root / "ignore.csv").write_text("not counted\n", encoding="utf-8")
PY_DATA

三个文本文件共 10 字节。其中一个名称含空格,另一个名称真的包含换行;CSV 不计入。下面所有命令都在这个演示目录执行。

3. 方法一:用 NUL 分隔文件名

  1. 在 Bash 中执行:
set -o pipefail
find ./data -type f -name '*.txt' -print0 | wc -c --files0-from=-

最后一行的合计应为 10 total,排版空格可能不同。*.txt 必须加引号,模式由 find 处理;-print0 用 NUL 分隔路径,换行和空格不会拆开名称。GNU wc 的 --files0-from=- 从标准输入读取这些路径,命令本身只有固定数量的参数。

这些选项见 GNU find 文件名处理说明和 GNU wc 手册。pipefail 让 find 扫描失败也能使流水线失败;退出码非零时,不要使用已经打印出来的部分统计。

输出中的文件名可能跨行,不适合按空白或换行解析。没有匹配文件时不会提供可依赖的合计行;只有一个文件时也不一定有 total。机器读取结果时建议采用下面固定结构的 JSON 输出。

4. 方法二:让 Python 在进程内部遍历

  1. 将完整代码保存为 size_tree.py,也可下载 size_tree.py:
"""Summarize regular *.txt files without expanding filenames into argv."""
import argparse
import errno
import json
import os
from pathlib import Path
import stat
import sys


def stop_walk(error):
    raise error


def summarize(directory):
    root = Path(directory).resolve(strict=True)
    if not root.is_dir():
        raise NotADirectoryError(errno.ENOTDIR, "not a directory", str(root))
    files = total = 0
    for parent, _, names in os.walk(root, followlinks=False, onerror=stop_walk):
        for name in names:
            if not name.endswith(".txt"):
                continue
            info = (Path(parent) / name).lstat()
            if stat.S_ISREG(info.st_mode):
                files += 1
                total += info.st_size
    return {"files": files, "bytes": total}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("directory", type=Path)
    args = parser.parse_args()
    try:
        report = summarize(args.directory)
    except OSError as exc:
        label = errno.errorcode.get(exc.errno, "OSERROR")
        print(f"ERROR {label}: {exc}", file=sys.stderr)
        return 1
    print(json.dumps(report, sort_keys=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
  1. 查看帮助并统计:
python3 size_tree.py --help
python3 size_tree.py ./data

预期输出:

{"bytes": 10, "files": 3}

脚本只把目录作为一个参数传入,然后按目录遍历。它不会建立整棵目录树的文件列表,但 os.walk 仍会为当前目录生成名称列表,超大单层目录仍需内存。

os.walk 官方文档说明,扫描错误默认会被忽略;这里用 onerror 重新抛出异常,避免把漏扫当成成功。lstat() 与 S_ISREG 只计入普通文件,跳过文件符号链接,不进入目录符号链接。传入的根路径会先解析,若根本身是符号链接,会扫描其目标。

这里累加的是文件元数据中的逻辑字节数,不读取内容、不验证可读性,也不等于实际磁盘占用。GNU wc 需要能够读取文件,因此权限受限时两者的结果可能不同。硬链接按路径重复计数;备份前的容量预算可继续参考磁盘空间预检。

5. 验证特殊名称与失败退出

在同一目录运行下列自检。测试使用独立临时目录并自动清理,不改动演示数据:

python3 - <<'PY_CHECK'
from pathlib import Path
from tempfile import TemporaryDirectory
import json
import subprocess
import sys
from size_tree import summarize

script = Path("size_tree.py").resolve()
with TemporaryDirectory() as work:
    root = Path(work)
    assert summarize(root) == {"files": 0, "bytes": 0}
    nested = root / "nested"
    nested.mkdir()
    for name in ("two words.txt", "line\nbreak.txt", "-option.txt"):
        (nested / name).write_bytes(b"abc\n")
    (root / "alias.txt").symlink_to(nested / "two words.txt")
    (root / "loop").symlink_to(root, target_is_directory=True)
    result = subprocess.run([sys.executable, str(script), str(root)],
                            capture_output=True, text=True, check=True)
    assert json.loads(result.stdout) == {"files": 3, "bytes": 12}
    for bad, label in [(root / "missing", "ENOENT"),
                       (nested / "-option.txt", "ENOTDIR")]:
        result = subprocess.run([sys.executable, str(script), str(bad)],
                                capture_output=True, text=True)
        assert result.returncode == 1 and not result.stdout, result
        assert label in result.stderr, result.stderr
print("PASS: empty, nested, unusual names, symlinks, error exits")
PY_CHECK

应输出 PASS: empty, nested, unusual names, symlinks, error exits。退出码约定:成功为 0;路径或扫描失败为 1,且标准输出没有部分 JSON;参数用法错误由 argparse 返回 2。

6. 排错与接入自动化

现象 具体处理
wc: unrecognized option --files0-from 当前 wc 不是兼容的 GNU 版本;使用 Python 方法。
仍出现 Argument list too long 检查是否又用了 *.txt 展开、命令替换或一次性拼接所有路径;只传目录或走标准输入。
Python 输出 ERROR ENOENT / ENOTDIR 检查实际工作目录和传入路径,传已有目录。
Python 输出 ERROR EACCES 用任务实际账号检查目录及其父目录的访问权限;不要忽略扫描失败。
JSON 字节数与 du 不同 脚本统计逻辑大小,du 常统计磁盘块;两者口径不同。

接入定时任务时使用绝对解释器、脚本和数据目录路径,并检查退出码,具体见定时任务路径排障。不要写成 python3 size_tree.py ./data/*.txt,这个脚本接收的是一个目录。

两种方法都不是目录快照:扫描期间文件新增、删除、替换或写入,会影响结果。应在自己控制、内容稳定的目录中运行;followlinks=False 也不能防御不可信用户在扫描过程中替换路径。不要把这段统计代码直接改造成以高权限运行的递归删除器。

7. 小结

处理大量文件时,把文件列表放到标准输入或 Python 进程内部,就能避开一次性展开所有文件名的参数限制。使用 NUL 分隔符保留特殊名称,自动化场景则用 JSON 和明确退出码接收结果。先在小目录验证统计口径与失败行为,再扫描真实数据。

标签

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 dns 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 imports 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 pathlib 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 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tensorflow tkinter tls toml tron tronpy troubleshooting turtle typesafe-ai ubuntu usdt uv vhhbert vite webp wordcloud wordpress workflow yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它