Python'da YAML ile çalışıyorsanız, büyük ihtimalle PyYAML kullanıyorsunuzdur. Bu standart kütüphane
2006'dan beri var ve kritik bir güvenlik açığı taşıyan yaml.load() adlı bir fonksiyonla geliyor;
bu açık pek çok ekibi zor durumda bırakmış. Çözüm tek kelime — safe_load — ama nedenini,
ne kaybettiğinizi ve daha yeni ruamel.yaml kütüphanesinin ne zaman daha iyi bir seçim olduğunu anlamanız gerekiyor.
Bu rehber Python'da pratik YAML ayrıştırmayı kapsar: güvenli yükleme, çok belgeli akışlar, Python nesnelerini tekrar YAML'a dönüştürme, varsayılan değerler içeren yapılandırma dosyası kalıpları ve hata yönetimi. Tüm örnekler gerçek dünya senaryolarını kullanır — yer tutucu veri yok.
Kurulum
pip install pyyaml
# For ruamel.yaml (covered later)
pip install ruamel.yamlyaml.safe_load() — Her Zaman Kullanmanız Gereken
PyYAML hakkında
bilmeniz gereken en önemli şey: yaml.load(), bir YAML dosyasına gömülü rastgele Python kodunu
çalıştırabilir. Bu teorik bir risk değil — iyi belgelenmiş bir saldırı vektörü. Her zaman yaml.safe_load() kullanın:
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() yalnızca standart YAML tiplerini destekler:
string, sayı, boolean, null, liste ve dict. YAML içinde !!python/object gibi Python'a özgü
etiketler bulunursa ConstructorError fırlatır. İstediğiniz davranış tam da bu.
yaml.full_load(), eski yaml.load()'dan daha güvenlidir ancak safe_load()'dan
daha az kısıtlayıcıdır. safe_load() ile başlayın ve gerçekten ihtiyaç duyduğunuzda yükseltin.Bir YAML Yapılandırma Dosyasını Yükleme
İşte bir web uygulaması için gerçekçi bir yapılandırma yükleme kalıbı. Bir YAML yapılandırma dosyasını yükleyip belirtilmemiş her şey için Python'un dict birleştirmesiyle varsayılan değerleri dolduruyoruz:
# 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)Python Nesnelerini YAML'a Dönüştürme
yaml.dump(), Python dict, liste, string, sayı, boolean ve None değerlerini YAML'a dönüştürür.
Varsayılan olarak akış stilini (satır içi süslü parantezler) kullanır — okunabilir blok stili için
default_flow_style=False ayarlayın:
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)load_all ile Çok Belgeli Akışlar
YAML, --- ile ayrılmış tek bir dosyada birden fazla belgeyi destekler. Bu, tek bir dosyanın
Deployment, Service ve ConfigMap içerebildiği Kubernetes manifest'lerinde yaygındır.
Tüm belgeler üzerinde yineleme yapmak için yaml.safe_load_all() kullanın:
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-configAyrıca yaml.dump_all() ile bir akışa birden fazla belge yazabilirsiniz:
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 — Yorumları Korumak İstediğinizde
PyYAML'ın önemli bir sınırlaması var: yükleme sırasında yorumları kaldırıyor. Bir YAML dosyasını yükleyip değiştirip geri yazarsanız tüm yorumlar gidiyor. İnsanların yönettiği yapılandırma dosyaları için yorumların kaybolması kabul edilemez bir durum.
ruamel.yaml, yorumları, anahtar sıralamasını ve biçimlendirmeyi koruyan round-trip bir ayrıştırıcı uygular — varsayılan olarak YAML 1.2 spesifikasyonunu hedefler. İnsanların sonradan okuyacağı YAML'ı programlı olarak düzenlediğinizde bu doğru seçimdir:
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- PyYAML kullanın: YAML'ı tüketmek için okurken — yapılandırmayı uygulamanıza ayrıştırırken, test fixture'larını yüklerken, Kubernetes manifest'lerini programlı olarak işlerken.
- ruamel.yaml kullanın: insanların yönettiği YAML'ı düzenlerken — yapılandırma dosyalarını yerinde güncellerken, CI yapılandırmalarını değiştiren araçlarda, yorumların kaybolmasının sorun yaratacağı her durumda.
- ruamel.yaml varsayılan olarak YAML 1.2 uyumludur; bu da Norveç Sorununu (
NO→false) etkilemediği anlamına gelir. PyYAML varsayılan olarak YAML 1.1 kullanır.
Hata Yönetimi
YAML ayrıştırma hataları, tüm PyYAML istisnalarının temel sınıfı olan yaml.YAMLError'u fırlatır.
Güvenilmeyen veya kullanıcı tarafından sağlanan kaynaklardan YAML yüklerken her zaman yakalayın:
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() yalnızca geçerli YAML sözdizimini garanti eder — verinin şeklini doğrulamaz. Üstte database: bulunan bir yapılandırma dosyası geçerlidir; üstte liste bulunan bir yapılandırma dosyası da geçerli YAML'dır. Yüklemeden sonra tür ve yapı kontrolleri ekleyin ya da yüklenen dict'i yazımlı bir modele ayrıştırmak için Pydantic gibi bir şema doğrulama kütüphanesi kullanın.Özet
PyYAML, Python'daki YAML işlerinin büyük çoğunluğunu karşılar: her zaman yaml.safe_load()
kullanın (yaml.load() değil), çok belgeli akışlar için yaml.safe_load_all() kullanın
ve okunabilir çıktı için yaml.dump()'u default_flow_style=False ile kullanın.
Yorumları korumak veya YAML 1.2 semantiği almak istediğinizde ruamel.yaml'a geçin — okuma için doğrudan
bir yükseltme, yazma için küçük bir API değişikliğidir. Kodunuz çalışmadan önce sözdizimi hataları için
YAML Doğrulayıcı, tam olarak hangi satır ve sütunun bozuk olduğunu söyler.