同一份 Python 脚本,在项目目录手动运行正常,交给定时任务或服务却报 FileNotFoundError;更隐蔽的是不报错,却读到了另一份同名配置。本文用普通磁盘脚本说明当前工作目录与脚本目录的区别,给出路径探针、完整读取代码和可复制的隔离实验。
一、先查三个位置,不要只盯文件名
报错中的 config.json 往往只是相对路径。它没有携带“项目目录”的约定,读取时要依赖进程当前工作目录,也就是 cwd。启动脚本的 Shell、任务执行器或服务配置可能给出不同 cwd;不能凭“定时任务”四个字就断言它一定在哪个目录启动。
把下面代码保存成普通 .py 文件,用实际启动方式运行。三个输出分别回答:进程现在站哪儿、脚本放哪儿、这次相对配置路径指向哪儿。记录脱敏后的路径信息;不要打印整份配置。
from pathlib import Path
print("当前工作目录:", Path.cwd())
print("脚本文件:", Path(__file__).resolve())
print("相对路径实际指向:", Path("config.json").resolve())
如果第三行不在预期目录,优先修路径约定。把脚本写成绝对路径只是在定位脚本,不会自动把进程 cwd 切到脚本旁边;调整 Python 导入路径同样不会替普通文件读取选择基准。
二、Path 对象不是文件,也不是目录书签
Path("config.json") 保存的是路径描述,不会在构造时读取文件,也不会把当时 cwd 永久绑定进去。同一个相对 Path 对象,如果先后在不同 cwd 下执行 read_text,可能读到不同文件。目录里恰好都有同名配置时,“运行成功”反而最容易让人放松警惕。
absolute() 把相对路径变成绝对路径,但不解析符号链接;resolve() 还会解析符号链接并消除 ..。两者都不是“自动找项目根目录”。对相对路径调用 resolve,采用的是调用当时的工作目录,而不是代码所在目录。
resolve(strict=False) 不保证目标存在;strict=True 会在缺失路径等错误下抛异常。即使解析成功,也没有证明文件格式正确、字段合规、后续业务成功,更不是抵御文件系统竞态的安全边界。

三、先给文件分类,再决定怎么拼路径
脚本随附的固定配置,可以从脚本目录拼接;部署者维护的配置,更适合显式传入绝对路径;命令行用户主动指定的输入文件,也可以约定相对调用者 cwd。三者没有谁天然高级,关键是约定明确、出错可诊断,不要一处按脚本算,另一处又偷偷按 cwd 算。
下面采用“默认脚本旁 config.json,APP_CONFIG 覆盖时只接受绝对路径”的约定。把代码保存为读取配置.py,脚本旁准备 UTF-8 文件,内容可用 {"名称":"演示任务"}。默认分支能直接运行;覆盖分支用你的启动器设置 APP_CONFIG。没有设置与设置为空串是两件事,空串不能悄悄降级成默认值。
四、完整读取示例:定位之后还要验内容
import json
import os
from pathlib import Path
# 约定:默认配置跟随真实脚本目录;覆盖值必须是绝对路径
脚本目录 = Path(__file__).resolve().parent
覆盖值 = os.environ.get("APP_CONFIG")
if 覆盖值 is None:
配置路径 = 脚本目录 / "config.json"
else:
if not 覆盖值.strip():
raise ValueError("APP_CONFIG 不能是空串或全空白")
配置路径 = Path(覆盖值).expanduser()
if not 配置路径.is_absolute():
raise ValueError("APP_CONFIG 必须是绝对路径")
配置路径 = 配置路径.resolve(strict=True)
if not 配置路径.is_file():
raise ValueError("配置路径必须指向普通文件")
配置 = json.loads(配置路径.read_text(encoding="utf-8"))
if not isinstance(配置, dict):
raise ValueError("配置必须是 JSON 对象")
名称 = 配置.get("名称")
if not isinstance(名称, str) or not 名称.strip():
raise ValueError("名称必须是非空字符串")
print("配置校验通过")
expanduser() 是主动支持 ~/ 开头的家目录写法;Path 构造本身不会展开 ~,也不会展开 $HOME 这样的环境变量。绝对路径判断使用当前系统的路径风格:Linux 上不能直接拿 Windows 的盘符路径来充当可访问文件。示例不截去路径首尾空格,避免擅改合法文件名。
本文代码以 CPython 3.14.3 在 Linux 隔离目录执行;read_text 从 Python 3.5 提供,resolve 的 strict 参数从 3.6 提供,不能把后者门槛说成全部 pathlib 功能的门槛。生产环境应使用受支持的版本。
五、换两个 cwd,复现“同名文件读错”
下面是可独立执行的最小实验,不需要下载内部测试文件。它只创建临时目录,分别从“应用”和“其他目录”启动同一个脚本;每次先相对读取,再从脚本目录读取。正常输出里,前者从应用配置变成其他配置,后者两次都是应用配置。
import subprocess
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
# 只在临时目录制造两个同名文件,不动现有项目
with TemporaryDirectory() as 临时目录:
根 = Path(临时目录)
应用 = 根 / "应用"
其他 = 根 / "其他目录"
应用.mkdir()
其他.mkdir()
(应用 / "config.txt").write_text("应用配置", encoding="utf-8")
(其他 / "config.txt").write_text("其他配置", encoding="utf-8")
脚本 = 应用 / "probe.py"
脚本.write_text(
'from pathlib import Path\n'
'print(Path("config.txt").read_text(encoding="utf-8"))\n'
'print((Path(__file__).resolve().parent / "config.txt")'
'.read_text(encoding="utf-8"))\n', encoding="utf-8")
for 工作目录 in (应用, 其他):
回 = subprocess.run([sys.executable, str(脚本)], cwd=工作目录,
capture_output=True, text=True, check=True,
encoding="utf-8")
print(工作目录.name, 回.stdout.splitlines())
这比单独打印 exists() 更有说服力:两个文件都存在,但读到的内容不同。实验只证明普通子进程和文件路径行为,没有运行真实 cron、改 systemd 服务或验证你的部署权限;上线时还要用真实服务账号和启动环境补做验收。
六、符号链接和包资源,要单独选约定
Path(__file__).resolve().parent 选的是解析符号链接后的真实脚本目录。若从某个入口链接启动,它不会把入口链接所在目录当默认配置目录。发布目录切换、共享配置、热更新各有约定,不能用一句“永远这样写”盖过去。本文选择真实脚本旁配置,部署配置则通过绝对覆盖值指定。
__file__ 不是所有运行环境的统一入口:交互执行、部分加载器或打包方式可能没有可用的普通磁盘脚本路径。包内资源宜评估 importlib.resources;外置部署配置依旧适合显式路径,不要猜包一定是展开的目录。也别在库函数里随手 os.chdir,它会改变进程 cwd,影响其他相对文件操作。
七、常见失败分支与处理表
| 现象 | 核对点 | 处理 |
|---|---|---|
| 只在另一启动方式报缺文件 | cwd 与相对路径实际目标 | 明确脚本目录或绝对覆盖路径 |
| 不报错但读错配置 | 同名文件内容、来源约定 | 核对选中的文件,不只看存在性 |
| APP_CONFIG 被拒绝 | 空白、相对路径、系统路径风格 | 给当前系统可访问的绝对路径 |
| 解析成功却不能读 | 目录、权限、编码、JSON 格式 | 保留异常类型,按失败层处理 |
| 链接入口读取了真实目录配置 | resolve 的符号链接语义 | 显式配置部署路径,不猜链接位置 |
路径拼接还存在一个容易漏看的边界:基准 / 绝对路径 时,右侧绝对路径会覆盖前面的基准。固定文件名与不可信用户输入不能混为一谈;即使先检查 is_absolute,也不能因此声称解决了目录穿越或符号链接攻击。本文的配置覆盖值来自受信任部署者,不提供文件访问沙箱。
八、上线验收清单与资料
- 记录脱敏后的 cwd、脚本来源和选中配置路径,核对路径约定。
- 从至少两个 cwd 启动同一脚本,检查默认配置是否稳定。
- 覆盖值分别测试未设置、合法绝对路径、相对路径、空串和缺失文件。
- 测试目录、非法 JSON、错误字段与编码失败,不能吞异常伪装成功。
- 用真实运行账号验读权限、业务结果及退出码;路径探针不能代替业务验收。
本地隔离检查还覆盖了符号链接入口、相对 Path 在切换 cwd 后的行为及绝对右侧覆盖基准。这里展示的是路径与配置读取边界,不是实际定时任务上线回执。核心只有一句:脚本位置、工作目录和配置归属是三个概念,先说清楚,再让代码执行。
🔕 评论已关闭