PyYAML 是解析和输出 YAML 的第三方 Python 包,安装名为 PyYAML,导入名为 yaml。读取普通配置优先使用 safe_load,输出普通 Python 数据使用 safe_dump;解析成功后还要验证配置是否符合程序需要的结构。本文示例以 Python 3.11、PyYAML 6.0.3 为基线。
从配置文件读入,再把字典写回文件
跳转到“从配置文件读入,再把字典写回文件”在对应的虚拟环境中执行 python -m pip install PyYAML==6.0.3 后,可以运行下面的完整例子。它在临时目录中创建 config.yaml 和 output_data.yaml,保留原记录里的姓名、年龄、技能列表与就业状态;退出上下文后清理演示文件。
from pathlib import Pathfrom tempfile import TemporaryDirectoryimport yaml
data = { "name": "Jane Doe", "age": 25, "skills": ["Python", "Web Development", "UI/UX Design"], "is_employed": False,}
with TemporaryDirectory() as directory: root = Path(directory) config = root / "config.yaml" config.write_text("name: Jane Doe\nage: 25\n", encoding="utf-8") with config.open("r", encoding="utf-8") as stream: loaded = yaml.safe_load(stream) assert loaded == {"name": "Jane Doe", "age": 25} print(loaded)
output = root / "output_data.yaml" with output.open("w", encoding="utf-8") as stream: yaml.safe_dump(data, stream, allow_unicode=True, default_flow_style=False, sort_keys=False) with output.open("r", encoding="utf-8") as stream: restored = yaml.safe_load(stream) assert restored == data assert list(restored) == list(data) print(output.read_text(encoding="utf-8"))
assert "中文" in yaml.safe_dump({"name": "中文"}, allow_unicode=True)allow_unicode=True 允许输出可直接阅读的 Unicode 字符;文件仍通过 encoding="utf-8" 决定实际字节编码。default_flow_style=False 倾向使用分行缩进形式,sort_keys=False 按当前字典遍历顺序输出。safe_dump 不保证保留输入文件的注释、引号风格和排版;读入再输出不是原文格式的无损往返。
YAML 结构与 Python 对象的对应关系
跳转到“YAML 结构与 Python 对象的对应关系”YAML 的核心结构是标量、序列和映射。时间戳、二进制及标签等是类型解析或图结构方面的规则,不是另一个与三种核心结构平行的容器分类。
| 原表主题 | PyYAML safe_load 中的结果 | 需要区分的边界 |
|---|---|---|
| 标量 Scalar | str、bool、int、float、None 等 | 引号、标签及隐式解析规则影响类型 |
| 序列 Sequence | list | 保留元素顺序,元素可以是嵌套结构 |
| 映射 Mapping | dict | Python 3.7 起字典保证插入顺序;YAML 映射本身不应当作业务排序规则 |
| 复合类型 | 嵌套的列表、字典和标量 | 解析出来不等于符合应用的数据模型 |
| 时间戳 Timestamp | 日期可为 datetime.date,含时间可为 datetime.datetime | 时区及格式影响具体对象;需要纯文本日期时可加引号 |
| 二进制 Binary | !!binary 通常解析为 bytes | YAML 文本内使用 Base64 表示,不是直接得到普通 str |
| 合并键 Merge Key | PyYAML 解析 << 后合并进字典 | 属于该解析器支持的合并语义,不是可随意跨版本假定的 YAML 核心规则 |
| 锚点与别名 Anchor/Alias | 多个位置可以引用同一个 Python 对象 | 不是自动深复制,修改可变对象会影响其他引用 |
| 标签 Tag | 决定如何构造节点的对象 | safe_load 支持标准安全类型,未知或 Python 对象标签会报错 |
下面把容易误判的类型放在一个可检查的文档里:
from datetime import date, datetime, timezoneimport yaml
text = """name: Jane Doeenabled: falsecount: 25ratio: 1.5missing: nullskills: [Python, Web Development]profile: {city: Shanghai}day: 2026-10-03moment: 2026-10-03T08:30:00Zpayload: !!binary aGVsbG8=literal_yes: !!str yesquoted_day: "2026-10-03"implicit_yes: yes"""value = yaml.safe_load(text)assert value["name"] == "Jane Doe"assert value["enabled"] is False and type(value["count"]) is intassert value["ratio"] == 1.5 and value["missing"] is Noneassert value["skills"] == ["Python", "Web Development"]assert value["profile"] == {"city": "Shanghai"}assert type(value["day"]) is dateassert value["moment"] == datetime(2026, 10, 3, 8, 30, tzinfo=timezone.utc)assert value["payload"] == b"hello"assert value["literal_yes"] == "yes" and value["quoted_day"] == "2026-10-03"assert value["implicit_yes"] is Trueprint("YAML type checks passed")PyYAML 6.0.3 默认使用的一些隐式解析规则来自 YAML 1.1,例如未加引号的 yes、on 可能变为布尔值。不要直接套用其他 YAML 1.2 解析器的结果;配置里确实需要这些字符串时,明确加引号或 !!str。对应行为见 PyYAML 文档与 PyYAML 项目版本信息。
锚点、别名和合并不会自动隔离嵌套对象
跳转到“锚点、别名和合并不会自动隔离嵌套对象”合并键让配置复用默认值;别名让节点共享引用。下面的 override 在普通 safe_load 下覆盖默认超时,但嵌套列表仍可能共享。
import yaml
config = yaml.safe_load("""defaults: &defaults timeout: 5 labels: [base]alias: *defaultsoverride: <<: *defaults timeout: 10""")assert config["alias"] is config["defaults"]assert config["override"] is not config["defaults"]assert config["override"]["timeout"] == 10assert config["defaults"]["timeout"] == 5assert config["override"]["labels"] is config["defaults"]["labels"]config["alias"]["labels"].append("extra")assert config["override"]["labels"] == ["base", "extra"]print("alias and merge checks passed")需要独立修改时,要按数据结构选择复制策略,参见对象复制。循环别名还可能构造循环对象,因此下游遍历不能假定解析结果总是一棵没有回路的树。
安全构造、字段验证和重复键是不同检查
跳转到“安全构造、字段验证和重复键是不同检查”safe_load 限制对象构造方式,但不会替你检查业务字段、文件大小或嵌套深度。空文档通常返回 None;顶层也可能是列表或字符串,不能读完就直接当字典使用。
import yaml
def require_profile(value): if not isinstance(value, dict): raise ValueError("profile must be a mapping") if not isinstance(value.get("name"), str) or not value["name"]: raise ValueError("name must be nonempty text") # bool 是 int 的子类,这里明确只接受整数年龄。 if type(value.get("age")) is not int or not 0 <= value["age"] <= 150: raise ValueError("age must be an integer between 0 and 150") return value
assert yaml.safe_load("") is Noneassert require_profile(yaml.safe_load("name: Jane Doe\nage: 25"))["age"] == 25for text in ["", "[Jane, 25]", "name: Jane\nage: true", "name: Jane\nage: -1"]: try: require_profile(yaml.safe_load(text)) except ValueError: pass else: raise AssertionError("invalid profile must be rejected")
for text in ["items: [1, 2", "item: !unknown value", "!!python/object/apply:builtins.str []"]: try: yaml.safe_load(text) except yaml.YAMLError: pass else: raise AssertionError("invalid or unsupported YAML must be rejected")print("construction and schema checks passed")默认 safe_load 遇到重复映射键时可能保留后一个值。若配置不允许这种覆盖,可显式提供更严格的加载器。以下策略在合并键展开后检查重复项,因此也会拒绝通过合并后重写同名键的配置;它比上一节的默认合并规则更严格。
import yamlfrom yaml.constructor import ConstructorError
class UniqueKeySafeLoader(yaml.SafeLoader): def construct_mapping(self, node, deep=False): if not isinstance(node, yaml.MappingNode): raise ConstructorError(None, None, "expected a mapping", node.start_mark) self.flatten_mapping(node) seen = set() for key_node, _ in node.value: key = self.construct_object(key_node, deep=deep) try: duplicate = key in seen seen.add(key) except TypeError as error: raise ConstructorError(None, None, "unhashable mapping key", key_node.start_mark) from error if duplicate: raise ConstructorError(None, None, "duplicate mapping key", key_node.start_mark) return super().construct_mapping(node, deep=deep)
def load_unique(text): return yaml.load(text, Loader=UniqueKeySafeLoader)
assert yaml.safe_load("port: 8000\nport: 9000") == {"port": 9000}assert load_unique("port: 8000\nhost: localhost") == {"port": 8000, "host": "localhost"}for text in ["port: 8000\nport: 9000", "outer: {a: 1, a: 2}", "base: &base {port: 8000}\nchild: {<<: *base, port: 9000}"]: try: load_unique(text) except ConstructorError: pass else: raise AssertionError("duplicate keys must be rejected")print("duplicate-key policy checks passed")这里的 yaml.load 显式使用继承自 SafeLoader 的受限加载器;不能把它改成任意对象加载器。若键经过类型转换后相等,例如 true 与 1,此策略也视为冲突,避免在 Python 字典中静默覆盖。
一个文件里有多个文档
跳转到“一个文件里有多个文档”safe_load 只接受一个文档;safe_load_all 逐个产生多个文档。读取文件时,要在文件仍打开的上下文中完成迭代。输出多个文档可用 safe_dump_all。
from pathlib import Pathfrom tempfile import TemporaryDirectoryimport yaml
with TemporaryDirectory() as directory: filename = Path(directory) / "documents.yaml" filename.write_text("name: first\n---\nname: second\n", encoding="utf-8") with filename.open("r", encoding="utf-8") as stream: documents = list(yaml.safe_load_all(stream)) assert documents == [{"name": "first"}, {"name": "second"}] encoded = yaml.safe_dump_all(documents, allow_unicode=True, sort_keys=False) assert list(yaml.safe_load_all(encoded)) == documents try: yaml.safe_load(encoded) except yaml.YAMLError: pass else: raise AssertionError("single-document loader must reject multiple documents")print("multi-document checks passed")项目需要限制大小、必填项、字段类型或允许的标签时,应把这些要求写成单独的输入规则。数据交换如果只需要 JSON 的类型集合,也可直接使用标准库 JSON。