加载不受信任的 YAML
来自用户、上传或第三方的 YAML 文件是输入,不是代码。本章展示如何在不构造任意 Python 对象的前提下读取它们、未知标签如何呈现,以及如何报告格式错误的输入。
1. 安全引擎
safe_load() 与 safe_load_all() 使用安全引擎。它们返回普通 Python 值——没有文档包装——并拒绝非标准应用标签,而不是解析它们:
import pythonizeyaml as yaml
manifest = yaml.safe_load("steps:\n - run: python -m pytest\n")
assert manifest == {"steps": [{"run": "python -m pytest"}]}
形如 !!python/object/apply:os.system 的输入会抛出 ConstructorError;不会有任何东西被执行。这使 safe_load 成为跨信任边界数据的默认选择。
对应的 safe_dump() 与 safe_dump_all() 只输出普通 YAML 类型:Tagged 值,以及 Document 中任何位置出现的自定义应用标签,都会以 RepresenterError 拒绝。
2. 校验器草图
把安全引擎与错误层级组合起来,就能为(例如)用户上传的 CI 清单写出简洁的校验器:
from pathlib import Path
import pythonizeyaml as yaml
def validate(path: Path) -> list[str]:
try:
manifest = yaml.safe_load(path.read_text(encoding="utf-8"))
except yaml.YAMLError as error:
return [f"invalid YAML: {error}"]
problems = []
if not isinstance(manifest, dict):
problems.append("the document must be a mapping")
elif "steps" not in manifest:
problems.append("missing steps")
return problems
捕获 yaml.YAMLError 即可覆盖整个层级——ScannerError 与 ParserError 对应格式错误的文本,ConstructorError 对应被拒绝的标签——且消息包含源位置:
invalid YAML: tab characters must not be used in indentation
in "unicode string", line 2, column 1
3. 常规引擎中的未知标签
往返引擎不会拒绝未知标签,因为逐字节保留文件正是它的职责。它把未知标签作为惰性的 Tagged 值返回:标签原文与解析出的值,不执行任何东西:
value = yaml.load("job: !runner {name: tests}\n")
assert isinstance(value["job"], yaml.Tagged)
assert value["job"].tag == "!runner"
assert value["job"].value == {"name": "tests"}
assert yaml.dump(value) == "job: !runner {name: tests}\n"
Tagged 对象会原样往返,因此工具可以在不理解这些标签的情况下编辑这类文件——文件也不会丢掉它们。
如果你想要的是普通 Python 数据而非可编辑文档,但输入可能携带应用标签,full_load() 会返回普通 dict/list 数据,并把未知标签包装为惰性的 Tagged 值。unsafe_load() 是 full_load() 的正式文档别名:与本库的每个加载器一样,它从不构造任意 Python 对象,这个名字只是兼容性写法,并不带来额外风险。
4. 解析器加固
格式错误的输入会安全而精确地失败:
- 嵌套深度超过解析器 128 层上限的输入会抛出
ParserError("maximum nesting depth exceeded"),而不是耗尽调用栈。 - 双引号标量中的非法转义序列——未知转义如
\q,或畸形十六进制转义如\xZZ——会抛出ScannerError。 - 失败绝不会以内部 panic 或原生崩溃的形式暴露:每个错误都会以文档记载的
YAMLError子类返回,解析侧的错误还带有源位置。
try:
yaml.safe_load('msg: "bad \\q escape"\n')
except yaml.ScannerError as error:
print(error) # found unknown escape character 'q' ...
5. 选择引擎
- 自己拥有并编辑的配置:
yaml.load()—— 你会得到文档、样式与注释。 - 来自外部的数据:
yaml.safe_load()—— 普通值,标签被拒绝。 - 含未知标签且必须原样保留的文件:
yaml.load()配合Tagged透传;或者当这类文件应当被直接拒绝时,使用safe_load()。
两个引擎共享同一个解析器和同一套错误层级,所以一套 except yaml.YAMLError 策略对两者都有效。