3. INI 配置格式:老兵不死,也不会凋零
说起 INI 格式,很多人觉得它「过时了」。说实话,INI 确实是配置格式里的「小透明」,但千万别小看它。我在项目中遇到过不少场景,用 JSON 太啰嗦,用 YAML 又太重,反而是 INI 一把梭搞定。
3.1 先看长相
INI 的结构很简单:就是一个扁平的键值对集合,用节来分组。嗯,咱们直接看例子比较直观。
; database.ini
; 数据库主配置
[database]
host = 127.0.0.1
port = 5432
name = myapp_prod
user = admin
password = ${DB_PASSWORD}
[pool]
min_connections = 5
max_connections = 20
timeout = 30
[logging]
level = INFO
file = /var/log/myapp/db.log
rotate = true
你盯着看两眼就能总结出规律:
- 节 用
[section]包裹,代表一个配置域 - 键值对 用
=或:分隔,左边是键,右边是值 - 注释 以
;或#开头,整行都是注释 - 空行 随意添加,解析器会忽略
说白了,这就是一个人类最直觉的配置文件写法——先分组,再给出具体数值。
3.2 节与键的命名规范
我刚开始写 INI 时,命名很随意,结果项目大了之后把自己坑了。现在我有自己的一套铁律:
| 元素 | 建议规则 | 错误示例 | 正确示例 |
|---|---|---|---|
| 节名 | 全小写+下划线 | [DatabaseConfig] |
[database] |
| 键名 | 蛇形命名,20 字符内 | maxConnectionCount = |
max_connections = |
| 值 | 字符串不加引号 | name = "myapp" |
name = myapp |
| 注释 | 用 ;,对齐排版 |
# 这个注释太随意了 |
; 数据库连接池配置 |
为什么要规定这么细?我给你说个教训。以前有个同事在一个环境里同时用 [database] 和 [DATABASE],结果两根配置互相覆盖,线上数据库连接池直接被冲垮。Windows 的 INI 解析器大小写不敏感,但 Linux 下的解析器严格区分——这个差异当年让我加了一个通宵的班。
3.3 编写一个解析器示例
我个人习惯用 Python 的 configparser 来读 INI。为什么不是手写正则?你想想看,标准库已经帮你处理了编码、注释、默认值这些问题,何必重复造轮子。
import configparser
config = configparser.ConfigParser()
config.read('database.ini', encoding='utf-8')
# 读取数据库配置
host = config['database']['host']
port = config['database'].getint('port')
name = config['database']['name']
# 读取连接池配置
max_conn = config['pool'].getint('max_connections')
timeout = config['pool'].getint('timeout')
print(f'Connecting to {host}:{port}/{name}')
print(f'Pool: max {max_conn}, timeout {timeout}s')
这里的 getint() 方法特别好用——它帮你做类型转换。你不需要手动 int(config['pool']['timeout']) ,万一值写成了 thirty,你还能优雅地捕获异常。我在项目里就是这么做的,稳得很。
3.4 版本锁定的关键策略
好,现在配置写好了,怎么保证它不被人偷偷改掉?
有两个层次需要锁定:
- 文件本身锁定:谁在什么时间改了哪一行?
- 字段值锁定:某个键的取值必须在规定范围内。
我曾经在处理一个金融项目时,运维同学把 port 从 5432 改成了 3306,理由是「换个 MySQL 玩玩」。结果数据库直接启动不了。从那以后,我开始在 INI 文件里加模式校验。
; 版本锁定标记
[__meta__]
version = 3.2.1
locked_fields = database.host, database.port, database.name
; 以下配置只允许管理员修改
[database]
host = 127.0.0.1 ; LOCKED
port = 5432 ; LOCKED
name = myapp_prod ; LOCKED
然后在解析脚本里增加校验逻辑:
import configparser
from pathlib import Path
def validate_locked_fields(config_file):
cfg = configparser.ConfigParser()
cfg.read(config_file, encoding='utf-8')
locked_fields = cfg.get('__meta__', 'locked_fields', fallback='').split(',')
locked_fields = [f.strip() for f in locked_fields if f.strip()]
for field in locked_fields:
section, key = field.split('.')
original_value = cfg[section][key]
# 对比预期值(实际项目中从环境变量或版本库获取)
expected = get_expected_value(section, key)
if original_value != expected:
raise ValueError(f'Field {field} has been modified! '
f'Expected {expected}, got {original_value}')
def get_expected_value(section, key):
# 从 Git 或 CI 中获得基准值
import hashlib
return hashlib.sha256(f'{section}.{key}'.encode()).hexdigest()[:8]
我的建议:不要过度锁定。只有真正关键的配置才需要锁定,比如数据库连接、API 密钥、加密域名。日志级别、线程数这些让运维自由调整就好。
特别注意:INI 不原生支持数据类型和嵌套结构。如果你发现配置特别复杂(比如需要 JSON 嵌套),赶紧换成 TOML 或 YAML。别硬撑,我见过有人用 INI 写出一棵配置树,那代码读起来跟天书一样。
3.5 多文件合并与覆盖
现实的工程里,你不会只有一个 config.ini。一般是三层结构:
- 基础配置:
base.ini,通用默认值 - 环境配置:
production.ini/development.ini,特定环境的差异化 - 本地覆盖:
local.ini,不上传到版本库,只给个人调试用
读的顺序是:base -> environment -> local,后面的覆盖前面的。
import configparser
config = configparser.ConfigParser()
# 按顺序读取,后面的覆盖前面的
config.read(['base.ini', 'production.ini', 'local.ini'], encoding='utf-8')
# 这样就实现了分层覆盖
host = config['database']['host']
关键点:local.ini 一定不能提交到 Git。我建议直接在 .gitignore 里加上 local.ini,并在 local.ini.example 里给一个参考模板。团队成员各自改自己的,互不干扰。
3.6 INI 在 CI/CD 中的应用
我在做自动化部署时,经常用 INI 来统一管理环境变量。配合 sed 命令可以快速替换值:
# 在 CI 脚本中替换数据库密码
sed -i "s/password = .*/password = ${NEW_DB_PASSWORD}/" config.ini
配合 git diff 可以在部署前检查哪些字段被改过:
#!/bin/bash
# 检查配置文件差异
git diff --name-only HEAD~1 | grep '\.ini$' && {
echo "检测到 INI 文件变动,执行锁定校验..."
python validate_ini.py
}
这样一来,每次部署都会自动检查被锁定的字段有没有被改动。我曾经就因为没做这个检查,把测试环境的数据库地址误改成生产环境地址——整个测试环境直接挂掉。嗯,从那以后我再也不敢省略校验了。
3.7 一张图总结 INI 配置体系
3.8 INI 与其他格式的对比
| 特性 | INI | JSON | YAML | TOML |
|---|---|---|---|---|
| 人类可读性 | ★★★★★ | ★★★☆☆ | ★★★★☆ | ★★★★★ |
| 嵌套支持 | ★☆☆☆☆ | ★★★★★ | ★★★★★ | ★★★★☆ |
| 数据类型 | 仅字符串 | 丰富 | 丰富 | 丰富 |
| 注释支持 | ★★★★★ | ★☆☆☆☆ | ★★★★★ | ★★★★★ |
| 版本锁定难度 | 简单 | 中等 | 较复杂 | 简单 |
| 生态成熟度 | ★★★★★ | ★★★★★ | ★★★★☆ | ★★★☆☆ |
你看这个表就明白为什么我还在用 INI。它简单、直接、易校验,尤其适合设备配置、CI 环境变量、脚本参数这些场景。嵌套多了?换 TOML。需要复杂类型?换 JSON。配置文件如做人,知道自己在哪能用在哪该收,才是真本事。