3. 配置文件管理
说实话,配置文件管理这件事,我早年吃过不少亏。
那时候我负责一个微服务项目的构建流水线,光环境就有开发、测试、预发、生产四套。每个环境的数据库地址、API 密钥、日志级别全都不一样。一开始我图省事,直接在代码里硬编码。结果呢?每次部署前都要手动改一遍,改漏一个就等着线上报错吧。那段时间我手机一响就心惊胆战。
后来我学乖了——把配置从代码里抽出来。这就是我们今天要聊的核心:用 configparser 或 YAML 来管理构建配置。
3.1 为什么需要配置文件管理?
你想想看,一个项目从开发到上线,要经历多少环境?少说三四个。每个环境的参数都不一样。如果每次部署都去改代码,那跟刀耕火种有什么区别?
配置文件的好处很明显:
- 环境隔离:不同环境用不同配置文件,互不干扰
- 安全可控:敏感信息(密码、密钥)可以单独管理,不混在代码里
- 易于维护:改配置不用改代码,改完重启服务就行
- 版本管理:配置文件可以进 Git,变更历史一目了然
核心原则:代码与配置分离。代码是逻辑,配置是数据。两者混在一起,迟早出事。
3.2 Python 内置方案:configparser
Python 自带的 configparser 模块,用来处理 INI 格式的配置文件。说实话,它功能不算强大,但胜在简单、零依赖。我个人习惯在小型项目或快速原型里用它。
3.2.1 INI 文件格式长什么样?
; build_config.ini
[build]
python_version = 3.10
output_dir = ./dist
clean_before_build = true
[deploy]
server = 192.168.1.100
port = 8080
username = deployer
; 注意:密码不要硬编码在配置文件里
password = ${DEPLOY_PASSWORD}
[logging]
level = INFO
file = ./logs/build.log
看到那个 ${DEPLOY_PASSWORD} 了吗?这是环境变量引用。我建议敏感信息一律走环境变量,配置文件里只留个占位符。
3.2.2 用 configparser 读取配置
import configparser
import os
from string import Template
config = configparser.ConfigParser()
config.read('build_config.ini')
# 读取构建配置
python_version = config.get('build', 'python_version')
output_dir = config.get('build', 'output_dir')
clean_build = config.getboolean('build', 'clean_before_build')
# 读取部署配置,支持环境变量替换
deploy_server = config.get('deploy', 'server')
deploy_port = config.getint('deploy', 'port')
deploy_user = config.get('deploy', 'username')
# 处理环境变量占位符
raw_password = config.get('deploy', 'password')
template = Template(raw_password)
deploy_password = template.safe_substitute(os.environ)
print(f"构建版本: {python_version}")
print(f"部署目标: {deploy_server}:{deploy_port}")
小技巧:config.getboolean() 可以直接把字符串 'true'/'false' 转成布尔值,省得自己写 if 判断。这个细节我当年找了半天文档才发现。
3.3 现代方案:YAML
YAML 现在基本是 DevOps 领域的标配了。Docker Compose、Kubernetes、Ansible、GitHub Actions……全在用 YAML。为什么?因为它比 INI 更灵活,比 JSON 更可读。
我个人更倾向 YAML,尤其是项目规模大了以后。INI 的层级结构有限,YAML 可以轻松表达嵌套关系。
3.3.1 YAML 配置文件示例
# build_config.yaml
project:
name: my-app
version: 2.1.0
build:
python_version: "3.10"
output_dir: ./dist
clean_before_build: true
dependencies:
- requests
- flask
- pyyaml
deploy:
server: 192.168.1.100
port: 8080
username: deployer
password: ${DEPLOY_PASSWORD}
retry:
max_attempts: 3
delay_seconds: 5
logging:
level: INFO
file: ./logs/build.log
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
environments:
dev:
server: dev.example.com
port: 8080
staging:
server: staging.example.com
port: 8080
production:
server: prod.example.com
port: 443
你看,YAML 支持列表、嵌套字典、多环境定义。INI 要做到这些,得写一堆 section 和奇怪的命名规则。
3.3.2 用 Python 读取 YAML
import yaml
import os
from string import Template
with open('build_config.yaml', 'r') as f:
config = yaml.safe_load(f)
# 读取构建配置
project_name = config['project']['name']
python_version = config['build']['python_version']
dependencies = config['build']['dependencies']
# 读取部署配置
deploy_config = config['deploy']
deploy_server = deploy_config['server']
deploy_port = deploy_config['port']
# 环境变量替换
raw_password = deploy_config['password']
template = Template(raw_password)
deploy_password = template.safe_substitute(os.environ)
# 读取多环境配置
env = os.getenv('APP_ENV', 'dev')
env_config = config['environments'].get(env, config['environments']['dev'])
print(f"项目: {project_name} v{config['project']['version']}")
print(f"当前环境: {env}")
print(f"目标服务器: {env_config['server']}:{env_config['port']}")
注意:yaml.safe_load() 和 yaml.load() 有区别。safe_load 只解析标准 YAML 标签,不会执行任意 Python 对象。我曾经见过有人用 yaml.load 加载不可信的配置文件,结果被注入了恶意代码。记住:永远用 safe_load。
3.4 两种方案的对比
| 特性 | configparser (INI) | YAML |
|---|---|---|
| 依赖 | Python 内置,零依赖 | 需要安装 PyYAML |
| 可读性 | 简单直观,适合扁平配置 | 支持嵌套,复杂结构更清晰 |
| 数据类型 | 所有值都是字符串,需手动转换 | 原生支持数字、布尔、列表、字典 |
| 层级深度 | 有限(通过 section 模拟) | 无限嵌套 |
| 注释支持 | 支持 ; 和 # 注释 | 支持 # 注释 |
| 适用场景 | 小型项目、快速原型 | 中大型项目、微服务、CI/CD |
3.5 实战:构建一个配置管理类
光会读配置还不够,得把它封装好。我在项目中一般会写一个配置管理类,统一处理加载、校验、环境变量替换。
import os
import yaml
from string import Template
from typing import Any, Dict
class BuildConfig:
"""统一的构建配置管理类"""
def __init__(self, config_path: str):
self.config_path = config_path
self._raw_config: Dict[str, Any] = {}
self._load_config()
self._resolve_env_vars()
def _load_config(self):
"""加载 YAML 配置文件"""
if not os.path.exists(self.config_path):
raise FileNotFoundError(f"配置文件不存在: {self.config_path}")
with open(self.config_path, 'r') as f:
self._raw_config = yaml.safe_load(f)
def _resolve_env_vars(self):
"""递归替换所有 ${VAR} 占位符为环境变量"""
def _resolve(obj):
if isinstance(obj, str):
template = Template(obj)
return template.safe_substitute(os.environ)
elif isinstance(obj, dict):
return {k: _resolve(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [_resolve(item) for item in obj]
return obj
self._raw_config = _resolve(self._raw_config)
def get(self, *keys, default=None):
"""安全获取配置值,支持多级 key"""
value = self._raw_config
for key in keys:
if isinstance(value, dict):
value = value.get(key)
if value is None:
return default
else:
return default
return value
def get_env_config(self, env: str = None):
"""获取指定环境的配置"""
env = env or os.getenv('APP_ENV', 'dev')
return self.get('environments', env, default={})
# 使用示例
config = BuildConfig('build_config.yaml')
print(config.get('project', 'name')) # my-app
print(config.get('build', 'python_version')) # 3.10
print(config.get_env_config('production')) # 生产环境配置
我的习惯:配置管理类一定要做两件事——校验和默认值。校验保证配置不缺项,默认值保证可选配置不报错。少了这两样,线上出问题你都不知道是代码的锅还是配置的锅。
3.6 避坑指南
- 我曾经把数据库密码直接写在 YAML 里,还 push 到了 GitHub 公开仓库。十分钟后收到安全警报,吓得我赶紧改密码、清历史、加 .gitignore。从那以后,敏感信息一律走环境变量或密钥管理服务。
- 编码问题:YAML 文件默认 UTF-8,但 Windows 上有些编辑器会存成 GBK。读取时指定
encoding='utf-8'可以避免乱码。 - 不要用 Tab 缩进:YAML 强制用空格缩进。Tab 会导致解析错误,而且这个错误信息特别隐晦。我建议编辑器里设置「将 Tab 转换为空格」。
- 配置版本化:配置文件一定要进 Git。但注意区分「模板」和「实际值」。模板文件(如
config.yaml.template)可以提交,实际值文件(含密码的)要加.gitignore。
3.7 小结
配置文件管理,说白了就是把变化的部分从代码里拆出来。configparser 适合小项目,YAML 适合大项目。我个人建议:新项目直接用 YAML,省得以后迁移麻烦。