Python 任务配置实战:用 tomllib 读取 TOML、校验参数与固定路径
自动化脚本换一批输入就要改源码,命令行参数越来越长;到了定时任务,配置里的相对路径又找不到文件。把任务设置放进 TOML,再在入口统一校验,可以让同一份代码服务多个任务。
本文面向初学者到中级读者,用 Python 标准库写一个完整的输入预览工具:读取配置、检查类型、允许命令行覆盖行数,并验证换工作目录后仍能找到数据。示例只读取文件并输出 JSON。
1. 方法:先校验配置,再运行任务
Python 的 tomllib 官方文档说明,该模块从 Python 3.11 开始提供,负责解析 TOML,不能写入 TOML。tomllib.load() 需要以二进制方式打开文件,返回 Python 字典;格式不合法时会抛出 TOMLDecodeError。
解析成功不等于任务参数有效。 本文自行定义以下规则:
- 顶层只允许
[task],表内只允许input和limit,拼错字段直接报错。 input必须是非空字符串。相对路径以配置文件所在目录为基准。limit必须是 1–1000 的整数;没写时默认 3。不能用字符串、浮点数或布尔值代替。- 优先级为默认值、配置文件、显式
--limit。先检查整份配置,即使命令行覆盖了limit,配置中的非法值也会报错。
输入预览按文本行读取,空行也算一行。它适合检查任务入口;真正的 CSV 处理可以接入流式处理大型 CSV中的记录解析逻辑。
2. 准备环境和测试数据
需要已有的 Python 3.11 或更新版本,不安装第三方包。本文在 Linux、Python 3.11.4 上验证。
- 在 bash 或 zsh 中运行以下命令。重复实验时用新的目录名,后续命令保持在同一个终端:
python3 --version
python3 -c 'import tomllib; print("tomllib ready")'
mkdir tomllib-demo
cd tomllib-demo
task_demo_root="$PWD"
mkdir configs data
printf 'alpha\nbeta\ngamma\n' > data/items.txt
cat > configs/task.toml <<'EOF'
[task]
input = "../data/items.txt"
limit = 2
EOF
- TOML 的
[task]会形成一个嵌套字典。这里的../data/items.txt从configs/目录计算,因此指向演示数据;limit = 2是整数,limit = "2"是字符串。 - 把下一节代码保存成当前目录的
preview_task.py,也可以下载 preview_task.py。
如果需要把带第三方依赖的工具发给别人,可继续参考 uv 单文件脚本教程。本文脚本无需依赖声明。
3. 完整实现:读取、校验和覆盖
"""Preview UTF-8 input using validated TOML settings. Python 3.11+."""
import argparse
from itertools import islice
import json
from pathlib import Path
import sys
import tomllib
def valid_limit(value):
if type(value) is not int or not 1 <= value <= 1000:
raise ValueError("limit must be an integer from 1 to 1000")
return value
def cli_limit(value):
try:
return valid_limit(int(value))
except ValueError as exc:
raise argparse.ArgumentTypeError(str(exc)) from exc
def load_settings(config):
config = config.expanduser().resolve(strict=True)
with config.open("rb") as file:
data = tomllib.load(file)
if set(data) != {"task"}:
raise ValueError("top level must contain only [task]")
task = data["task"]
if not isinstance(task, dict):
raise ValueError("task must be a table")
unknown = set(task) - {"input", "limit"}
if unknown:
raise ValueError("unknown task keys: " + ", ".join(sorted(unknown)))
raw_input = task.get("input")
if not isinstance(raw_input, str) or not raw_input.strip():
raise ValueError("task.input must be a nonempty string")
limit = valid_limit(task.get("limit", 3))
input_path = Path(raw_input).expanduser()
if not input_path.is_absolute():
input_path = config.parent / input_path
return config, input_path.resolve(strict=True), limit
def main():
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument("--config", required=True, type=Path, help="TOML file")
parser.add_argument("--limit", type=cli_limit, help="override preview line limit")
args = parser.parse_args()
try:
config, input_path, limit = load_settings(args.config)
if args.limit is not None:
limit = args.limit
with input_path.open(encoding="utf-8") as file:
lines = [line.rstrip("\r\n") for line in islice(file, limit)]
print(json.dumps({
"config": str(config),
"input": str(input_path),
"limit": limit,
"lines": lines,
}, ensure_ascii=False, indent=2))
except (OSError, ValueError, RuntimeError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
valid_limit() 用 type(value) is int 严格检查。Python 中 bool 是 int 的子类,所以只用 isinstance(value, int) 会把 true 当成整数接受。
argparse 的 type 参数让命令行字符串经过 cli_limit() 校验。这里把未提供的 --limit 保留为 None,避免一个命令行默认值意外盖掉文件中的设置。
路径处理使用 Path.resolve()。它会解析符号链接,因此基准目录是配置文件的实际位置;符号链接旁边的相对目录不会作为基准。代码支持 ~,不替换 TOML 字符串里的 $变量。
4. 运行并检查输出
- 查看帮助、使用文件设置,再临时覆盖行数:
python3 preview_task.py --help
python3 preview_task.py --config configs/task.toml
python3 preview_task.py --config configs/task.toml --limit 1
第二条命令输出的 limit 是 2,lines 是 ["alpha", "beta"];第三条是 1 和 ["alpha"]。config 与 input 都显示绝对路径,方便检查任务实际使用了哪个文件。
- 用断言检查结果:
python3 preview_task.py --config configs/task.toml > preview.json
python3 - <<'PY_CHECK'
import json
from pathlib import Path
result = json.loads(Path("preview.json").read_text(encoding="utf-8"))
assert result["limit"] == 2
assert result["lines"] == ["alpha", "beta"]
assert Path(result["input"]) == Path("data/items.txt").resolve()
print("OK: settings and input path")
PY_CHECK
预期输出 OK: settings and input path。如果输入少于指定行数,返回已有行;空文件返回空列表。limit 表示请求上限,实际读取行数是 len(lines)。
程序只限制行数,会把选中的行保存在内存中;单行特别长时仍会占用较多内存。把它作为小型输入预览,处理大型记录时改用逐条解析。
5. 验证换目录后仍可运行
在同一个终端执行:
(
cd /
python3 "$task_demo_root/preview_task.py" \
--config "$task_demo_root/configs/task.toml" --limit 1
)
虽然子 shell 的工作目录变成 /,输出仍应是 limit = 1,lines = ["alpha"]。--config 的绝对路径负责找到配置,配置内的相对路径负责找到数据。
接入定时任务时,使用已有项目解释器、脚本和配置的绝对路径。如果任务还依赖其他文件或环境变量,按 Linux 定时任务路径排查逐项确认。本文实验无需修改 crontab。
6. 验证错误退出
创建一份 TOML 语法正确、业务类型错误的配置,再测试非法命令行参数:
cat > configs/bad.toml <<'EOF'
[task]
input = "../data/items.txt"
limit = true
EOF
python3 preview_task.py --config configs/bad.toml
printf 'exit=%s\n' "$?"
python3 preview_task.py --config configs/task.toml --limit 0
printf 'exit=%s\n' "$?"
第一条任务命令应输出 ERROR: limit must be an integer from 1 to 1000,随后显示 exit=1。第二条显示 argparse 的用法提示和参数错误,随后是 exit=2;正常任务返回 0。
验证时还检查了默认值、未知字段、缺少输入、非法 TOML、浮点数和字符串行数、缺失文件、Unicode 输入以及配置文件符号链接。代码先校验配置,再读取任务输入;没有通过校验就不会产生成功 JSON。
7. 常见问题与修复
| 现象 | 具体修复 |
|---|---|
No module named 'tomllib' |
用 python3 --version 检查,改用已有的 Python 3.11+ 解释器;不要执行 pip install tomllib。 |
| TOML 解析失败 | 检查引号、重复字段和表声明,修复错误提示所指的配置内容。 |
unknown task keys |
将拼错的字段改成 input 或 limit,新增参数时同步修改允许列表和校验。 |
limit must be an integer |
在文件里写 limit = 2,在命令行写 --limit 2;不要写 true、"2" 或 0。 |
No such file or directory |
先检查传入的配置路径,再按配置实际所在目录计算 input。 |
Permission denied 或 UTF-8 解码失败 |
使用当前用户可读的普通 UTF-8 文本文件,并检查文件和父目录权限。 |
总结
把任务设置放到 TOML,代码负责类型、范围和字段校验,再允许少量命令行覆盖。固定相对路径的基准目录,并用成功与失败两类实验确认行为,这套入口就能用于日常自动化脚本。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-tomllib-task-config.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。