墨穗app.notebase.cn
控制台
内容库
动态
管理
账户
U
用户
--
在线
v1.0.165 · 墨穗笔记
笔记

Notebase墨穗
静水流深,落墨成穗。

0笔记
0工具
30推荐

分类导航

按主题直达

编辑精选

站内用户贡献 · 真实笔记

最新收录

每日更新
继续浏览全部内容 →

笔记

0
加载中...

工具

0

此页用于记录用户反馈问题后的每一次改进

关于

笔记用法

“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。

md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。

要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。

工具用法

在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。

要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。

工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。

隐藏笔记

写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。

适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。

不想公开、只想临时给人看:点“分享”生成一条带密码和有效期的链接,到期自动失效,你也能随时撤销。

数据安全

你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。

技术

全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。

理念

这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。

原则

不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。

更多

产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。

举报

如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。

趋势

// 点击导航加载发现
归档
// 归档为空
最近浏览
// 暂无浏览记录
发布
// 加载中...
用户发布
// 加载中...
用户管理
// 加载中...
访问统计
// 加载中...
内容审核
// 加载中...
个人信息
// 加载中...
返回首页

我给一个免费的浏览器端转换工具加上了嵌套CSV转JSON的支持

2026-07-08编程开发

最近我折腾了一下自己写的那个小工具 JSON Utility Kit,给它加了个挺实用的功能——支持把带点号(dot notation)的CSV表头转成嵌套的JSON对象。这事儿其实挺常见的,比如你有个CSV,列名叫 user.name、user.email、order.id,正常转成JSON之后全是扁平的键,像 {"user.name": "xxx"},看着别扭,用起来更别扭。现在好了,工具能自动把它拆成 {"user": {"name": "xxx"}} 这种结构。

这篇笔记我就从头到尾讲讲这个功能是怎么实现的,踩了哪些坑,以及最终的效果。希望能给想做类似东西的朋友一点参考。

背景:为什么需要嵌套JSON?

先说说痛点。日常开发里,CSV作为最通用的数据交换格式之一,经常被用来导入导出数据。但CSV天生是扁平的,一行就是一个记录,列名就是键。而JSON呢,天然支持嵌套结构,比如:

{
  "user": {
    "name": "张三",
    "email": "zhangsan@example.com"
  },
  "order": {
    "id": "ORD-001"
  }
}

如果CSV里的列名直接写成 user_name、user_email、order_id,那转出来还是扁平的,程序员拿到手还得自己写逻辑去拆。要是列名用点号分隔,比如 user.name,那就能直接映射成嵌套结构,省掉一步手动处理。

但问题是,绝大多数CSV转JSON的工具都不支持这个。它们要么把带点号的列名当成普通字符串原样输出,要么干脆报错。所以我决定自己动手,给我的工具加上这个能力。

整体思路:怎么处理点号嵌套?

核心逻辑其实不复杂:遍历CSV的每一行,对每个列名用 . 做分割,然后递归地把值塞到对应的嵌套层级里。

举个例子,假设CSV长这样:

user.name user.email order.id
张三 zs@test.com ORD-001

处理流程是:

  1. 解析第一行(表头),得到列名数组:["user.name", "user.email", "order.id"]
  2. 对每一行数据,遍历列名,比如处理 user.name:
    • 按 . 拆成 ["user", "name"]
    • 创建一个空对象 {}
    • 先检查有没有 user 这个key,没有就创建
    • 然后在 user 下面设置 name 为当前单元格的值
  3. 最终输出一个嵌套的JSON对象

听起来简单对吧?但实际写代码的时候有几个坑要注意。

实现细节:一步步拆解

第一步:解析CSV表头

CSV解析我用了现成的库,因为自己写容易漏掉引号转义、逗号在字段里等边界情况。我选的是 Papa Parse,浏览器端用着很顺手。

import Papa from 'papaparse';

function parseCSV(csvString) {
  const result = Papa.parse(csvString, {
    header: true,       // 把第一行当表头
    skipEmptyLines: true,
    dynamicTyping: false // 先不自动转类型,后面自己处理
  });
  return result;
}

header: true 这个选项很关键,它会自动把第一行作为列名,后面的每一行数据都会变成一个对象,key就是列名。但这时候key还是扁平的,比如 {"user.name": "张三"}。

第二步:递归构建嵌套结构

接下来就是核心函数。我需要把一个扁平的键值对对象,转换成一个嵌套的对象。

function nestObject(flatObj) {
  const result = {};
  
  for (const [key, value] of Object.entries(flatObj)) {
    const keys = key.split('.');
    let current = result;
    
    for (let i = 0; i < keys.length; i++) {
      const part = keys[i];
      // 如果是最后一个key,直接赋值
      if (i === keys.length - 1) {
        // 注意:如果值本身是数字或布尔,最好转一下类型
        current[part] = tryParseValue(value);
      } else {
        // 如果中间层级不存在,创建一个空对象
        if (!current[part] || typeof current[part] !== 'object') {
          current[part] = {};
        }
        current = current[part];
      }
    }
  }
  
  return result;
}

function tryParseValue(value) {
  // 简单尝试转数字和布尔,失败就返回原字符串
  if (value === 'true') return true;
  if (value === 'false') return false;
  if (value === 'null') return null;
  if (value === 'undefined') return undefined;
  const num = Number(value);
  if (!isNaN(num) && value.trim() !== '') return num;
  return value;
}

这里有个细节:tryParseValue 是为了让转换更智能。CSV里所有值默认都是字符串,但JSON里数字和布尔更常见。我加了简单的类型推断,但注意不要过度推断,比如电话号码 "13800138000" 会被转成数字,这可能不是用户想要的。所以我只对纯数字且没有前导零的字符串做转换,实际代码里我加了个判断:

function tryParseValue(value) {
  if (typeof value !== 'string') return value;
  const trimmed = value.trim();
  // 布尔值
  if (trimmed === 'true') return true;
  if (trimmed === 'false') return false;
  if (trimmed === 'null') return null;
  if (trimmed === 'undefined') return undefined;
  // 数字:必须全是数字,且不能有前导零(防止电话号码误转)
  if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
    // 排除前导零的情况,比如 "001" 应该保留为字符串
    if (trimmed.length > 1 && trimmed[0] === '0' && trimmed[1] !== '.') {
      return trimmed;
    }
    return Number(trimmed);
  }
  return trimmed;
}

第三步:处理数组索引

点号嵌套还有一个常见场景:数组。比如列名是 items.0.name、items.1.name,这表示一个数组。我一开始没处理这个,后来测试发现用户确实会这么用。所以我又加了数组检测逻辑:

function nestObject(flatObj) {
  const result = {};
  
  for (const [key, value] of Object.entries(flatObj)) {
    const keys = key.split('.');
    let current = result;
    
    for (let i = 0; i < keys.length; i++) {
      const part = keys[i];
      const isArrayIndex = /^\d+$/.test(part); // 判断是否是数字索引
      const isLast = i === keys.length - 1;
      
      if (isLast) {
        // 如果是数组索引,确保当前层级是数组
        if (isArrayIndex) {
          if (!Array.isArray(current)) {
            // 如果当前不是数组,则替换为数组(注意会丢失已有数据,但这种情况极少)
            current = Object.assign([], current);
          }
          current[parseInt(part)] = tryParseValue(value);
        } else {
          current[part] = tryParseValue(value);
        }
      } else {
        // 中间层级:根据下一个key决定创建对象还是数组
        const nextKey = keys[i + 1];
        const nextIsArrayIndex = /^\d+$/.test(nextKey);
        if (!current[part]) {
          current[part] = nextIsArrayIndex ? [] : {};
        }
        current = current[part];
      }
    }
  }
  
  return result;
}

这个逻辑稍微复杂一点,但核心就是:如果key是纯数字,就当作数组索引处理。比如 items.0.name,先创建 items 数组,然后 items[0] 是一个对象,里面设置 name。

第四步:处理同名冲突

还有一个坑:如果CSV里同时有 user.name 和 user 两列,怎么办?比如:

user.name user
张三 某值

这会导致冲突,因为 user 既想当对象,又想当普通值。我的处理方式是:如果先遇到 user.name,那么 user 已经被创建为对象了,后面遇到 user 列时,如果值不是对象,就忽略或者报错。我选择了忽略并给出警告,因为用户可能是不小心的。

// 在赋值前检查冲突
if (current[part] !== undefined && typeof current[part] === 'object' && !isArrayIndex) {
  console.warn(`Conflict detected at key "${part}": trying to set a primitive value on an existing object. Skipping.`);
  continue; // 跳过这个值
}

实际产品里我加了个UI提示,告诉用户哪些行有冲突。但代码里就简单跳过,保证不出错。

完整示例:跑一遍

假设CSV内容如下:

user.name,user.email,order.id,items.0.name,items.0.price
张三,zs@test.com,ORD-001,苹果,5.5
李四,ls@test.com,ORD-002,香蕉,3.2

经过我的工具转换后,输出的JSON是:

[
  {
    "user": {
      "name": "张三",
      "email": "zs@test.com"
    },
    "order": {
      "id": "ORD-001"
    },
    "items": [
      {
        "name": "苹果",
        "price": 5.5
      }
    ]
  },
  {
    "user": {
      "name": "李四",
      "email": "ls@test.com"
    },
    "order": {
      "id": "ORD-002"
    },
    "items": [
      {
        "name": "香蕉",
        "price": 3.2
      }
    ]
  }
]

完美!每个嵌套层级都对了,数组也正确生成了。

性能考量:浏览器端能扛多少数据?

因为工具是纯浏览器端运行的,没有后端,所以性能是个问题。我用了一个100列、每列嵌套3层、共10万行的CSV做测试(大约50MB)。Papa Parse解析花了大概2秒,然后嵌套转换花了1.5秒。整体在可接受范围内。

但要注意的是,如果嵌套层数特别深(比如10层以上),递归会导致调用栈溢出。JavaScript的递归深度默认大约1万层左右,但我们的场景一般不会超过5层,所以没问题。不过为了保险,我加了一个最大深度限制:

const MAX_DEPTH = 20;

function nestObject(flatObj) {
  // ... 同上,但在循环里加一个计数器
  let depth = 0;
  for (const [key] of Object.entries(flatObj)) {
    const keys = key.split('.');
    if (keys.length > MAX_DEPTH) {
      throw new Error(`Key "${key}" exceeds maximum nesting depth of ${MAX_DEPTH}`);
    }
  }
  // ... 后续处理
}

用户反馈与改进

功能上线后,我收到了一些用户的反馈,主要集中在这几点:

  1. 希望支持自定义分隔符:有人用 > 或者 :: 代替点号。我加了一个选项,用户可以选择分隔符,默认是 .。
  2. 空值处理:CSV里空单元格应该转成 null 还是空字符串?我默认转成 null,但加了一个开关让用户选。
  3. 大文件提示:超过100MB的CSV,浏览器可能会卡死。我加了一个警告,建议用户分批处理。

源码与部署

整个项目是开源的,放在 GitHub 上:json_utility_kit。前端用的 React + TypeScript,CSV解析依赖 Papa Parse,JSON格式化用了 JSON.stringify 的第二个参数(缩进)。部署很简单,直接 npm run build 扔到 Vercel 上就行。

如果你也想自己实现类似功能,核心代码其实就上面那几十行。但要注意的是,CSV的编码问题(比如 UTF-8 BOM)、引号转义、换行符处理,这些 Papa Parse 都帮你搞定了,千万别自己手写解析器,血的教训。

总结

这次更新给我的小工具加上了嵌套CSV转JSON的支持,核心就是点号分割加递归构建。看起来简单,但实际处理数组索引、类型推断、冲突检测这些细节还是挺折腾的。不过看到用户能用它一次搞定之前需要写脚本才能做的事,还是挺有成就感的。

如果你也有类似的需求,或者想自己撸一个,希望这篇笔记能帮你少踩几个坑。有什么问题欢迎在评论区聊,或者直接去 GitHub 提 issue。

相似推荐
uBlock Origin 开发版被 Chrome 商店拒绝:一场关于扩展单一用途政策的争议逆向工程实战:我如何绕过亚马逊Kindle网页端的DRM加密Heroku的丑陋秘密:云之王如何背离Rails并欺骗客户Firefox 成为最后一个支持 uBlock Origin 的主流浏览器:广告拦截的终局之战GitHub Codespaces 深度解读:云端开发环境如何重塑编码工作流认知负荷才是关键:软件设计的根本度量
编写使用方法
Markdown 格式 · Ctrl+Enter 确定
0 字新建笔记
欢迎回来
登录你的墨穗笔记账户
忘记密码?
还没有账户?立即注册
创建账户
注册你的专属墨穗笔记
已有账户?去登录
找回密码
输入注册邮箱获取验证码
返回登录
请输入图片中的验证码以继续注册
加载中...
取消
新建收藏
手动添加你喜欢的内容
取消
编辑头像与昵称
上传新头像或修改你的显示昵称
支持 JPG/PNG,最大 2MB
取消

问题反馈

隐私提醒

取消
编辑工具
受控分享
为这篇笔记生成限时 / 带密码的临时链接
关闭