Python configparser 实战:校验 INI 配置,避开百分号与布尔值陷阱
任务名里出现 100%,读取 INI 时却报 InterpolationSyntaxError;配置写着 enabled = false,脚本仍然进入执行分支。小型自动化任务也需要明确的配置读取规则,而不是把字符串直接交给业务代码。
本文面向初级到中级读者,用 Python 标准库写一个只读 INI 配置检查器:保留字面百分号,转换布尔值和整数,拒绝重复键、拼写错误与缺失文件。它只打印检查结果,不启动任务、不修改调度器或系统配置。
1. 方法:先定义配置规则
已有 TOML 配置的项目可以继续使用 tomllib 任务配置方案。这里专门处理仍在使用 INI 的小脚本,不把 INI 当成 TOML,也不自动迁移格式。
本例约定:
- 只读取一个必需的 UTF-8 文件,只允许一个
[task]节。节名区分大小写;选项名按默认规则不区分大小写。 name、enabled、interval都必须显式提供,未知选项报错。禁止[DEFAULT]中的选项,避免隐式继承掩盖缺失字段;空的[DEFAULT]不影响检查。name去掉首尾空白后必须非空且为单行,%按普通字符处理。enabled使用getboolean(),不要使用bool("false")。interval使用getint()转换,并限制在 1–86400 秒。通过检查不表示任务已启用:JSON 中的enabled才是业务代码应读取的布尔值。
Python 官方插值说明介绍了默认的 % 插值。这里通过 interpolation=None 关闭它,因此 %(other)s 也只是字面文本,不会替换变量。
2. 准备:建立独立演示目录
使用现有 Python 3.9 或更新版本,无需安装第三方包。命令在 Linux、Python 3.11.4 下验证,可在 bash 或 zsh 中运行。
- 在同一个终端执行以下命令。重复实验时换一个新目录名,避免覆盖旧的演示文件:
python3 --version
mkdir ini-task-demo
cd ini-task-demo
cat > task.ini <<'INI'
[task]
name = nightly-100%
enabled = false
interval = 60
INI
- 将下一节完整代码保存为当前目录下的
read_task.py。示例文件不包含密码;真实项目不要把带凭据的配置输出到日志。
3. 实现:必需文件、严格解析与类型转换
"""Validate one small local INI task file without running the task. Python 3.9+."""
import argparse
import configparser
import json
from pathlib import Path
import sys
def load_task(path):
parser = configparser.ConfigParser(interpolation=None, strict=True)
with path.open(encoding="utf-8") as stream:
parser.read_file(stream)
if parser.defaults():
raise ValueError("DEFAULT values are not allowed")
if parser.sections() != ["task"]:
raise ValueError("expected exactly one [task] section")
section = parser["task"]
required = {"name", "enabled", "interval"}
unknown = set(section) - required
missing = required - set(section)
if unknown or missing:
raise ValueError(
f"unknown keys: {sorted(unknown)}; missing keys: {sorted(missing)}"
)
name = section["name"].strip()
if not name or "\n" in name or "\r" in name:
raise ValueError("name must be nonempty and single-line")
enabled = section.getboolean("enabled")
interval = section.getint("interval")
if not 1 <= interval <= 86400:
raise ValueError("interval must be between 1 and 86400 seconds")
return {"name": name, "enabled": enabled, "interval": interval}
def main():
cli = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
cli.add_argument("config", type=Path, help="UTF-8 INI file to validate")
args = cli.parse_args()
try:
task = load_task(args.config)
except (OSError, UnicodeError, configparser.Error, ValueError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
print(json.dumps(task, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
必需文件用 open() 后调用 read_file(),缺失文件会进入失败分支。不要把必需文件改为 parser.read() 后忽略返回值:该方法会跳过无法打开的文件。
strict=True 拒绝同一输入源中的重复节和重复选项。代码不合并多个文件,也不通过后读配置覆盖前读配置。先拒绝默认值,再检查字段集合,避免继承值被误当成显式字段。
getboolean() 官方说明列出了可接受的值:yes/no、on/off、true/false、1/0,不区分大小写。整数转换成功后仍要检查范围;这些类型检查不会自动执行你的任务。
4. 运行:核对输出与退出码
- 查看帮助并检查配置:
python3 read_task.py --help
python3 read_task.py task.ini
期望输出:
{"name": "nightly-100%", "enabled": false, "interval": 60}
- 用断言确认
%没被插值,false不是字符串,间隔确实是整数:
python3 - <<'PY_CHECK'
import json
import subprocess
import sys
result = subprocess.run(
[sys.executable, "read_task.py", "task.ini"],
capture_output=True, text=True, check=False,
)
assert result.returncode == 0, result.stderr
assert result.stderr == ""
assert json.loads(result.stdout) == {
"name": "nightly-100%", "enabled": False, "interval": 60,
}
print("PASS: literal percent, boolean false, integer interval")
PY_CHECK
成功返回 0;文件、解析或字段错误返回 1,错误写入 stderr,stdout 留空;命令行参数错误返回 2。enabled = false 是合法配置,因此仍返回 0;调用方必须根据已解析的布尔值决定是否执行。
若任务还使用环境变量,先阅读 Python 环境变量校验,明确配置来源和优先级,不要直接把环境字符串的真假当成布尔值。
5. 验证:故意制造错误配置
下面的测试只创建当前演示目录内的错误副本,不改动 task.ini。相同名字的测试副本会被覆盖:
python3 - <<'PY_TEST'
from pathlib import Path
import subprocess
import sys
base = Path("task.ini").read_text(encoding="utf-8")
cases = [
("duplicate.ini", base + "enabled = true\n", "already exists"),
("typo.ini", base.replace("interval =", "intervl ="), "unknown keys"),
("boolean.ini", base.replace("false", "maybe"), "Not a boolean"),
("range.ini", base.replace("interval = 60", "interval = 0"), "between 1"),
("defaults.ini", "[DEFAULT]\nenabled = true\n" + base, "DEFAULT values"),
("missing-key.ini", base.replace("enabled = false\n", ""), "missing keys"),
]
for name, text, message in cases:
path = Path(name)
path.write_text(text, encoding="utf-8")
before = path.read_bytes()
result = subprocess.run(
[sys.executable, "read_task.py", name],
capture_output=True, text=True, check=False,
)
assert result.returncode == 1, result
assert result.stdout == "", result.stdout
assert message in result.stderr, result.stderr
assert path.read_bytes() == before
print(f"PASS: {name}")
missing = Path("not-created.ini")
assert not missing.exists()
result = subprocess.run(
[sys.executable, "read_task.py", str(missing)],
capture_output=True, text=True, check=False,
)
assert result.returncode == 1 and not result.stdout and result.stderr
assert not missing.exists()
print("PASS: required file missing")
PY_TEST
应看到七行 PASS。测试同时确认失败时没有成功报告,错误副本内容保持不变,缺失文件也不会被创建。除了文中案例,还应在接入业务前测试空文件、错误编码、重复节和区间边界。
6. 排障:从报错定位具体修复
| 现象 | 原因与修复 |
|---|---|
% 引发 InterpolationSyntaxError |
实际创建的仍是默认解析器;确认代码使用 ConfigParser(interpolation=None, strict=True)。不要在该字面模式下把 % 改成 %%,否则会保留两个百分号。 |
Not a boolean |
值不是约定的布尔写法;改成独立一行的 enabled = false,不要写 enabled = false # disabled。本例不启用行尾注释。 |
already exists |
同一个文件存在重复项,包括 enabled 与 Enabled;保留一处明确值,不要用 strict=False 掩盖歧义。 |
DEFAULT values are not allowed |
把 [DEFAULT] 中需要的选项移到 [task],然后移除默认选项。 |
| 找不到配置文件 | 当前目录不对或路径写错;传入配置的绝对路径。调度环境的工作目录问题见 Linux 定时任务 PATH 与 cwd 排查。 |
missing keys 或 unknown keys |
对照 name、enabled、interval 修正缺失或拼写错误,而不是添加静默回退。 |
INI 不是 shell:本例不会展开 $HOME 或执行命令,包在引号中的值也不会自动去掉引号。注释放在单独一行。该工具面向自己管理的小型本地配置,不提供不可信大文件的资源隔离。
7. 总结
INI 读取成功不代表任务配置有效:文件存在、字段完整、类型正确与范围合理都应在业务执行前确认。字面模式解决百分号插值问题,严格解析则暴露重复项与拼写错误。先用有效配置和失败副本验证退出码,再把 load_task() 接入真正的自动化任务。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-configparser-ini-validation.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。