V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Jobing
V2EX  ›  程序员

当你解析 JSON 时, Number 类型双精度丢失可以尝试使用 json-bn

  •  
  •   Jobing · 2022-11-05 21:59:43 +08:00 · 2633 次点击
    这是一个创建于 509 天前的主题,其中的信息可能已经有所发展或是发生改变。

    支持 ESM 和 CJS 环境,开源地址: https://github.com/jobinben/json-bn

    16 条回复    2022-11-07 11:44:21 +08:00
    bertonzh
        1
    bertonzh  
       2022-11-05 23:05:29 +08:00   ❤️ 9
    建议先看看别人是怎么做的:
    https://github.com/sidorares/json-bigint

    你这几十行靠粗糙的正则替换实现的 json bigint 支持,自己在小项目里面对付简单场景可以,就不要发到 github 和 npm 了哈。

    parse('{"x": ": 12345678901234561111"}')

    Uncaught SyntaxError: Expected ',' or '}' after property value in JSON at position 11
    at JSON.parse (<anonymous>)
    at e (<anonymous>:1:242)
    at <anonymous>:1:1
    bertonzh
        2
    bertonzh  
       2022-11-05 23:21:36 +08:00
    其实你这个实现完善一下应该可以处理这个问题(再往前判断一下属性名的右引号)。

    但是别忘了 JSON 里面有数组:

    parse('[12345678901234561111]')

    通过正则不可能完全识别数组中 Int 。
    kid740246048
        3
    kid740246048  
       2022-11-06 02:01:02 +08:00
    真要搞个正经的 json 解析库的话,还是得测试先行,github 上有很多相关的 test suite ,比如:
    https://github.com/nst/JSONTestSuite
    https://github.com/dscape/clarinet/tree/master/test
    himself65
        4
    himself65  
       2022-11-06 03:27:31 +08:00 via iPhone   ❤️ 13
    题主有点不要脸了吧……把别的项目 download 数还专门贴到自己项目 readme 里面

    https://github.com/jobinben/json-bn/commit/e14e7951835bf97242f6e7f8dd7fcae166e83f43
    wxf666
        5
    wxf666  
       2022-11-06 04:04:48 +08:00
    @bertonzh `js` 新手。感觉通过正则找到数字,并替换为 `bigint`,最后再 `eval` 或 `new Function`,也能读取含有大数字的 `json` 诶:

    ```javascript
    s = '{"x": ": 12345678901234561111", "y": [-12345678901234561111, "12345678901234561111", -123456, "\\"12345678901234561111\\""]}';

    eval('(' + s.replace(/"(?:\\?.)*?"|-?\d+/g, m => !isFinite(m) || Number.isSafeInteger(+m) ? m : m + 'n') + ')');
    ```

    结果:

    ```javascript
    {
      x: ": 12345678901234561111",
      y: [
      -12345678901234561111n,
      "12345678901234561111",
      -123456,
      "\"12345678901234561111\""
     ]
    }
    ```
    Red998
        6
    Red998  
       2022-11-06 10:24:41 +08:00
    字符天下第一
    yulon
        7
    yulon  
       2022-11-06 11:22:34 +08:00
    用正则永远不要在函数名里带上 parse 😅
    bertonzh
        8
    bertonzh  
       2022-11-06 16:25:46 +08:00
    @wxf666 嗯,看上去这个思路是可以的,优先匹配掉字符串,再匹配的数字串就肯定是字符串外面的。
    对转义的处理目前没有发现什么毛病。

    @Jobing 楼主可以借鉴一下。
    wxf666
        9
    wxf666  
       2022-11-06 16:52:23 +08:00
    @bertonzh 回头再看看,其实应该还没处理好有浮点数的 `json`。但思路是一样的,碰到字符串、浮点数,跳过就好。


    如果担心 `eval` 了危险的 `js`,我觉得可以正则匹配下,看是不是全为 `json` 的元素:

    `/^(?:字符串|整数|浮点|true|false|null|[[\]{},]|\s+)$/`


    其实,我感觉可以:

    1. 正则匹配出,并用一个数组记录下所有整数,再替换为在数组中的下标。
    2. `JSON.parse()` 后,再遍历所有为整数的值,替换回来(这时候就可以决定用不用 `bigint` 了)

    好处:

    1. 这应该会比手动解析 `json` 快
    2. 而且代码体积小(可能十来行就可以了)
    3. 不用担心 `eval`、`new Function` 能不能用,危不危险的问题
    wxf666
        10
    wxf666  
       2022-11-06 16:53:55 +08:00
    @bertonzh 写漏了:

    `/^(?:字符串|整数|浮点|true|false|null|[[\]{},]|\s+)*$/`
    bertonzh
        11
    bertonzh  
       2022-11-06 21:55:18 +08:00
    @wxf666 你这个办法可不行,JS 遍历对象 key 的顺序并不是按照字面量 key 顺序来的,所以你无法通过数组按顺序记录整数。
    另外,性能也不一定快,因为你做了三步:分词 + JSON.parse + 遍历替换,而手动解析只需要一趟(分词和生成结果同时处理)。(具体我没测过)


    所以要么是用你最开始的 eval 方式。不过 eval 比 JSON.parse 更慢,因为 eval 处理的是 JS 代码,语法比 JSON 复杂很多倍。
    或者用楼主的实现,即先替换成一个带特殊唯一标志的字符串,然后在 JSON.parse 的第二个回调参数里面做替换,或者在 parse 之后遍历。
    wxf666
        12
    wxf666  
       2022-11-07 01:21:17 +08:00
    @bertonzh 试着写出来了:

    *( V 站排版原因,行首有全角空格)*

    ```javascript
    function parseJson(json) {

      function restore(obj) {
       if (typeof obj === 'number')
        return nums[obj];
       else if (Array.isArray(obj))
        obj.forEach((v, i) => obj[i] = restore(v));
       else if (typeof obj === 'object' && obj !== null)
        Object.keys(obj).forEach(k => obj[k] = restore(obj[k]));
       return obj;
     }

      let nums = [];
      return restore(JSON.parse(json.replace(/"(?:\\?.)*?"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g, m =>
       m[0] === '"' ? m : nums.push(/[.eE]/.test(m) || Number.isSafeInteger(+m) ? +m : BigInt(m)) - 1
     )));
    }
    ```

    拿了个本地 64.8 MB 的 `~/.conda/pkgs/cache/e5e4a514.json` 测了下速度: *( i5-8250U ,`nodejs` v16 )*

    - #5 楼的方法:3.3 s
    - 此楼方法:2.2 s = 正则替换 1.5 s + json 解析 0.4 s + 递归替换 0.3 s
    - `json-bigint` *(`{useNativeBigInt: true}`)*:1.7 s
    chenjiangui998
        13
    chenjiangui998  
       2022-11-07 09:38:24 +08:00
    @himself65 太无耻了
    bertonzh
        14
    bertonzh  
       2022-11-07 11:08:56 +08:00
    @wxf666 原来是把数组序号替换进去了啊,可以可以
    bertonzh
        15
    bertonzh  
       2022-11-07 11:13:49 +08:00
    @wxf666 试试这样呢?

    ```
    function parseJson(json) {
      let nums = [];
      return JSON.parse(json.replace(/"(?:\\?.)*?"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g, m =>
       m[0] === '"' ? m : nums.push(/[.eE]/.test(m) || Number.isSafeInteger(+m) ? +m : BigInt(m)) - 1
     ), (key, val) => typeof val === 'number' ? nums[val] : val);
    }
    ```
    wxf666
        16
    wxf666  
       2022-11-07 11:44:21 +08:00
    @bertonzh `JSON.parse` 还能这样用?学习了!

    但耗时更久了,2.8 s 左右

    会不会是有了转换器,就用不了内部高度优化的 `json` 解析器了。。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3651 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 10:27 · PVG 18:27 · LAX 03:27 · JFK 06:27
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.