如何解析stream流式json数据?

问题:假设有一个json对象如下:

{
    table: [
        {
            name: 'js',
            age: 20
        }
    ]
}

现在以上对象在服务端被格式化stream流返回(一个请求,分为多个片段返回),并且每个片段的内容都是不确定的,前端接收到内容后,都需要用JSON.parse解析代码,并渲染到界面上。
假设服务端返回的每个片段可能如下:

// 1
"{"
// 2
"table: ["
// 3
"{name: 'js',"
// 4
"age: 20"
// 5
"}"
// 6
"]"
// 7
"}"

从上面可以看出,服务端每个片段返回到可能是总的json中任意片段长度的字符串,前端需要每次都能解析出来,我的理解是每次都自动拼接对应的后续缺失的},],'等。比如收到第一个片段时,自动拼接上},此时整体变成{}就可以直接使用JSON.parse解析而不会报错,到第二个片段时,应该拼接后变成{ table: [] },以此类推,如果某个片段返回了对应}或者]则需要移除前面手动拼接的}]

有没有类似的专门处理以上需求的算法,或者npm包呢。

阅读 4.9k
4 个回答

可以看看这个 https://github.com/creationix/jsonparse

算法的话,我能想到的就是 Stack,LeetCode 有一道题叫 Valid Parentheses 就是说这个的,只不过这个放在 json string 上面逻辑更复杂一些。

实际上我觉得看你用到什么程度,如果是大概代码能“跑”就行的话,其实不用做到完美拼接那么麻烦,就直接用 JSON.parse 试错迭代就完事儿了。当然为了降低频率,可以简单的进行方括号和花括号的拼接,我随便撸了个简单版本:

const { Readable } = require("node:stream");

const jsonSegmentsRS = Readable.from([
  // 1
  "{",
  // 2
  '"table": [',
  // 3
  '{"name": "js",',
  // 4
  '"age',
  // 5
  '": 20}',
  // 6
  "]",
  // 7
  "}",
]);

const KeyWords = {
  CURLY_BRACE: {
    L: "{",
    R: "}",
  },
  SQUARE_BRACKET: {
    L: "[",
    R: "]",
  },
  // todo: also needs to resolve the special char, eg: comma
};

class StreamMannerJsonParser {
  records = [];
  bufferStack = [];
  pairStack = [];

  parse(segment) {
    const chars = segment.replace("s", "").split("");

    chars.forEach((c) => {
      this.bufferStack.push(c);

      switch (c) {
        case KeyWords.CURLY_BRACE.L:
        case KeyWords.SQUARE_BRACKET.L:
          this.pairStack.push(c);
          break;
        case KeyWords.CURLY_BRACE.R:
          if (this.pairStack.at(-1) === KeyWords.CURLY_BRACE.L)
            this.pairStack.pop();
          break;
        case KeyWords.SQUARE_BRACKET.R:
          if (this.pairStack.at(-1) === KeyWords.SQUARE_BRACKET.L)
            this.pairStack.pop();
          break;
      }
    });

    try {
      // todo: simple bypass the failed parse action
      return JSON.parse(
        this.bufferStack
          .concat(
            [...this.pairStack]
              .reverse()
              .map((c) =>
                c === KeyWords.CURLY_BRACE.L
                  ? KeyWords.CURLY_BRACE.R
                  : KeyWords.SQUARE_BRACKET.R
              )
          )
          .join("")
      );
    } catch (err) {
      console.warn("warn: parse failed, waiting for another chunk");
    }
  }
}

const parser = new StreamMannerJsonParser();

jsonSegmentsRS.on("data", (chunk) => {
  const parsedChunk = parser.parse(chunk);

  typeof parsedChunk !== "undefined" && console.log(parsedChunk);
});

执行结果(改了点你的数据,因为有些地方不是合法的 json 字符串):
image.png

我这里为了图省事儿,就当做 string stream 来处理了,实际上真要实现的话,应该用 object mode,然后封装成 Readable Stream(或双工) 直接和响应请求的 Stream pipe 起来就行了。

你可以JSONStream这个库:https://github.com/dominictarr/JSONStream
先安装:

npm install JSONStream

再用JSONStream.parse()方法来解析数据流:

const JSONStream = require('JSONStream');
const request = require('request');

const url = 'http://your-server.com/streaming-json-api';

// 用 request 库获取 JSON 数据流
request(url)
  .pipe(JSONStream.parse('table.*'))
  .on('data', (data) => {
    // 处理解析到的 JSON 片段
    console.log('Parsed JSON piece:', data);
  });

推广一下自己写的一个库:

npm i stream-json-parse

https://github.com/maotong06/stream-json-parse

https://segmentfault.com/a/1190000044384699

利用Generator和Fetch。前端可以直接使用,相比 clarinet 不用手动处理每个key,value回调。更方便快捷,还能自定义解析的回调更新时间,避免前端因为数据流解析的太快,造成渲染性能问题。也能自定义路径,解析完你想要对应路径下的数据完整后,再返回回调。

import { fetchStreamJson, arrayItemSymbol } from 'stream-json-parse'

fetchStreamJson({
  // 请求地址
  url: './bigJson1.json',
  // 解析配置
  JSONParseOption: {
    // 要求完整解析对应路径下的数据,才能上报(可选), arrayItemSymbol 表示数组项
    completeItemPath: ['data', arrayItemSymbol],
    // json解析的回调
    jsonCallback: (error, isDone, value) => {
      console.log('jsonCallback', error, isDone, value)
    },
    diffCallBack: (json, isEq) => {
      console.log('diffCallBack', json, isEq)
    }
  },
  // fetch请求配置,同浏览器 fetch api
  fetchOptions: {
    method: 'GET',
  },
})
推荐问题
logo
Microsoft
子站问答
访问
宣传栏