一. yaml基本概念
yaml简介
yaml有种含义:
-
yaml是专门用来写配置文件的语言。
-
yaml文件是一种常用的配置文件类型,后缀名是.yaml 或 .yml。
yaml的基本语法规则
- 区分大小写
- 使用缩进表示层级关系
- 使用空格缩进,而非Tab键缩进
- 缩进的空格数目不固定,只需要相同层级的元素左侧对齐
- 文件中的字符串不需要引号标注,但若字符串中包含有特殊字符则需用引号标注
- 注释标识为 #
yaml文件数据结构
- 对象:键值对的集合(简称:"映射或字典")
键值对用冒号:
结构来表示,冒号与值之间需用空格分隔 - 数组: 一组按序排列的值(简称"序列或表")
数组前加有-
符号,符号与值之间需用空格分隔 - 纯量(scalars): 单个的、不可再分的值(如:字符串、bool值、整数、浮点数、时间、日期、null等) None值可用
null
或~
表示
二、需求分析
我写这个工具的本意是为了整合项目里的配置文件,之前是分散在几个ini文件中的,不好维护。 整合之后的话,我可能会需要对yaml进行比较复杂的操作,比如在一个yaml文件的中首部,尾部,任一位置,插入,删除,修改指定的yaml文档。
所以分析下来,我要实现的工具的功能是:
- 支持单个与多个yaml文档的场景
- 支持对同一个yaml文件的首部,尾部,任一位置,插入,删除,修改指定的yaml文档。
三、技术选型
操作yaml的常用python库有哪些,各自有什么优缺点
操作yaml的常用python库有yaml
,PyYAML
,ruamel.yaml
,yamlize
等。
yaml
其中首先排除yaml
,它功能比较简陋,不符合我的需求。
yaml是 Python 标准库中的 YAML 解析器和生成器。它简单易用,并且对于大多数常见的 YAML 用例来说已经足够了。但是,它不支持 YAML 1.2 规范中的一些新特性,例如合并键和显式序列标签。
PyYAML
-
优点:
- 广泛使用: PyYAML是Python社区中最知名的YAML处理库之一,有着广泛的用户基础和成熟的应用案例。
- 简单易用 : 提供了简洁的API,如
yaml.safe_load()
和yaml.dump()
,方便快速读写YAML数据。 - 跨平台兼容: 支持多种操作系统和Python版本,兼容性良好。
- 标准符合: 符合YAML 1.1规范,能够处理大部分常见YAML数据结构。
-
缺点:
- 部分功能缺失: 对较新的YAML 1.2规范支持不够全面,缺少对某些特性的支持(如标签和插件系统)。
- 顺序保留问题: 默认情况下,加载YAML时不保证元素的原始顺序,可能导致顺序敏感的数据丢失其排列信息。
- 安全性考虑 :
yaml.load()
函数可能存在潜在的安全风险(如代码注入),推荐使用yaml.safe_load()
替代。
ruamel.yaml
-
优点:
- 先进功能: 支持YAML 1.2规范,提供比PyYAML更丰富的功能,如保留注释、原始标记、精确的缩进控制等。
- 顺序保留: 读取YAML时能准确保持元素的原始顺序,适用于对顺序敏感的数据。
- 多文档支持: 可以轻松处理包含多个独立YAML文档的文件。
- 安全性增强: 提供了更安全的加载机制,降低代码注入的风险。
-
缺点:
- 学习曲线较陡: API相对于PyYAML来说稍显复杂,初学者可能需要更多时间熟悉。
- 性能影响: 由于提供了额外的特性支持,如注释保留等,可能在处理大型文件时比PyYAML稍慢。
- 依赖关系 : 一些高级功能可能需要额外的库支持,如颜色高亮输出需要
colorama
库。
yamlize
-
优点:
- 类型安全: 提供了基于数据类(dataclasses)的序列化和反序列化,确保类型安全和自动转换。
- 易于定制: 用户可以自定义数据类以匹配特定的YAML结构,简化了复杂数据模型的处理。
- 代码整洁: 通过装饰器声明方式简化了代码,使得业务逻辑和YAML处理分离,提高代码可读性和维护性。
-
缺点:
- 应用场景受限: 更适合处理具有严格结构定义的数据模型,对于松散结构或灵活的数据表示可能不如其他库便捷。
- 功能相对有限: 相对于PyYAML和ruamel.yaml,yamlize提供的功能相对较少,可能不适合复杂的YAML处理需求。
- 社区活跃度: 相比于前两者,yamlize的社区活跃度和用户基数较小,遇到问题可能求助资源较少。
各库适用场景总结
总结起来,选择哪个库取决于具体的项目需求:
- 如果需要一个简单、通用且广泛使用的库,PyYAML可能是首选。
- 若项目涉及复杂的YAML结构、需要保留注释或精确控制序列化细节,ruamel.yaml更适合。
- 对于注重类型安全、希望基于数据类进行序列化/反序列化的场景,yamlize提供了便利的解决方案。
四、功能实现
基于第二章节的需求分析和第三章节的技术选型,我最终选择的是ruamel.yaml
最终代码呈现如下: 基类:
python
class BaseYAMLManager:
def __init__(self, file_path: str, auto_save: bool = False):
self.file_path = file_path
self.auto_save = auto_save
def save(self, file_path: str = None, encoding='utf-8'):
pass
def load(self, file_path: str = None, encoding='utf-8'):
pass
def rollback(self, previous_documents):
pass
def backup(self):
pass
def insert_document(self, new_content: dict, position: int):
pass
def update_document(self, position: int, updated_content: dict):
pass
def delete_document(self, position: int):
pass
def get_document(self, position: int):
pass
def get_documents(self):
pass
装饰器:
python
def with_auto_save_rollback(method):
@wraps(method)
def wrapper(file_manager: BaseYAMLManager, *args, **kwargs) -> Any:
previous_documents = file_manager.backup() # Backup each document individually
try:
result = method(file_manager, *args, **kwargs)
if file_manager.auto_save:
file_manager.save()
return result
except Exception as e:
file_manager.rollback(previous_documents)
raise e
return wrapper
YAMLFileManager类:
python
from functools import wraps
from types import NoneType
from typing import Union, Any, Dict, List, Tuple, Set
import copy
import ruamel.yaml
from .base import BaseYAMLManager
YAMLValue = Union[
Dict[str, Any],
List[Any],
Tuple[Any, ...],
Set[Any],
bool,
str,
int,
float,
NoneType,
]
class YAMLDocument(BaseYAMLManager):
def __init__(self, content: dict, position: int = None):
self.content = content
self.position = position
self._backup_content = None
def backup(self):
self._backup_content = copy.deepcopy(self.content)
def restore_from_backup(self):
if self._backup_content is not None:
self.content = self._backup_content
self._backup_content = None
class YAMLFileManager:
def __init__(self, file_path: str, auto_save: bool = False):
super().__init__(file_path, auto_save)
self.file_path = file_path
self.documents = []
self._auto_save = auto_save
self.load()
def load(self, file_path: str = None, encoding='utf-8'):
"""
加载YAML文件,保留注释和原始格式
:param file_path: 加载的yaml文件路径,若不传,则默认使用当前对象的file_path
:param encoding: 文件编码格式
:return:
"""
file_path = file_path or self.file_path
with open(file_path, 'r', encoding=encoding) as f:
yaml = ruamel.yaml.YAML()
yaml.preserve_quotes = True
yaml.constructor.add_constructor(
None, lambda loader, node: loader.construct_mapping(node, deep=True))
yaml.representer.add_representer(
type(None), lambda dumper, data: dumper.represent_scalar('tag:yaml.org,2002:null', ''))
for i, doc in enumerate(yaml.load_all(f)):
self.documents.append(YAMLDocument(content=doc, position=i))
def save(self, file_path: str = None, encoding='utf-8'):
"""
保存YAML文件,精确控制输出格式
:param file_path: 输出yaml文件路径,若不传,则使用当前对象的file_path
:param encoding: 文件编码格式
:return:
"""
file_path = file_path or self.file_path
with open(file_path, 'w', encoding=encoding) as f:
yaml = ruamel.yaml.YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
yaml.explicit_start = True
yaml.explicit_end = True
yaml.preserve_quotes = True
yaml.width = 79
yaml.allow_unicode = True
yaml.default_flow_style = False
yaml.sort_base_mapping_type_on_output = False
for doc in self.documents:
yaml.dump(doc.content, f)
if doc.position < self.documents[-1].position: # Not the last document, add separator
f.write('\n---\n')
def rollback(self, previous_documents):
for prev_doc, curr_doc in zip(previous_documents, self.documents):
curr_doc.restore_from_backup()
def backup(self):
return [doc.backup() for doc in self.documents]
@with_auto_save_rollback
def insert_document(self, new_content: dict, position: int):
"""在指定位置插入新文档"""
self.documents.insert(position, YAMLDocument(new_content, position))
@with_auto_save_rollback
def update_document(self, position: int, updated_content: dict):
"""更新指定位置的文档"""
self.documents[position].content = updated_content
@with_auto_save_rollback
def delete_document(self, position: int):
"""删除指定位置的文档"""
# del self.documents[position]
self.documents.pop(position)
def get_documents(self) -> List[YAMLDocument]:
"""获取所有文档"""
return self.documents
def get_document(self, position: int) -> YAMLDocument:
"""获取指定位置的文档"""
return self.documents[position]
@property
def auto_save(self):
return self._auto_save
@auto_save.setter
def auto_save(self, auto_save: bool):
self._auto_save = auto_save
五、附录
关于yaml介绍和语法部分参考了这位老哥的文章 Python中yaml的使用方法