春江暮客

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

用 Python 扫描失效的符号链接:区分断链、权限错误与循环链接

2026-10-16 技术
用 Python 扫描失效的符号链接:区分断链、权限错误与循环链接

文件管理器里能看到链接,Python 却报 FileNotFoundError:移动了数据目录、清理了旧版本,链接仍保留着原来的目标。本文用 Python 标准库写一个只读检查工具,列出目录里的断链,并把“目标缺失”和“无法完成检查”分开报告。

适合会运行终端命令的初级到中级读者。若问题只在定时任务中出现,也要结合 Linux 定时任务的路径与工作目录排查,确认任务检查的确实是同一个目录。

1. 方法:先检查链接本身,再查询目标

  1. 用 os.walk() 遍历真实子目录,设置 followlinks=False,不递归进入目录符号链接;onerror 收集目录无法列出的错误。
  2. 用 Path.lstat() 确认条目本身是符号链接,再用 os.readlink() 取得它保存的目标字符串。
  3. 用 Path.stat() 跟随链接查询目标。目标路径不存在,或中间路径应为目录却实际是文件,记为断链;权限不足、循环链接等错误单独记录。

相对目标从链接所在目录开始解释,与启动脚本的工作目录无关。有效的外部目标也算正常,但脚本不会进入链接指向的外部目录。若之前用 Python 大文件排名工具排查过空间,它会跳过符号链接;本工具补充检查这些链接的目标。

这里只验证目标能否解析,不验证目标内容、业务格式或可读性。请检查暂停变更的本地项目目录:扫描不是原子快照,也不会阻止扫描期间的目录替换;实际子目录中的挂载点仍可能被遍历。

2. 准备环境和可重复的样例

需要 Linux、Python 3.9 或更新版本和 bash,无需安装第三方包。本文命令已在 Linux、Python 3.11.4 上验证。

  1. 在同一个 bash 终端创建独立目录。重复练习时换一个目录名:
python3 --version
mkdir symlink-audit-demo
cd symlink-audit-demo
python3 - <<'PY_DATA'
from pathlib import Path
root = Path("sample")
(root / "nested").mkdir(parents=True)
(root / "data.txt").write_text("ready\n", encoding="utf-8")
(root / "good-file").symlink_to("data.txt")
(root / "good-dir").symlink_to("nested", target_is_directory=True)
(root / "missing-file").symlink_to("gone.txt")
(root / "missing-dir").symlink_to("gone-dir", target_is_directory=True)
PY_DATA
  1. 将下一节保存为 check_symlinks.py,放在当前的 symlink-audit-demo 目录中,也可下载完整脚本。样例有四个链接:两个有效,另外两个分别指向缺失的文件和目录。

3. 完整实现:输出 JSON 和退出码

"""Read-only symlink audit for a quiet local directory. Python 3.9+."""
import argparse
import json
import os
from pathlib import Path
import stat


def audit(root):
    broken = []
    errors = []
    symlinks = 0

    def record_error(path, exc):
        errors.append({"path": str(path), "errno": exc.errno,
                       "message": str(exc)})

    def walk_error(exc):
        record_error(exc.filename or root, exc)

    for folder, dirs, names in os.walk(root, onerror=walk_error,
                                       followlinks=False):
        dirs.sort()
        for name in sorted(dirs + names):
            path = Path(folder) / name
            try:
                mode = path.lstat().st_mode
                if not stat.S_ISLNK(mode):
                    continue
                symlinks += 1
                target = os.readlink(path)
            except OSError as exc:
                record_error(path, exc)
                continue
            try:
                path.stat()
            except (FileNotFoundError, NotADirectoryError):
                broken.append({"path": str(path.relative_to(root)),
                               "target": target})
            except OSError as exc:
                record_error(path, exc)

    return {"root": str(root), "symlinks": symlinks,
            "broken": sorted(broken, key=lambda item: item["path"]),
            "errors": sorted(errors, key=lambda item: item["path"]),
            "complete": not errors}


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("directory", type=Path, help="directory to inspect")
    args = parser.parse_args(argv)
    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))

    report = audit(root)
    print(json.dumps(report, indent=2, sort_keys=True))
    if report["errors"]:
        return 2
    return 1 if report["broken"] else 0


if __name__ == "__main__":
    raise SystemExit(main())

根目录会先解析成真实路径,因此传入的根目录本身可以是有效的目录链接。遍历时同时检查 dirs 和 names,才能包含目录链接。报告中的断链路径相对于根目录,错误路径保留绝对位置;JSON 会转义特殊字符,包含空格或换行的名称也不会被当成多条记录。

退出码 含义 后续动作
0 检查完成,没有断链 继续后续任务。
1 检查完成,发现断链 核对并恢复目标,或单独评审链接路径。
2 参数、根目录或扫描错误 读取 stderr 或报告中的 errors,修复后重查。

扫描错误优先返回 2,即使同时发现断链。complete: true 表示未记录到扫描错误,并不表示没有断链;参数或根目录无效时,错误写入 stderr,不产生 JSON。报告会在内存中保留全部断链和错误,适合项目目录排障。

4. 验证断链与目标恢复

  1. 运行工具,并保留退出码。报告写在被检查目录之外:
python3 check_symlinks.py --help
if python3 check_symlinks.py sample > report.json; then
  scan_rc=0
else
  scan_rc=$?
fi
printf 'exit=%s\n' "$scan_rc"
python3 - <<'PY_CHECK'
import json
from pathlib import Path
report = json.loads(Path("report.json").read_text())
assert report["symlinks"] == 4
assert report["complete"] is True
assert report["errors"] == []
assert report["broken"] == [
    {"path": "missing-dir", "target": "gone-dir"},
    {"path": "missing-file", "target": "gone.txt"},
]
print(f"symlinks={report['symlinks']} broken={len(report['broken'])} complete={report['complete']}")
PY_CHECK

预期输出末尾为:

exit=1
symlinks=4 broken=2 complete=True
  1. 在这个演示目录恢复两个缺失目标,再运行检查:
printf 'restored\n' > sample/gone.txt
mkdir sample/gone-dir
if python3 check_symlinks.py sample > repaired.json; then
  scan_rc=0
else
  scan_rc=$?
fi
printf 'exit=%s\n' "$scan_rc"

此时应输出 exit=0,repaired.json 中 broken 和 errors 都是空列表。链接本身没有被删除或重建。真实项目中先核对目标是否应该恢复,不能仅为消除告警而创建空文件。

5. 验证循环链接,再接入自动化

  1. 在刚才已经恢复目标的样例里添加一对循环链接:
ln -s loop-b sample/loop-a
ln -s loop-a sample/loop-b
if python3 check_symlinks.py sample > loop-report.json; then
  scan_rc=0
else
  scan_rc=$?
fi
printf 'exit=%s\n' "$scan_rc"
python3 - <<'PY_LOOP'
import errno
import json
from pathlib import Path
report = json.loads(Path("loop-report.json").read_text())
assert report["complete"] is False
assert report["broken"] == []
assert len(report["errors"]) == 2
assert all(item["errno"] == errno.ELOOP for item in report["errors"])
print("loop errors=2 complete=False")
PY_LOOP

预期输出为 exit=2 和 loop errors=2 complete=False。循环解析失败不能伪装成“目录里没有断链”。此外还验证了空目录、无效根目录、相对和绝对目标、Unicode 名称、目标路径中间不是目录,以及低权限子进程遇到的真实 PermissionError。

  1. 用于自己的批处理时,先建立可写的报告目录,并替换下例的解释器、脚本、数据和报告绝对路径。不要把这些示例路径原样加入定时器:
if /usr/bin/python3 /srv/tools/check_symlinks.py /srv/project/data > /srv/reports/symlinks.json; then
  scan_rc=0
else
  scan_rc=$?
fi
printf 'symlink audit exit=%s\n' "$scan_rc"
exit "$scan_rc"

这个包装片段将工具的退出码传回调用方;即使开启 set -e,也能通过 if 捕获预期的非零结果。重定向会覆盖旧报告;如果需要保留历史,改用每次不同的报告名。

6. 常见问题与直接处理

现象 处理方法
目标文件被移动,出现断链 对照 target 检查链接所在目录,恢复正确目标或在单独的维护步骤中重建链接,再运行工具。
errors 出现 Permission denied 使用实际任务账户复查目录的读取和路径各层的执行权限;按项目需求修正权限,不要把这一项当作缺失目标。
errors 出现 Too many levels of symbolic links 检查链接链条,修复循环或过长链条,再扫描。
目标存在,程序仍读不到内容 本工具不测试内容读取权限;使用运行该程序的账户检查目标文件权限和格式。
目录链接内部的断链没出现 工具只检查这个目录链接的目标是否存在。要扫描目标内部,请另一次运行并把目标目录作为根目录。

7. 总结

先区分链接本身与目标,再根据退出码处理断链和扫描失败,就能把文件找不到的问题变成可重复的检查。脚本只读元数据,不会自动删除或修复链接。需要进一步检查普通数据文件是否改变时,可以接着使用 SHA-256 数据清单教程。

标签

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 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它