OpenCode 实践课:3 分钟用 Python 写出一个命令行 JSON 数据查询器

项目介绍

你是否经常需要在一大坨 JSON 里找某个嵌套字段的值?用 grep 不精准,用 jq 语法又太难记。今天我们用 OpenCode 花 3 分钟写一个轻量级命令行 JSON 查询器——用直观的点号路径(如 users.0.name)就能快速提取或修改 JSON 中的任意嵌套值。

技术栈:Python 3,纯标准库(argparse + json),零依赖。
最终代码:约 90 行,完整可运行。

最终效果:

$ python jsonquery.py sample.json users.0.name
张三

$ python jsonquery.py sample.json config.limits -p
{
  "max_upload": 50,
  "max_connections": 100
}

$ python jsonquery.py sample.json -k
users
config

准备工作

  • Python 3.6+ 即可
  • 安装 OpenCode(本文用 opencode CLI 模式)
  • 一个测试用的 JSON 文件(项目会自动创建)

在终端输入 opencode,看到交互界面后就可以开始了。

实践过程

步骤一:搭骨架——核心查询函数

我对 OpenCode 说:

> 写一个 Python 函数 json_get(obj, path),obj 是 dict 或 list,path 是点号分隔的字符串如 "users.0.name"。要求支持字典的字符串 key 和列表的数字索引,找不到时返回 None。

OpenCode 生成了:

def json_get(obj, path):
    keys = path.split(".")
    current = obj
    for key in keys:
        if isinstance(current, list):
            try:
                current = current[int(key)]
            except (ValueError, IndexError):
                return None
        elif isinstance(current, dict):
            current = current.get(key)
            if current is None:
                return None
        else:
            return None
    return current

寥寥 14 行,逻辑清晰:按点号切分路径 → 逐层递进 → 列表用数字下标、字典用 key 查询。OpenCode 很聪明地处理了异常边界——比如路径不存在或类型不匹配时返回 None。

步骤二:加修改能力——set 功能

光能查还不够,有时候我们需要把某个字段改了再输出整个 JSON。

我对 OpenCode 说:

> 再加一个 json_set(obj, path, value) 函数,支持同样的点号路径,在指定位置设置新值,返回布尔值表示成功或失败。

OpenCode 生成了:

def json_set(obj, path, value):
    keys = path.split(".")
    current = obj
    for key in keys[:-1]:
        if isinstance(current, list):
            try:
                current = current[int(key)]
            except (ValueError, IndexError):
                return False
        elif isinstance(current, dict):
            if key not in current:
                return False
            current = current[key]
        else:
            return False

    last = keys[-1]
    if isinstance(current, list):
        current[int(last)] = value
    elif isinstance(current, dict):
        current[last] = value
    return True

OpenCode 的亮点:它知道遍历到倒数第二个 key 就停下,然后再根据最后一级的容器类型来决定是 current[int(last)] = value 还是 current[last] = value。这种细节如果是手写可能会漏掉。

步骤三:用 argparse 封装成 CLI

核心逻辑有了,下一步是让它变成真正的命令行工具。

我对 OpenCode 说:

> 用 argparse 把这两个函数封装成命令行工具。支持参数:
> - 位置参数 file(可选,不传从 stdin 读)
> - 位置参数 path
> - -s / --set:设置值
> - -p / --pretty:格式化输出
> - -k / --keys:列出顶层 key
> 另外从 stdin 读时用 sys.stdin。

OpenCode 生成了一个完整的 main() 函数,包括从文件/stdin 读取 JSON、根据参数分发到不同模式,这里摘录关键部分:

parser = argparse.ArgumentParser(description="用点号路径查询和修改 JSON 数据")
parser.add_argument("file", nargs="?", help="JSON 文件路径(省略则从 stdin 读取)")
parser.add_argument("path", nargs="?", help='查询路径,如 "users.0.name"')
parser.add_argument("-s", "--set", help="设置指定路径的值")
parser.add_argument("-p", "--pretty", action="store_true", help="格式化输出")
parser.add_argument("-k", "--keys", action="store_true", help="列出顶层键")

设置模式尤其聪明——它会先用 json.loads() 解析用户传入的值字符串,然后调用 json_set,最后把整个修改后的 JSON 原样打印出来,这样就能通过管道传给下一个命令:

$ python jsonquery.py sample.json status --set '"offline"' | curl -X POST -d @- http://api.example.com/update

步骤四:验证与测试

代码写完了,一般还得自己测。但我直接对 OpenCode 说:

我对 OpenCode 说:

> 写一个 sample.json 文件,包含嵌套的 users 数组和 config 对象,然后分别测试查询单个值、列出全部键、格式化输出嵌套对象这三个功能。

OpenCode 一秒写完测试数据和验证命令,运行结果一切正常——users.0.name 返回"张三",-k 列出 usersconfigconfig.limits -p 格式化输出两个限制参数。

<details>
<summary>点击展开测试用 sample.json</summary>

{
  "users": [
    {"name": "张三", "email": "zhangsan@example.com", "roles": ["admin", "editor"]},
    {"name": "李四", "email": "lisi@example.com", "roles": ["viewer"]}
  ],
  "config": {
    "version": "1.0",
    "debug": true,
    "limits": {"max_upload": 50, "max_connections": 100}
  }
}

</details>

完整代码

<details>
<summary>jsonquery.py(90 行,点击展开)</summary>

#!/usr/bin/env python3
"""JSON Path Query Tool - 用点号路径查询和修改 JSON 数据"""

import json
import sys
import argparse


def json_get(obj, path):
    keys = path.split(".")
    current = obj
    for key in keys:
        if isinstance(current, list):
            try:
                current = current[int(key)]
            except (ValueError, IndexError):
                return None
        elif isinstance(current, dict):
            current = current.get(key)
            if current is None:
                return None
        else:
            return None
    return current


def json_set(obj, path, value):
    keys = path.split(".")
    current = obj
    for key in keys[:-1]:
        if isinstance(current, list):
            try:
                current = current[int(key)]
            except (ValueError, IndexError):
                return False
        elif isinstance(current, dict):
            if key not in current:
                return False
            current = current[key]
        else:
            return False

    last = keys[-1]
    if isinstance(current, list):
        current[int(last)] = value
    elif isinstance(current, dict):
        current[last] = value
    return True


def main():
    parser = argparse.ArgumentParser(description="用点号路径查询和修改 JSON 数据")
    parser.add_argument("file", nargs="?", help="JSON 文件路径(省略则从 stdin 读取)")
    parser.add_argument("path", nargs="?", help='查询路径,如 "users.0.name"')
    parser.add_argument("-s", "--set", help="设置指定路径的值")
    parser.add_argument("-p", "--pretty", action="store_true", help="格式化输出")
    parser.add_argument("-k", "--keys", action="store_true", help="列出顶层键")

    args = parser.parse_args()

    if args.file:
        with open(args.file, "r", encoding="utf-8") as f:
            data = json.load(f)
    else:
        data = json.load(sys.stdin)

    if args.keys:
        if isinstance(data, dict):
            for k in data:
                print(k)
        elif isinstance(data, list):
            for i in range(len(data)):
                print(i)
        return

    if args.set is not None:
        value = json.loads(args.set)
        if json_set(data, args.path, value):
            print(json.dumps(data, indent=2, ensure_ascii=False))
        else:
            print(f"错误: 无法设置路径 '{args.path}'", file=sys.stderr)
            sys.exit(1)
        return

    result = json_get(data, args.path)
    if result is None:
        print("null")
    elif args.pretty:
        print(json.dumps(result, indent=2, ensure_ascii=False))
    else:
        if isinstance(result, (dict, list)):
            print(json.dumps(result, ensure_ascii=False))
        else:
            print(result)


if __name__ == "__main__":
    main()

</details>

小结

这个 JSON 查询器从头到尾写了 4 轮对话,每次就一句自然语言描述需求。对比一下效率:

| 环节 | OpenCode 方式 | 传统手写方式 |
|------|-------------|------------|
| 调研 API | 0 分钟(自动选用 json + argparse) | 5 分钟(查文档) |
| 核心代码 | 14 行,30 秒生成 | 14 行,约 5 分钟(含调试边界) |
| argparse 封装 | 40 行,1 分钟生成 | 40 行,约 10 分钟 |
| 测试验证 | 1 句话生成测试数据 + 命令 | 手动构造数据,约 5 分钟 |
| 总耗时 | ~3 分钟 | ~25 分钟 |

OpenCode 最让我舒服的一点是:不用记 API。无论 json.loads 的参数还是 argparse.ArgumentParser 的用法,你只管描述需求,它负责查文档、写代码、处理边界。你真正花时间的是思考"我要做什么",而不是"这个函数第三个参数该传什么"。

下篇预告:用 OpenCode 开发现代 CLI 工具的必修课——带颜色输出、进度条和 spinner 的终极大改造。