Hvis du arbejder med YAML i Python, bruger du næsten helt sikkert PyYAML. Det er standardbiblioteket,
det har eksisteret siden 2006 og leveres med en funktion kaldet yaml.load(), der har en kritisk
sikkerhedssårbarhed, som har skadet mange teams. Løsningen er ét ord — safe_load — men du
skal forstå hvorfor, hvad du går på kompromis med, og hvornår det nyere ruamel.yaml-bibliotek er det bedre valg.
Denne guide dækker praktisk YAML-parsing i Python: sikker indlæsning, strømme med flere dokumenter, dumpning af Python-objekter tilbage til YAML, konfigfil-mønstre med standardværdier og fejlhåndtering. Alle eksempler bruger virkelige scenarier — ingen pladsholder-data.
Installation
pip install pyyaml
# For ruamel.yaml (covered later)
pip install ruamel.yamlyaml.safe_load() — Den Du Altid Bør Bruge
Det vigtigste at vide om
PyYAML
er, at yaml.load() kan udføre vilkårlig Python-kode indlejret i en YAML-fil.
Dette er ikke en teoretisk risiko — det er en veldokumenteret angrebsvektor. Brug altid yaml.safe_load():
import yaml
# DANGEROUS — never use this with untrusted input
data = yaml.load(open('config.yaml'), Loader=yaml.FullLoader)
# SAFE — use this for any YAML from external sources
data = yaml.safe_load(open('config.yaml'))
# The attack: a YAML file could contain this, which executes Python
# !!python/object/apply:os.system ["rm -rf /important-dir"]yaml.safe_load() understøtter kun standard YAML-typer:
strenge, tal, booleske værdier, null, lister og dictionaries. Det kaster en ConstructorError, hvis YAML
indeholder Python-specifikke tags som !!python/object. Dette er præcis den adfærd, du ønsker.
yaml.full_load() er sikrere end den gamle bare yaml.load() men stadig mindre restriktiv
end safe_load(). Start med safe_load() og opgrader kun, hvis du virkelig har brug for det.Indlæsning af en YAML-konfigfil
Her er et realistisk konfigurationsindlæsningsmønster til en webapplikation. Vi indlæser en YAML-konfigfil og bruger Pythons dictionary-sammensmeltning til at udfylde standardværdier for alt, der ikke er angivet:
# config.yaml
database:
host: postgres.internal
port: 5432
name: myapp_prod
pool_size: 10
redis:
host: redis.internal
port: 6379
logging:
level: INFO
format: jsonimport yaml
from pathlib import Path
from typing import Any
DEFAULT_CONFIG = {
'database': {
'host': 'localhost',
'port': 5432,
'name': 'myapp',
'pool_size': 5,
'ssl': False,
},
'redis': {
'host': 'localhost',
'port': 6379,
'db': 0,
},
'logging': {
'level': 'DEBUG',
'format': 'text',
}
}
def deep_merge(base: dict, override: dict) -> dict:
"""Recursively merge override into base, returning a new dict."""
result = base.copy()
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = value
return result
def load_config(config_path: str | Path) -> dict[str, Any]:
path = Path(config_path)
if not path.exists():
raise FileNotFoundError(f"Config file not found: {path}")
with path.open('r', encoding='utf-8') as f:
raw = yaml.safe_load(f)
if raw is None:
return DEFAULT_CONFIG.copy()
return deep_merge(DEFAULT_CONFIG, raw)
config = load_config('config.yaml')
print(config['database']['host']) # postgres.internal
print(config['database']['ssl']) # False (from defaults)
print(config['redis']['db']) # 0 (from defaults)Dumpning af Python-objekter til YAML
yaml.dump() serialiserer Python-dictionaries, lister, strenge, tal, booleske værdier og None til YAML.
Som standard bruger det flowstil (inline-tuborg-parenteser) — sæt default_flow_style=False for det
læsbare blokstil:
import yaml
from dataclasses import dataclass, asdict
@dataclass
class ServiceConfig:
name: str
replicas: int
image: str
port: int
tags: list[str]
service = ServiceConfig(
name='payment-api',
replicas=3,
image='payment-api:2.4.1',
port=8080,
tags=['payments', 'backend', 'critical']
)
# Convert dataclass to dict first, then dump to YAML
output = yaml.dump(
asdict(service),
default_flow_style=False,
sort_keys=False, # preserve insertion order
allow_unicode=True
)
print(output)
# image: payment-api:2.4.1
# name: payment-api
# port: 8080
# replicas: 3
# tags:
# - payments
# - backend
# - critical
# Write to file
with open('service-config.yaml', 'w', encoding='utf-8') as f:
yaml.dump(asdict(service), f, default_flow_style=False, sort_keys=False)Strømme med Flere Dokumenter med load_all
YAML understøtter flere dokumenter i en enkelt fil, adskilt af ---. Dette er almindeligt i
Kubernetes-manifester, hvor en enkelt fil kan indeholde en Deployment, en Service og en ConfigMap.
Brug yaml.safe_load_all() til at iterere over alle dokumenter:
import yaml
# manifests.yaml contains multiple Kubernetes resources separated by ---
with open('manifests.yaml', 'r') as f:
# safe_load_all returns a generator
documents = list(yaml.safe_load_all(f))
for doc in documents:
if doc is None:
continue
kind = doc.get('kind', 'Unknown')
name = doc.get('metadata', {}).get('name', 'unnamed')
print(f"{kind}: {name}")
# Deployment: payment-api
# Service: payment-api-svc
# ConfigMap: payment-api-configDu kan også skrive flere dokumenter til en strøm med yaml.dump_all():
import yaml
documents = [
{'kind': 'Deployment', 'metadata': {'name': 'api'}, 'spec': {'replicas': 2}},
{'kind': 'Service', 'metadata': {'name': 'api-svc'}, 'spec': {'port': 80}},
]
output = yaml.dump_all(documents, default_flow_style=False)
print(output)
# kind: Deployment
# metadata:
# name: api
# spec:
# replicas: 2
# ---
# kind: Service
# metadata:
# name: api-svc
# spec:
# port: 80ruamel.yaml — Når Du Skal Bevare Kommentarer
PyYAML har én væsentlig begrænsning: det fjerner kommentarer ved indlæsning. Hvis du indlæser en YAML-fil, ændrer den og skriver den tilbage, er alle kommentarer væk. For konfigfiler, som mennesker vedligeholder, er tabet af kommentarer en dealbreaker.
ruamel.yaml implementerer en round-trip-parser, der bevarer kommentarer, nøgleorden og formatering — det understøtter som standard YAML 1.2-specifikationen. Det er det rigtige valg, når du programmatisk redigerer YAML, som mennesker vil læse efterfølgende:
from ruamel.yaml import YAML
yaml = YAML()
yaml.preserve_quotes = True
# This config.yaml has important comments we need to keep:
# database:
# host: localhost # change this for production
# port: 5432 # default PostgreSQL port
# pool_size: 5 # increase under heavy load
with open('config.yaml', 'r') as f:
config = yaml.load(f)
# Modify a value
config['database']['host'] = 'postgres.prod.internal'
config['database']['pool_size'] = 20
# Write back — comments and formatting are preserved!
with open('config.yaml', 'w') as f:
yaml.dump(config, f)
# Result:
# database:
# host: postgres.prod.internal # change this for production
# port: 5432 # default PostgreSQL port
# pool_size: 20 # increase under heavy load- Brug PyYAML, når du læser YAML til forbrug — parsing af konfiguration til din app, indlæsning af testfixtures, behandling af Kubernetes-manifester programmatisk.
- Brug ruamel.yaml, når du redigerer YAML, som mennesker vedligeholder — opdatering af konfigfiler på stedet, værktøj, der ændrer CI-konfigurationer, alt, hvor tab af kommentarer ville være et problem.
- ruamel.yaml er også YAML 1.2-kompatibelt som standard, hvilket betyder, at Norge-problemet (
NO→false) ikke påvirker det. PyYAML bruger YAML 1.1 som standard.
Fejlhåndtering
YAML-parsningsfejl kaster yaml.YAMLError, som er basisklassen for alle PyYAML-undtagelser.
Fang altid det, når du indlæser YAML fra upålidelige eller brugerleverede kilder:
import yaml
from pathlib import Path
def load_user_config(path: str) -> dict:
try:
with open(path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
except FileNotFoundError:
raise FileNotFoundError(f"Config file not found: {path}")
except yaml.scanner.ScannerError as e:
# Includes line/column info in the error message
raise ValueError(f"YAML syntax error in {path}:\n{e}")
except yaml.YAMLError as e:
raise ValueError(f"Invalid YAML in {path}: {e}")
if data is None:
return {}
if not isinstance(data, dict):
raise TypeError(f"Expected a YAML mapping at top level, got {type(data).__name__}")
return datayaml.safe_load() garanterer kun
gyldig YAML-syntaks — det validerer ikke dataformens form. En konfigfil med database:
øverst er fin; en konfigfil med en liste øverst er også gyldigt YAML. Tilføj type- og strukturkontroller
efter indlæsning, eller brug et skemavalideringsbibliotek som
Pydantic til at parse den indlæste dictionary
til en typsikker model.Opsummering
PyYAML dækker det store flertal af YAML-arbejde i Python: brug altid yaml.safe_load()
(ikke yaml.load()), brug yaml.safe_load_all() til strømme med flere dokumenter,
og brug yaml.dump() med default_flow_style=False for læsbar output.
Når du skal bevare kommentarer eller have YAML 1.2-semantik, skift til ruamel.yaml — det er en enkel
opgradering til læsning og en mindre API-ændring til skrivning. For syntaksfejl inden din kode kører,
YAML-validatoren fortæller dig præcis, hvilken linje og kolonne der er ødelagt.