我能想到的最接近的东西可以被视为某种约定,即 Linux 配置文件。在现代 Linux 中,您经常将服务的配置拆分为驻留在某个目录中的多个文件,例如,/etc/exim4/conf.d/
而不是只有一个文件/etc/exim/exim4.conf
。这样做有多种原因:
- 一些配置可能由包管理器提供(例如链接到通过包管理器安装的其他服务),而其他部分是用户定义的。由于如果用户编辑包管理器提供的文件会发生冲突,他们可以创建一个新文件并在那里输入其他配置。
- 对于大型配置文件(例如 exim4),如果您有多个文件用于不同的问题(核心 vim 用户可能不同意),则更容易导航配置。
- 您可以通过重命名/移动包含特定部分的文件来更轻松地启用/禁用部分配置。
我们可以从中学到一点:如果内容的语义是正交的,即一个文件的语义不依赖于另一个文件的语义,则应该将其分成不同的文件。这当然是兄弟文件的规则;我们不能真正从中推断出将树结构序列化为目录树的规则。但是,我们绝对可以看到不拆分自己文件中的每个值的原因。
您提到将特殊字符编码为文件名的问题。如果您违反约定,您只会遇到这个问题!文件和目录名称的隐含约定是它们充当文件的定位器/ID,而不是内容。同样,我们可以从 Linux 配置文件中学到一些东西:通常,有一个主文件,其中包含一个加载所有拆分文件的 include 语句。include 语句给出了一个路径 glob 表达式,用于定位其他文件。这些文件的路径与其内容的语义无关。从技术上讲,我们可以用 YAML 做类似的事情。
假设我们想将这个 YAML 文件拆分成多个文件(请原谅我缺乏创造力):
spam:
spam: spam
egg: sausage
baked beans:
- spam
- spam
- bacon
/
可能的转换是这样的(读取以目录结尾的内容,:
开始文件内容):
confdir/
main.yaml:
spam: !include spammap/main.yaml
baked beans: !include beans/
spammap/
main.yaml:
spam: !include spam.yaml
egg: !include egg.yaml
spam.yaml:
spam
egg.yaml:
sausage
beans/
1.yaml:
spam
2.yaml:
spam
3.yaml:
bacon
(在 YAML 中,!include
是一个本地标签。对于大多数实现,您可以为其注册自定义构造函数,从而将整个层次结构加载为单个文档。)
如您所见,我将每个层次结构级别和每个值都放入一个单独的文件中。我使用两种包含:对文件的引用将加载该文件的内容;对目录的引用将生成一个序列,其中每个项目的值是该目录中一个文件的内容,按文件名排序。如您所见,文件名和目录名从来不是内容的一部分,有时我选择不同的名称(例如baked beans
-> beans/
)以避免可能的文件系统问题(在这种情况下,文件名中的空格 - 现在通常不是一个严重的问题) . 另外,我遵守文件扩展名约定(让文件带有.yaml
)。如果您将内容放入文件名中,这将更加古怪。
我在每个级别上命名了起始文件main.yaml
(不需要,beans/
因为它是一个序列)。虽然确切的名称是任意的,但这是在其他几个工具中使用的约定,例如 Python 和__init__.py
Nix 包管理器default.nix
。然后我在这个主文件之外放置了其他文件或目录。
由于包含其他文件是明确的,因此将大部分内容放入单个文件中不是问题。请注意,JSON 缺少 YAML 的标签功能,但您仍然可以浏览已加载的 JSON 文件并预处理诸如{"!include": "path"}
.
总结一下:虽然没有直接的约定如何做你想做的事,但部分问题已经在不同的地方得到了解决,你可以从中继承智慧。
这是一个关于如何使用 PyYAML 执行此操作的最小工作示例。这只是一个概念证明;缺少一些功能(例如,自动生成的文件名将是升序,不支持将列表序列化到目录中)。它显示了在对用户透明的同时存储有关数据布局的信息需要做什么(可以像普通的 dict 结构一样访问数据)。它会记住文件名内容已从这些文件中加载并再次存储到这些文件中。
import os.path
from pathlib import Path
import yaml
from yaml.reader import Reader
from yaml.scanner import Scanner
from yaml.parser import Parser
from yaml.composer import Composer
from yaml.constructor import SafeConstructor
from yaml.resolver import Resolver
from yaml.emitter import Emitter
from yaml.serializer import Serializer
from yaml.representer import SafeRepresenter
class SplitValue(object):
"""This is a value that should be written into its own YAML file."""
def __init__(self, content, path = None):
self._content = content
self._path = path
def getval(self):
return self._content
def setval(self, value):
self._content = value
def __repr__(self):
return self._content.__repr__()
class TransparentContainer(object):
"""Makes SplitValues transparent to the user."""
def __getitem__(self, key):
val = super(TransparentContainer, self).__getitem__(key)
return val.getval() if isinstance(val, SplitValue) else val
def __setitem__(self, key, value):
val = super(TransparentContainer, self).__getitem__(key)
if isinstance(val, SplitValue) and not isinstance(value, SplitValue):
val.setval(value)
else:
super(TransparentContainer, self).__setitem__(key, value)
class TransparentList(TransparentContainer, list):
pass
class TransparentDict(TransparentContainer, dict):
pass
class DirectoryAwareFileProcessor(object):
def __init__(self, path, mode):
self._basedir = os.path.dirname(path)
self._file = open(path, mode)
def close(self):
try:
self._file.close()
finally:
self.dispose() # implemented by PyYAML
# __enter__ / __exit__ to use this in a `with` construct
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.close()
class FilesystemLoader(DirectoryAwareFileProcessor, Reader, Scanner,
Parser, Composer, SafeConstructor, Resolver):
"""Loads YAML file from a directory structure."""
def __init__(self, path):
DirectoryAwareFileProcessor.__init__(self, path, 'r')
Reader.__init__(self, self._file)
Scanner.__init__(self)
Parser.__init__(self)
Composer.__init__(self)
SafeConstructor.__init__(self)
Resolver.__init__(self)
def split_value_constructor(loader, node):
path = loader.construct_scalar(node)
with FilesystemLoader(os.path.join(loader._basedir, path)) as childLoader:
return SplitValue(childLoader.get_single_data(), path)
FilesystemLoader.add_constructor(u'!include', split_value_constructor)
def transp_dict_constructor(loader, node):
ret = TransparentDict()
ret.update(loader.construct_mapping(node, deep=True))
return ret
# override constructor for !!map, the default resolved tag for mappings
FilesystemLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
transp_dict_constructor)
def transp_list_constructor(loader, node):
ret = TransparentList()
ret.append(loader.construct_sequence(node, deep=True))
return ret
# like above, for !!seq
FilesystemLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_SEQUENCE_TAG,
transp_list_constructor)
class FilesystemDumper(DirectoryAwareFileProcessor, Emitter,
Serializer, SafeRepresenter, Resolver):
def __init__(self, path):
DirectoryAwareFileProcessor.__init__(self, path, 'w')
Emitter.__init__(self, self._file)
Serializer.__init__(self)
SafeRepresenter.__init__(self)
Resolver.__init__(self)
self.__next_unique_name = 1
Serializer.open(self)
def gen_unique_name(self):
val = self.__next_unique_name
self.__next_unique_name = self.__next_unique_name + 1
return str(val)
def close(self):
try:
Serializer.close(self)
finally:
DirectoryAwareFileProcessor.close(self)
def split_value_representer(dumper, data):
if data._path is None:
if isinstance(data._content, TransparentContainer):
data._path = os.path.join(dumper.gen_unique_name(), "main.yaml")
else:
data._path = dumper.gen_unique_name() + ".yaml"
Path(os.path.dirname(data._path)).mkdir(exist_ok=True)
with FilesystemDumper(os.path.join(dumper._basedir, data._path)) as childDumper:
childDumper.represent(data._content)
return dumper.represent_scalar(u'!include', data._path)
yaml.add_representer(SplitValue, split_value_representer, FilesystemDumper)
def transp_dict_representer(dumper, data):
return dumper.represent_dict(data)
yaml.add_representer(TransparentDict, transp_dict_representer, FilesystemDumper)
def transp_list_representer(dumper, data):
return dumper.represent_list(data)
# example usage:
# explicitly specify values that should be split.
myData = TransparentDict({
"spam": SplitValue({
"spam": SplitValue("spam", "spam.yaml"),
"egg": SplitValue("sausage", "sausage.yaml")}, "spammap/main.yaml")})
with FilesystemDumper("root.yaml") as dumper:
dumper.represent(myData)
# load values from stored files.
# The loaded data remembers which values have been in which files.
with FilesystemLoader("root.yaml") as loader:
loaded = loader.get_single_data()
# modify a value as if it was a normal structure.
# actually updates a SplitValue
loaded["spam"]["spam"] = "baked beans"
# dumps the same structure as before, with the modified value.
with FilesystemDumper("root.yaml") as dumper:
dumper.represent(loaded)