Python处理yaml和嵌套数据结构的一些技巧

本文介绍了如何使用Python在保留注释的情况下修改yaml文件,以及如何通过正则表达式、语法树解析和递归算法来定位和修改嵌套数据结构。文章详细展示了从正则表达式的简单替换到解析语法树的复杂查找,再到处理深层嵌套数据结构的递归方法,提供了一套完整的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分享一些用Python处理yaml和嵌套数据结构的的一些技巧,首先从修改yaml格式文件的问题出发,演变出了各个解决办法,又从最后的解决办法中引申出了普适性更强的嵌套数据结构的定位方法。

  1. 保留注释修改yaml文件
  2. 定位嵌套数据结构
  3. 定位嵌套数据结构2

保留注释修改yaml文件

yaml比之json文件的其中一个区别就是可以注释,这些注释有时候是很重要的内容,就像代码中的注释一样,如果是手动编辑自然是没有问题的,那么如何在保留注释的情况下用代码修改yaml文件呢?

假设我们要修改的yaml文件如下:

# 主要维护人
name: zhangsan

# 各集群运维人员
cluster1:
  node1:
    tomcat: user11

cluster2:
  node1:
    tomcat: user21

不保留注释

为了演示处理yaml的各个方法,这里把不保留注释的方法也纳入到本文了。

def ignore_comment():
    data = yaml.load(text, Loader=yaml.Loader)
    data["name"] = "wangwu"
    print(yaml.dump(data))

输出如下:

cluster1:
  node1:
    tomcat: user11
cluster2:
  node1:
    tomcat: user21
name: wangwu

很显然,这不是我们要的结果, 那么就淘汰这个方法吧。

此方法只适用于不需要保留注释的修改。

正则表达式

既然load, dump方法会丢弃注释,那么用正则表达式不就可以了么,处理文本一定有正则表达式一席之地的。

假设还是将name: zhangsan改成name: wangwu

def regex1():
    pattern = "name:\s+\w+"
    pat = re.compile(pattern=pattern)
    # 首先匹配到对应的字符串
    sub_text = pat.findall(text)[0]
    # 根据这个字符串找到在文本的位置
    start_index = text.index(sub_text)
    # 根据起始位置计算结束位置
    end_index = start_index + len(sub_text)
    print(start_index, end_index, text[start_index:end_index])

    # 将根据索引替换内容
    replace_text = "name: wangwu"
    new_text = text[:start_index] + replace_text + text[end_index:]
    print("="*10)
    print(new_text)

输出如下:

8 22 name: zhangsan
==========
# 主要维护人
name: wangwu

# 各集群运维人员
cluster1:
  node1:
    tomcat: user11

cluster2:
  node1:
    tomcat: user21

看起来不错,好像能够满足需求,但是这里有一个问题就是,假设修改是cluster2.node1.tomcat的值呢?

因为文本中有两个tomcat的值,所以只是通过正则表达式不能一击即中,需要多一些判断条件,比如首先找到cluster2的起始位置,然后过滤掉小于这个起始位置的索引值,但是如果还有cluster3,cluster4呢?总的来说还 是需要人工的过一遍,然后根据观察结果来编写正则表达式,但是这样太不智能,太不自动了。

此方法适用于比较容器匹配的文本。

语法树

其实整个文本的数据结构大致如下:

tree.png

无论是编程语言还是数据文本,如json, yaml, toml都可以得到这样的语法树,通过搜索这颗语法树,我们就能找到对应的键值对。

def tree1():
    tree = yaml.compose(text)
    print(tree)

输出如下:

MappingNode(tag='tag:yaml.org,2002:map', value=[(ScalarNode(tag='tag:yaml.org,2002:str', value='name'), ScalarNode(tag='tag:yaml.org,2002:str', value='zhangsan')), (ScalarNode(tag='tag:yaml.org,2002:str', value='cluster1'), MappingNode(tag='tag:yaml.org,2002:map', value=[(ScalarNode(tag='tag:yaml.org,2002:str', value='node1'), MappingNode(tag='tag:yaml.org,2002:map', value=[(ScalarNode(tag='tag:yaml.org,2002:str', value='tomcat'), ScalarNode(tag='tag:yaml.org,2002:str', value='user11'))]))])), (ScalarNode(tag='tag:yaml.org,2002:str', value='cluster2'), MappingNode(tag='tag:yaml.org,2002:map', value=[(ScalarNode(tag='tag:yaml.org,2002:str', value='node1'), MappingNode(tag='tag:yaml.org,2002:map', value=[(ScalarNode(tag='tag:yaml.org,2002:str', value='tomcat'), ScalarNode(tag='tag:yaml.org,2002:str', value='user21'))]))]))])

通过yaml.compose方法我们就能得到一颗节点树,并且每个节点会包括该节点的文本信息,比如起始,终止的文本索引。

通过观察我们能找到name: zhangsan的两个节点, 键name是一个ScalarNode节点, 值zhangsan也是一个ScalarNode, 所以我们可以打印一下看看是否和正则表达式的结果一致。

def tree2():
    tree = yaml.compose(text)
    key_name_node = tree.value[0][0]
    value_name_node = tree.value[0][1]
    print(key_name_node.start_mark.pointer, value_name_node.end_mark.pointer, key_name_node.value, value_name_node.value)

输出如下:

8 22 name zhangsan

结果与正则表达式一致,所以说明这种方法可行并且准确。

得到了修改文本的索引位置,就可以替换了,这里就不再演示了。

此方法适合保留注释的修改,并且定位嵌套结构较之正则表达式要简单,并且不需要人工介入。

那么如何定位嵌套结构呢?

定位嵌套数据结构

从上一节我们了解到了数据结构可以抽象成一颗语法树, 那么利用一些树的搜索算法就可以定位到目标文本了。

这里展示一下不包含列表节点的搜索算法。

def find_slice(tree: yaml.MappingNode, keys: List[str]) -> Tuple[Tuple[int, int], Tuple[int, int]]:
    """
    找到yaml文件中对应键值对的索引, 返回一个((key起始索引, key结束索引+1), (value起始索引, value结束索引+1))的元组
    暂时只支持键值对的寻找.

    比如:
    >>> find_slice("name: zhangsan", ["name"])
    ((0, 4), (6, 14))
    """
    if isinstance(tree, str):
        tree = yaml.compose(tree, Loader=yaml.Loader)
    assert isinstance(tree, yaml.MappingNode), "未支持的yaml格式"
    target_key = keys[0]
    for node in tree.value:
        if target_key == node[0].value:
            key_node, value_node = node
            if len(keys) == 1:
                key_pointers = (key_node.start_mark.pointer, key_node.end_mark.pointer)
                value_pointers = (value_node.start_mark.pointer, value_node.end_mark.pointer)
                return (key_pointers, value_pointers)

            return find_slice(node[1], keys[1:])

    return ValueError("没有找到对应的值")

算法核心在于递归。

这里的实现并没有处理列表节点(SequenceNode)。

假设我们要找cluster1.node1.tomcat并将其值改成changed, 代码如下:

def tree3():
    slices = find_slice(text, ["cluster1", "node1", "tomcat"])
    value_start_index, value_end_index = slices[1]
    replace_text = "changed"
    new_text = text[:value_start_index] + replace_text + text[value_end_index:]
    print(new_text)

输出如下

# 主要维护人
name: zhangsan

# 各集群运维人员
cluster1:
  node1:
    tomcat: changed

cluster2:
  node1:
    tomcat: user21

上面的算法只能定位key-value类型的数据结构,现在在此优化一下,让其 支持序列。

def find_slice2(tree: yaml.MappingNode, keys: List[str]) -> Tuple[Tuple[int, int], Tuple[int, int]]:
    """
    找到yaml文件中对应键值对的索引, 返回一个((key起始索引, key结束索引+1), (value起始索引, value结束索引+1))的元组
    暂时只支持键值对的寻找.

    比如:
    >>> find_slice2("name: zhangsan", ["name"])
    ((0, 4), (6, 14))
    """
    if isinstance(tree, str):
        tree = yaml.compose(tree, Loader=yaml.Loader)
    target_key = keys[0]

    assert isinstance(tree, yaml.MappingNode) or isinstance(tree, yaml.SequenceNode), "未支持的yaml格式"

    ret_key_node = None
    ret_value_node = None
    value_pointers= (-1, -1)

    if isinstance(tree, yaml.SequenceNode):
        assert isinstance(target_key, int), "错误的数据格式"
        # 索引可以是负索引, 比如[1,2,3][-1]
        if len(tree.value) < abs(target_key):
            raise IndexError("索引值大于列表长度")

        node = tree.value[target_key]
        if len(keys) > 1:
            return find_slice2(tree.value[target_key], keys[1:])

        if isinstance(node, yaml.MappingNode):
            ret_key_node, ret_value_node = node.value[0]
        else:
            ret_key_node = node

    if isinstance(tree, yaml.MappingNode):
        for node in tree.value:
            if target_key == node[0].value:
                key_node, value_node = node
                if len(keys) > 1:
                    return find_slice2(node[1], keys[1:])
                ret_key_node = key_node
                ret_value_node = value_node

    if ret_key_node:
        key_pointers = (ret_key_node.start_mark.pointer, ret_key_node.end_mark.pointer)

    if ret_value_node:
        value_pointers = (ret_value_node.start_mark.pointer, ret_value_node.end_mark.pointer)

    if ret_key_node:
        return (key_pointers, value_pointers)

    return ValueError("没有找到对应的值")

假设yaml文件如下:

# 用户列表
users:
  - user1: wangwu
  - user2: zhangsan

# 集群中间件版本
cluster:
  - name: tomcat
    version: 9.0.63
  - name: nginx
    version: 1.21.6

然后运行代码:

def tree4():
    slices = find_slice2(text2, ["cluster", 1, "version"])
    value_start_index, value_end_index = slices[1]
    replace_text = "1.22.0"
    new_text = text2[:value_start_index] + replace_text + text2[value_end_index:]
    print(new_text)

输出如下:

# 用户列表
users:
  - user1: wangwu
  - user2: zhangsan

# 集群中间件版本
cluster:
  - name: tomcat
    version: 9.0.63
  - name: nginx
    version: 1.22.0

结果符合预期。

定位嵌套数据结构2

上面介绍了如何定位嵌套的数据结构树,这一节介绍一下如何定位较深的树结构(主要指python字典)。

链式调用get

在获取api数据的时候因为想要的数据结构比较深,用索引会报错,那么就 需要捕获异常,这样很麻烦,并且代码很冗长,比如:

data1 = {"message": "success", "data": {"limit": 0, "offset": 10, "total": 100, "data": ["value1", "value1"]}}
data2 = {"message": "success", "data": None}
data3 = {"message": "success", "data": {"limit": 0, "offset": 10, "total": 100, "data": None}}

上面的数据结构很有可能来自同一个api结构,但是数据结构却不太一样。

如果直接用索引,就需要捕获异常,这样看起来很烦,那么可以利用字典的get方法。

ret = data1.get("data", {}).get("data", [])
if ret:
    pass # 做一些操作
if data2.get("data"):
    ret = data2["data"].get("data", [])
ret = data3.get("data", {}).get("data", [])

通过给定一个预期的数据空对象,让get可以一致写下去。

写一个递归的get

起始在之前的find_slice方法中,我们就发现递归可以比较好的处理这种嵌套的数据结构,我们可以写一个递归处理函数,用来处理很深的数据结构。

假设数据结构如下:

data = {"message": "success", "data": {"data": {"name": "zhangsan", "scores": {"math": {"mid-term": 88, "end-of-term": 90}}}}}

我们的目标就是获取数据中张三期中数学成绩: 88

实现的递归调用如下:

def super_get(data: Union[dict, list], keys: List[Union[str, int]]):
    assert isinstance(data, dict) or isinstance(data, list), "只支持字典和列表类型"
    key = keys[0]

    if isinstance(data, list) and isinstance(key, int):
        try:
            new_data = data[key]
        except IndexError as exc:
            raise IndexError("索引值大于列表长度") from exc
    elif isinstance(data, dict) and isinstance(key, str):
        new_data = data.get(key)
    else:
        raise ValueError(f"数据类型({type(data)})与索引值类型(f{type(key)}不匹配")

    if len(keys) == 1:
        return new_data

    if not isinstance(new_data, dict) and not isinstance(new_data, list):
        raise ValueError("找不到对应的值")
    return super_get(new_data, keys[1:])

然后执行代码:

def get2():
    data = {"message": "success", "data": {"data": {"zhangsan": {"scores": {"math": {"mid-term": 88, "end-of-term": 90}}}}}}
    print(super_get(data, ["data", "data", "zhangsan", "scores", "math", "mid-term"]))
    # 输出 88
    data = {"message": "success", "data": {"data": {"zhangsan": {"scores": {"math": [88, 90]}}}}}
    print(super_get(data, ["data", "data", "zhangsan", "scores", "math", 0]))
    # 输出 88
    data = {"message": "success", "data": {"data": {"zhangsan": {"scores": {"math": [88, 90]}}}}}
    print(super_get(data, ["data", "data", "zhangsan", "scores", "math", -1]))
    # 输出 90 

第三方库

其实有语法比较强大的库,比如jq, 但是毕竟多了一个依赖,并且需要一定的学习成本,但是,如果确定自己需要更多的语法,那么可以去安装一下第三方库。

总结

如果遇到较深的嵌套,递归总能很好的解决,如果实在想不出比较好的算法,那就找个第三方库吧,调库嘛,不寒碜。

<think>我们已知:用户想查找如何使用Python查询YAML数据的方法或代码示例根据引用内容,我们知道:-引用[1][4]提供了基本的读取YAML文件的方法,使用`yaml.safe_load`将YAML文件解析为Python对象(通常是字典或列表)。-引用[3]提到PyYAML模块可以将YAML转换为自定义Python对象,而不仅仅是字典或内置类型。-引用[5]提供了关于如何保留原格式以及处理中文编码的注意事项,同时也提到了dump方法。用户强调“查询YAML数据”,意味着在解析后,需要对解析得到的Python数据结构进行访问操作。因此,回答应该包含:1.解析YAML文件的基本步骤(使用pyyaml库,读取文件,解析为Python对象)。2.解析后的Python对象通常是字典或列表,因此可以按照Python中操作字典列表的方式进行查询。3.如果YAML结构较深,可能需要使用递归或路径查询,但Python标准库没有提供专门的YAML查询语言(如XPathforXML),所以主要依靠对字典列表的操作。另外,引用[2]虽然提到了饼状图流程,但似乎与查询无关,可能只是示例中的一部分,我们可以忽略。步骤:(1)安装PyYAML库(如果未安装)(2)使用`yaml.safe_load`加载YAML文件,得到数据(通常为字典或列表)(3)通过键或索引访问数据(若嵌套则逐层访问)示例代码:假设有一个YAML文件(example.yaml)如下:```yamluser:name:JohnDoeage:30address:street:123MainStcity:Anytown```我们解析后查询其中的数据:```pythonimportyamlwithopen('example.yaml','r',encoding='utf-8')asfile:data=yaml.safe_load(file)#查询用户名称user_name=data['user']['name']print(user_name)#输出:JohnDoe#查询城市city=data['user']['address']['city']print(city)#输出:Anytown```注意事项:-引用[5]提到编码问题:如果YAML中有中文,需要确保文件以UTF-8编码打开(使用`encoding='utf-8'`)。-如果数据中某些键可能不存在,可以使用`get`方法避免KeyError。另外,如果用户希望像XPath那样查询,可以考虑使用第三方库如`jsonpath-ng`(虽然为JSON设计,但YAML解析后也是Python对象,同样适用)或者使用递归函数遍历。但是,由于用户要求的是“查询”,基本方法就是上述的字典列表操作。对于复杂查询,可以自定义函数或使用递归。由于用户没有要求复杂查询,我们先提供基础方法。相关问题建议:根据用户的问题,可以生成以下相关问题:1.如何安装PyYAML库?2.如何处理YAML文件中的嵌套数据结构?3.如果YAML数据中有数组,如何遍历查询?4.如何将YAML数据转换为自定义Python对象?5.如何处理YAML数据中的中文?根据以上思路,组织回答。</think> ### Python解析与查询YAML文件数据的方法 根据引用内容,Python解析查询YAML数据主要通过PyYAML库实现,以下是详细步骤代码示例: #### **1. 基础解析方法** ```python import yaml # 读取YAML文件 with open('config.yaml', 'r', encoding='utf-8') as file: data = yaml.safe_load(file) # 安全加载,避免执行恶意代码 # 打印解析结果 print(data) ``` - `yaml.safe_load()` 将YAML转为Python字典/列表结构 - 支持嵌套结构访问:`data['user']['address']['city']`[^1][^5] #### **2. 数据查询操作** ```python # 示例YAML内容 # user: # name: 张三 # roles: [admin, editor] # settings: # dark_mode: true # 查询具体值 print("用户名:", data['user']['name']) # 输出: 张三 print("是否暗黑模式:", data['user']['settings']['dark_mode']) # 遍历列表数据 for role in data['user']['roles']: print("角色:", role) ``` #### **3. 高级查询技巧** ```python # 通过路径查询(需安装pip install python-box) from box import Box conf = Box(data, box_dots=True) print(conf.user.settings.dark_mode) # 点操作符访问 # 处理不存在的键(避免KeyError) print(data.get('server', {}).get('port', 8080)) # 默认值8080 ``` #### **4. 特殊场景处理** ```python # 保留原始格式(锚点/引用) from yaml import CLoader with open('config.yaml') as f: raw_data = yaml.load(f, Loader=CLoader) # 需LibYAML支持[^4] # 中文编码处理[^5] with open('config.yaml', 'r', encoding='utf-8') as f: content = f.read().encode('utf-8') data = yaml.safe_load(content) ``` > **关键点总结**: > - 基础查询通过字典键路径实现(`data['a']['b']`) > - 使用`.get()`方法避免KeyError异常 > - 中文文件必须指定`encoding='utf-8'`[^5] > - 需解析器支持`Loader=CLoader`(性能更好)[^4]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值