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。一般是三层结构:

  1. 基础配置:base.ini,通用默认值
  2. 环境配置:production.ini / development.ini,特定环境的差异化
  3. 本地覆盖: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 配置体系

INI 配置与版本锁定核心流程 base.ini production.ini local.ini 分层合并读取 base → env → local 字段锁定校验 匹配预期值 生效配置 最终 config 对象 常见锁定字段 database.host database.port database.name

3.8 INI 与其他格式的对比

特性 INI JSON YAML TOML
人类可读性 ★★★★★ ★★★☆☆ ★★★★☆ ★★★★★
嵌套支持 ★☆☆☆☆ ★★★★★ ★★★★★ ★★★★☆
数据类型 仅字符串 丰富 丰富 丰富
注释支持 ★★★★★ ★☆☆☆☆ ★★★★★ ★★★★★
版本锁定难度 简单 中等 较复杂 简单
生态成熟度 ★★★★★ ★★★★★ ★★★★☆ ★★★☆☆

你看这个表就明白为什么我还在用 INI。它简单、直接、易校验,尤其适合设备配置、CI 环境变量、脚本参数这些场景。嵌套多了?换 TOML。需要复杂类型?换 JSON。配置文件如做人,知道自己在哪能用在哪该收,才是真本事。