我该如何在YAML中进行字符串拼接或替换?

67

我有这个:

user_dir: /home/user
user_pics: /home/user/pics

我如何使用 user_dir 存储用户照片?如果我必须指定其他属性,这将不太符合DRY原则。

10个回答

61
您可以使用重复的节点来实现,示例如下:
user_dir: &user_home /home/user
user_pics: *user_home

我认为你无法进行字符串拼接,因此这个方法不会起作用:

user_dir: &user_home /home/user
user_pics: *user_home/pics

3
谢谢你澄清这个问题。我真傻,以为这是可能的。 - Shawn Vader
11
说实话,如果不能用指针来连接,我不明白它们的意义在哪里。。 :/ - Bruno Ambrozio
这个答案展示了如何定义!join以便你可以连接。 - bryan
@BrunoAmbrozio 因为您可能希望您的变量成为yaml文件中多个嵌套结构的一部分,而不想在每个地方复制相同的值并跟踪多个值。 - MattSt

44

令人惊讶的是,由于YAML锚点和引用的目的是将重复内容从YAML数据文件中分离出来,竟然没有一种内置的方法可以使用引用来连接字符串。您的用例是从多个部分构建路径名,这是一个很好的示例。一定有很多类似的用途。

幸运的是,通过用户定义标签,可以很容易地将字符串连接添加到YAML中。

用户定义标签是标准的YAML功能-YAML 1.2规范表示YAML模式允许“使用任意显式标签”。对于这些自定义标签的处理程序需要在每个目标语言中以自定义方式实现。在Python中的实现如下:

## in your python code

import yaml

## define custom tag handler
def join(loader, node):
    seq = loader.construct_sequence(node)
    return ''.join([str(i) for i in seq])

## register the tag handler
yaml.add_constructor('!join', join)

## using your sample data
yaml.load("""
user_dir: &DIR /home/user
user_pics: !join [*DIR, /pics]
""")

这将导致:

{'user_dir': '/home/user', 'user_pics': '/home/user/pics'}

您可以向数组中添加更多项,例如" ""-",如果字符串应该被分隔。

其他编程语言也可以做类似的事情,这取决于它们的解析器能够做什么。

有几条评论是“这似乎是错误的,因为YAML是一种标准的、实现中立的语言”。实际上,YAML不是这样的。YAML是一个框架,用于将YAML模式(由标记组成)映射到特定于实现的数据类型,例如int如何映射到Python、Javascript、C++等。有多个标准的YAML模式,哪些模式受解析器支持是一个实现决策。当有用时,您可以创建具有附加自定义标记的模式,当然这需要额外的解析器实现。添加自定义标记是否是一个好主意取决于您的用例。YAML具有这种能力;是否以及如何应用它取决于您。请谨慎使用 :)


27
这与YAML有什么关系呢?YAML是一种独立于Python、Haskell或其他语言的标记语言。 - Hi-Angel
11
YAML规范说:“使用感叹号(“!”)来表示显式类型...也可以使用特定应用程序的本地标记。” 我提供了符合规范的显式类型的实现。该实现必须使用某种编程语言。我选择了Python,因为原贴作者说他想要一种更加“DRY”的方法来处理他的YAML,而DRY是Python人最常使用的术语。同样的自定义标记也可以在其他编程语言中实现。换句话说,OP所要求的内容在纯YAML中不可用,但可以通过YAML定义的扩展机制来实现。 - Chris Johnson
2
@KenWilliams 好的,我会记住的。所以下次我遇到一个使用 YAML 进行配置的专有应用程序,并且需要字符串连接时,我应该要求作者提供解析器源代码和所有必要的构建系统组件。 - Hi-Angel
4
正如克里斯·约翰逊所说,这是 YAML 规范本身定义的显式扩展机制。不需要刻薄的语气。 - Ken Williams
2
现有的答案不会修改YAML解析器代码。它不假设您可以访问yaml软件包的源代码。它只是使用其公共API来扩展YAML规范允许的方式。 - Ken Williams
显示剩余6条评论

9
如果你正在使用带有PyYaml的Python,那么可以在YAML文件中对字符串进行拼接。不幸的是,这只是Python解决方案,而不是通用解决方案:
使用os.path.join
user_dir: &home /home/user
user_pics: !!python/object/apply:os.path.join [*home, pics]

使用string.join方法(为了完整起见——该方法具有灵活性,可用于多种形式的字符串拼接:

user_dir: &home /home/user
user_pics: !!python/object/apply:string.join [[*home, pics], /]

9
请明确这种方法的含义——您允许Pyyaml解析器从yaml输入文件中执行任意Python代码。如果您无法控制输入源,则这是一种极为危险的安全问题。许多程序将使用“yaml.safe_load()”函数来避免安全问题。在这种情况下,您的代码将无法正常工作。 - Chris Johnson
你有 Python3 字符串拼接的示例吗?“无法找到模块 'str'(没有名为 'str' 的模块)”。 - Jeremy Leipzig
如上面所指出的那样,无法与yaml.safe_load()一起使用。 - dataviews

7
我会使用一个数组,然后使用当前操作系统分隔符将字符串连接在一起,就像这样:
default: &default_path "you should not use paths in config"
pictures:
  - *default_path
  - pics

4

我的yaml文件用作配置文件。我上面粘贴的只是一个示例,用于说明问题。 - Geo
@ArnisL。例如,使用此代码(https://github.com/Hi-Angel/yi/blob/7dd59102a84dec6a1e52f4ddca49d5f259a76fdc/example-configs/yi-emacs-vty-static/stack.yaml)在构建单独模块时定义源代码目录的路径。 - Hi-Angel

3
截至2019年8月:
为了使Chris'的解决方案有效,实际上需要在yaml.load()中添加Loader=yaml.Loader。最终,代码会像这样:
import yaml

## define custom tag handler
def join(loader, node):
    seq = loader.construct_sequence(node)
    return ''.join([str(i) for i in seq])

## register the tag handler
yaml.add_constructor('!join', join)

## using your sample data
yaml.load("""
user_dir: &DIR /home/user
user_pics: !join [*DIR, /pics]
""", Loader=yaml.Loader)

请参考这个 GitHub 问题进行进一步讨论。


3

string.join() 在Python3中不可用,但是你可以像这样定义一个!join

import functools
import yaml

class StringConcatinator(yaml.YAMLObject):
    yaml_loader = yaml.SafeLoader
    yaml_tag = '!join'
    @classmethod
    def from_yaml(cls, loader, node):
        return functools.reduce(lambda a, b: a.value + b.value, node.value)

c=yaml.safe_load('''
user_dir: &user_dir /home/user
user_pics: !join [*user_dir, /pics]''')
print(c)

0
一个类似于@Chris的解决方案,但使用Node.JS:

const yourYaml = `
user_dir: &user_home /home/user
user_pics: !join [*user_home, '/pics']
`;

const JoinYamlType = new jsyaml.Type('!join', {
    kind: 'sequence',
    construct: (data) => data.join(''),    
})

const schema = jsyaml.DEFAULT_SCHEMA.extend([JoinYamlType]);

console.log(jsyaml.load(yourYaml, { schema }));
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>

要在Javascript / NodeJS中使用yaml,我们可以使用js-yaml:

import jsyaml from 'js-yaml';
// or
const jsyaml = require('js-yaml');

0
这是使用Python和ruamel.yaml实现join标签的示例。
from ruamel.yaml import YAML

class JoinTag:
    """a tag to join strings in a list"""

    yaml_tag = u'!join'

    @classmethod
    def from_yaml(cls, constructor, node):
        seq = constructor.construct_sequence(node)
        return ''.join([str(i) for i in seq])

    @classmethod
    def to_yaml(cls, dumper, data):
        # do nothing
        return dumper.represent_sequence(cls.yaml_tag, data)

    @classmethod
    def register(cls, yaml: YAML):
        yaml.register_class(cls)


if __name__ == '__main__':
    import io
    f = io.StringIO('''\
base_dir: &base_dir /this/is/a/very/very/long/path/
data_file: !join [*base_dir, data.csv]
    ''')
    yaml = YAML(typ='safe')
    JoinTag.register(yaml)
    print(yaml.load(f))

输出将是:

{'base_dir': '/this/is/a/very/very/long/path/', 'data_file': '/this/is/a/very/very/long/path/data.csv'}

0

yaml文件支持变量替换,但默认情况下采用惰性执行的方法。

yaml文件中的变量替换语法为

# this is test.yaml file and its contents.
server:
  host: localhost
  port: 80

client:
  url: http://${server.host}:${server.port}/
  server_port: ${server.port}
  # relative interpolation
  description: Client of ${.url}

如果我们使用这种默认的慵懒方法:
from omegaconf import OmegaConf

conf = Omegaconf.load("test.yaml")

print(f"type: {type(conf).__name__}, value: {repr(conf)}")
print(f"url: {conf.client.url}\n")
print(f"server_port: {conf.client.server_port}\n")
print(f"description: {conf.client.description}\n")

输出:

type: DictConfig, value: {'server': {'host': 'localhost', 'port': 80}, 'client': {'url': 'http://${server.host}:${server.port}/', 'server_port': '${server.port}', 'description': 'Client of ${.url}'}}

url: http://localhost:80/
server_port: 80
description: Client of http://localhost:80/

注意现在当我们访问和打印变量的值时,变量已被替换。

但是当我们想要将整个字典作为参数传递时,我们应该使用这种方法:

from omegaconf import OmegaConf

conf = Omegaconf.load("test.yaml")
conf = OmegaConf.to_container(conf, resolve=True)
print(conf)

# output
type: dict, value: {'server': {'host': 'localhost', 'port': 80}, 'client': {'url': 'http://localhost:80/', 'server_port': 80, 'description': 'Client of http://localhost:80/'}}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接