← 返回博客首页

JSON 数据转换技巧:jq、JSONPath、映射、过滤与归约实战

为什么需要 JSON 转换?

在现代开发中,JSON 数据来自各种来源——API 响应、配置文件、数据库、日志。原始数据往往不能直接使用,需要经过转换才能满足业务需求。常见的转换场景包括:

  • 从嵌套结构中提取特定字段
  • 过滤符合条件的记录
  • 将数据从一种结构映射为另一种
  • 聚合统计(求和、分组、计数)
  • 多数据源合并

jq:命令行 JSON 处理利器

jq 是处理 JSON 的瑞士军刀,支持过滤、映射、转换,语法简洁且功能强大。

基础语法

# 格式化输出
echo '{"name":"张三","age":30}' | jq '.'

# 提取字段
echo '{"name":"张三","age":30}' | jq '.name'
# "张三"

# 提取嵌套字段
echo '{"user":{"name":"张三"}}' | jq '.user.name'

数组操作

# 数组所有元素
echo '[1,2,3,4,5]' | jq '.[]'
# 1 2 3 4 5

# 数组切片
echo '[1,2,3,4,5]' | jq '.[1:3]'
# [2,3]

# map 映射
echo '[{"name":"张三","age":30},{"name":"李四","age":25}]' | jq 'map(.name)'
# ["张三","李四"]

# select 过滤
echo '[{"name":"张三","age":30},{"name":"李四","age":25}]' | jq '.[] | select(.age > 28)'
# {"name":"张三","age":30}

高级转换

# 构造新对象
echo '[{"name":"张三","age":30}]' | jq 'map({username: .name, years: .age})'
# [{"username":"张三","years":30}]

# 分组统计
echo '[{"dept":"技术","salary":10000},{"dept":"技术","salary":12000},{"dept":"市场","salary":8000}]' \
  | jq 'group_by(.dept) | map({dept: .[0].dept, total: (map(.salary) | add)})'
# [{"dept":"技术","total":22000},{"dept":"市场","total":8000}]

# 键值转换
echo '{"a":1,"b":2,"c":3}' | jq 'to_entries | map({(.key): (.value * 10)}) | add'
# {"a":10,"b":20,"c":30}

JSONPath 查询语言

JSONPath 是类似 XPath 的 JSON 查询表达式,广泛用于编程库和 API 查询中。

基本语法

表达式 说明
$ 根对象
.property 访问属性
['property'] 访问属性(带特殊字符时使用)
[index] 数组索引
[start:end] 数组切片
[*] 所有元素
.. 递归下降
?(filter) 过滤表达式

查询示例

给定数据:

{
  "store": {
    "books": [
      {"title": "深入理解计算机系统", "price": 99, "category": "技术"},
      {"title": "代码大全", "price": 108, "category": "技术"},
      {"title": "小王子", "price": 25, "category": "文学"}
    ]
  }
}
// 所有书名
$.store.books[*].title

// 价格大于 50 的书
$.store.books[?(@.price > 50)]

// 技术类书籍
$.store.books[?(@.category == '技术')]

// 递归查找所有 price
$..price

各语言实现

  • JavaScriptjsonpath-plusjsonpath
  • Pythonjsonpath-ng
  • JavaJayway JsonPath
  • Gogithub.com/PaesslerAG/jsonpath

映射(Map)

映射是将数据集合中的每个元素转换为新形式的过程。

JavaScript

const users = [
  { id: 1, name: '张三', email: 'zhang@example.com' },
  { id: 2, name: '李四', email: 'li@example.com' }
];

// 提取姓名列表
const names = users.map(u => u.name);
// ["张三", "李四"]

// 转换为以 ID 为键的对象
const userMap = Object.fromEntries(users.map(u => [u.id, u]));
// {1: {...}, 2: {...}}

// 结构重塑
const dtos = users.map(({ id, name }) => ({ userId: id, displayName: name }));

Python

users = [
    {"id": 1, "name": "张三", "email": "zhang@example.com"},
    {"id": 2, "name": "李四", "email": "li@example.com"}
]

names = [u["name"] for u in users]
user_map = {u["id"]: u for u in users}

过滤(Filter)

过滤是保留满足条件的元素。

const products = [
  { name: '键盘', price: 200, inStock: true },
  { name: '鼠标', price: 80, inStock: false },
  { name: '显示器', price: 1500, inStock: true }
];

// 有库存且价格低于 1000
const affordable = products.filter(p => p.inStock && p.price < 1000);
// [{ name: '键盘', ... }]

归约(Reduce)

归约将集合累积为单个值,用于求和、分组、扁平化等场景。

const orders = [
  { product: '键盘', qty: 2, price: 200 },
  { product: '鼠标', qty: 3, price: 80 },
  { product: '键盘', qty: 1, price: 200 }
];

// 总金额
const total = orders.reduce((sum, o) => sum + o.qty * o.price, 0);
// 1040

// 按商品分组统计数量
const grouped = orders.reduce((acc, o) => {
  acc[o.product] = (acc[o.product] || 0) + o.qty;
  return acc;
}, {});
// { 键盘: 3, 鼠标: 3 }

// 扁平化嵌套数组
const nested = [[1, 2], [3, 4], [5]];
const flat = nested.reduce((acc, arr) => acc.concat(arr), []);
// [1, 2, 3, 4, 5]

实战案例:API 数据转换

场景:将分页 API 响应转换为前端表格数据

后端返回:

{
  "code": 200,
  "data": {
    "total": 100,
    "page": 1,
    "list": [
      {"user_id": 1, "user_name": "张三", "created_at": "2025-07-01T10:00:00Z"},
      {"user_id": 2, "user_name": "李四", "created_at": "2025-07-02T11:30:00Z"}
    ]
  }
}

转换为目标格式:

function transformResponse(resp) {
  return {
    total: resp.data.total,
    currentPage: resp.data.page,
    rows: resp.data.list.map(item => ({
      id: item.user_id,
      name: item.user_name,
      date: new Date(item.created_at).toLocaleDateString('zh-CN')
    }))
  };
}

场景:树形结构扁平化

function flattenTree(nodes, parentId = null) {
  return nodes.reduce((acc, node) => {
    const flatNode = { id: node.id, name: node.name, parentId };
    acc.push(flatNode);
    if (node.children) {
      acc.push(...flattenTree(node.children, node.id));
    }
    return acc;
  }, []);
}

性能建议

  • 处理大型 JSON(>10MB)时,优先使用流式解析,避免一次性加载到内存
  • jq 处理大文件时加 --slurp 或使用流模式 --stream
  • 前端深层遍历注意时间复杂度,避免在渲染循环中做复杂转换
  • 对于固定结构的转换,考虑使用 JSON Schema + 代码生成工具

工具推荐

工具 类型 适用场景
jq 命令行 Shell 脚本、快速调试
JSONPath Plus JS 库 复杂查询表达式
Jayway JsonPath Java 库 Spring 项目集成
JMESPath 查询语言 AWS CLI、通用查询
jsonata 表达式语言 复杂转换规则
← 返回博客首页