🐍 第11课:JSON/YAML

—— 配置文件的黄金搭档

🏆 配置文件解析+生成
✅ Python验证通过

📌 本课目标

1️⃣ JSON 高级操作

import json

# 美化输出
data = {"name": "张三", "skills": ["Python", "Go"]}
pretty = json.dumps(data, ensure_ascii=False, indent=2)
print(pretty)

# 自定义序列化
from datetime import datetime, date
from decimal import Decimal
from pathlib import Path

class SmartEncoder(json.JSONEncoder):
    """智能 JSON 编码器"""
    def default(self, obj):
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()
        if isinstance(obj, Decimal):
            return float(obj)
        if isinstance(obj, Path):
            return str(obj)
        if isinstance(obj, set):
            return sorted(list(obj))
        if isinstance(obj, bytes):
            return obj.decode("utf-8")
        return super().default(obj)

# 使用
data = {
    "created": datetime.now(),
    "price": Decimal("99.99"),
    "tags": {"python", "go"},
    "path": Path("/tmp/test"),
}
encoded = json.dumps(data, cls=SmartEncoder, ensure_ascii=False, indent=2)

# 自定义反序列化
def smart_decode(d):
    """自动转换 ISO 日期字符串"""
    for k, v in d.items():
        if isinstance(v, str):
            try:
                d[k] = datetime.fromisoformat(v)
            except ValueError:
                pass
    return d

2️⃣ YAML 读写

import yaml  # pip install pyyaml

# YAML 字符串 → Python
yaml_str = """
database:
  host: localhost
  port: 3306
  name: mydb
  credentials:
    username: admin
    password: secret

logging:
  level: INFO
  handlers:
    - console
    - file

features:
  cache: true
  retry: 3
"""

config = yaml.safe_load(yaml_str)
print(config["database"]["host"])  # localhost
print(config["logging"]["handlers"])  # ['console', 'file']

# Python → YAML 字符串
data = {
    "server": {
        "host": "0.0.0.0",
        "port": 8080,
        "debug": False,
    },
    "routes": [
        {"path": "/api", "method": "GET"},
        {"path": "/submit", "method": "POST"},
    ],
}
yaml_output = yaml.dump(data, allow_unicode=True, default_flow_style=False)
print(yaml_output)

# 读写 YAML 文件
with open("config.yaml", "w", encoding="utf-8") as f:
    yaml.dump(data, f, allow_unicode=True, default_flow_style=False)

with open("config.yaml", "r", encoding="utf-8") as f:
    loaded = yaml.safe_load(f)
⚠️ 永远用 yaml.safe_load(),不要用 yaml.load()!后者会执行任意 Python 代码,有安全风险。

3️⃣ 多环境配置管理

import json
import yaml
from pathlib import Path
from typing import Any

class ConfigManager:
    """多环境配置管理器"""

    def __init__(self, config_dir: str = "config"):
        self.config_dir = Path(config_dir)
        self._config = {}

    def load(self, env: str = "dev"):
        """
        加载配置: base.yaml + {env}.yaml → 合并
        环境配置覆盖基础配置
        """
        base_file = self.config_dir / "base.yaml"
        env_file = self.config_dir / f"{env}.yaml"

        self._config = {}
        if base_file.exists():
            with open(base_file, encoding="utf-8") as f:
                self._config = yaml.safe_load(f) or {}

        if env_file.exists():
            with open(env_file, encoding="utf-8") as f:
                env_config = yaml.safe_load(f) or {}
                self._deep_merge(self._config, env_config)

        return self

    def get(self, key: str, default: Any = None) -> Any:
        """支持点号分隔的键: 'database.host'"""
        keys = key.split(".")
        value = self._config
        for k in keys:
            if isinstance(value, dict):
                value = value.get(k)
            else:
                return default
            if value is None:
                return default
        return value

    @staticmethod
    def _deep_merge(base: dict, override: dict) -> dict:
        """深度合并字典"""
        for k, v in override.items():
            if k in base and isinstance(base[k], dict) and isinstance(v, dict):
                ConfigManager._deep_merge(base[k], v)
            else:
                base[k] = v
        return base

# 使用
# cfg = ConfigManager("config").load("production")
# db_host = cfg.get("database.host", "localhost")
# db_port = cfg.get("database.port", 3306)

4️⃣ JSON Schema 验证

import json

def validate_config(data: dict, required_keys: list[str]) -> list[str]:
    """简单配置验证(不依赖第三方库)"""
    errors = []
    for key_path in required_keys:
        keys = key_path.split(".")
        current = data
        for key in keys:
            if isinstance(current, dict) and key in current:
                current = current[key]
            else:
                errors.append(f"缺少必需配置: {key_path}")
                break
    return errors

# 使用
required = ["database.host", "database.port", "logging.level"]
config = {"database": {"host": "localhost", "port": 3306}}
errors = validate_config(config, required)
# ["缺少必需配置: logging.level"]

5️⃣ 验证脚本

#!/usr/bin/env python3
"""第11课 JSON/YAML验证"""
import json
import tempfile
import os
from datetime import datetime
from decimal import Decimal
from pathlib import Path

def test_json_advanced():
    """JSON高级操作测试"""
    class SmartEncoder(json.JSONEncoder):
        def default(self, obj):
            if isinstance(obj, datetime):
                return obj.isoformat()
            if isinstance(obj, Decimal):
                return float(obj)
            if isinstance(obj, Path):
                return str(obj)
            if isinstance(obj, set):
                return sorted(list(obj))
            return super().default(obj)

    data = {"ts": datetime(2026,1,1), "price": Decimal("9.99"),
            "tags": {"a","b"}, "path": Path("/tmp")}
    encoded = json.dumps(data, cls=SmartEncoder)
    parsed = json.loads(encoded)
    assert parsed["price"] == 9.99
    assert parsed["tags"] == ["a", "b"]
    print("✅ JSON高级操作测试通过")

def test_json_file():
    """JSON文件读写测试"""
    data = {"name": "张三", "scores": [95,88]}
    with tempfile.NamedTemporaryFile(suffix=".json", delete=False,
                                      mode="w", encoding="utf-8") as f:
        path = f.name
        json.dump(data, f, ensure_ascii=False)
    with open(path, "r", encoding="utf-8") as f:
        loaded = json.load(f)
    assert loaded["name"] == "张三"
    os.unlink(path)
    print("✅ JSON文件读写测试通过")

def test_yaml():
    """YAML读写测试"""
    import yaml
    yaml_str = "host: localhost\nport: 3306\n"
    config = yaml.safe_load(yaml_str)
    assert config["host"] == "localhost"
    assert config["port"] == 3306

    output = yaml.dump(config, allow_unicode=True)
    parsed = yaml.safe_load(output)
    assert parsed["host"] == "localhost"
    print("✅ YAML读写测试通过")

def test_config_merge():
    """配置合并测试"""
    def deep_merge(base, override):
        for k, v in override.items():
            if k in base and isinstance(base[k], dict) and isinstance(v, dict):
                deep_merge(base[k], v)
            else:
                base[k] = v
        return base

    base = {"db": {"host": "localhost", "port": 3306}, "debug": True}
    override = {"db": {"host": "prod-db"}, "debug": False}
    result = deep_merge(base, override)
    assert result["db"]["host"] == "prod-db"
    assert result["db"]["port"] == 3306
    assert result["debug"] == False
    print("✅ 配置合并测试通过")

if __name__ == "__main__":
    test_json_advanced()
    test_json_file()
    test_yaml()
    test_config_merge()
    print("\n🎉 第11课全部验证通过!")
✅ JSON高级操作测试通过 ✅ JSON文件读写测试通过 ✅ YAML读写测试通过 ✅ 配置合并测试通过 🎉 第11课全部验证通过!

🔑 本课要点

  1. SmartEncoder——自定义编码器处理 datetime/Decimal/set/Path
  2. yaml.safe_load()——永远用 safe_load,不用 load
  3. allow_unicode=True——YAML 中文不转义
  4. 深度合并——环境配置覆盖基础配置
  5. 点号路径——"database.host" 比层层 get 更优雅