前言:当数据流"说话"时,我们需要一个"翻译官"
想象一下这样的场景:数据像瀑布一样源源不断地涌来,里面混杂着文本、图片、视频、JSON对象... 就像一锅大杂烩!这时候,我们需要一个聪明的"翻译官"来帮我们理清头绪。今天要介绍的 StreamParser 就是这样一个角色------它能够实时解析复杂的数据流,让每种数据类型都找到自己的"归宿"。
🎯 什么是StreamParser?
简单来说,StreamParser 是一个基于标记的流式数据解析器。它就像一个有经验的邮递员,能够识别不同的"信封"(标记),然后把里面的"信件"(数据)准确无误地送到对应的"收件箱"(回调函数)。
它能做什么?
- 🎵 实时解析:数据来一点,解析一点,不耽误事儿
- 🎨 多类型支持:文本、图片、视频、JSON对象... 统统拿下
- 🔧 高度可配:想怎么解析,你说了算
- 🛡️ 错误容忍:即使数据有问题,也不会"崩溃"
🚀 核心概念:先认识一下"标记语言"
在深入代码之前,我们先来理解几个关键概念:
1. 数据类型(TypeMarkers)
            
            
              javascript
              
              
            
          
          const TypeMarkers = {
  TEXT: 'TEXT',        // 纯文本,就像普通的信件
  OBJECT: 'OBJECT',    // JSON对象,像是个精美包装的礼物
  RICH: 'RICH',        // 富文本,带着格式的漂亮文档
  VIDEO: 'VIDEO',      // 视频,会动的画面
  IMAGE: 'IMAGE',      // 图片,一图胜千言
  VIDEO_LIST: 'VIDEO_LIST' // 视频列表,整个影视库
};2. 标记配置(Markers)
每个数据类型都有自己独特的"信封"格式:
            
            
              javascript
              
              
            
          
          {
  type: 'TEXT',                    // 数据类型
  start: '@s_text@',              // 开始标记 - 就像"亲爱的,"
  end: '@e_text@',                // 结束标记 - 就像"此致,敬礼!"
  stream: true,                   // 是否流式返回 - 边读边处理
  parse: customParseFunction      // 自定义解析 - 特殊要求特殊处理
}🔍 深入源码:看看"翻译官"是怎么工作的
构造函数:装备齐全上岗
            
            
              javascript
              
              
            
          
          constructor(callback, markers = []) {
  // 装备解析规则:用自定义的,或者默认的
  this.markers = markers.length ? markers : [...defaultMarkers];
  
  // 工作台初始化
  this.buffer = '';                    // 待处理数据缓冲区
  this.currentContext = null;          // 当前正在处理的任务
  this.pendingEndBuffer = '';          // 临时存放"半截"标记
  this.callback = callback;            // 结果汇报通道
}数据流入:来活了!
            
            
              javascript
              
              
            
          
          feed(chunk, message) {
  this.message = message;           // 记下是谁的活儿
  if (!chunk) return;               // 空数据?那歇会儿
  
  this.buffer += chunk;             // 把新数据放到工作台上
  this.processBuffer();             // 开干!
}核心处理流程:翻译官的"工作流"
整个解析过程就像是一个严谨的流水线:
- 检测开始标记 - 找到"信封"的开头
- 进入处理上下文 - 准备拆"信封"
- 寻找结束标记 - 找到"信封"的结尾
- 提取并解析内容 - 拿出"信件"并翻译
- 回调结果 - 把翻译好的内容交给上级
开始标记检测:火眼金睛
            
            
              javascript
              
              
            
          
          detectStartMark() {
  // 用正则表达式扫描所有可能的标记
  const matchList = this.buffer.match(/@[^@]+@/gi) || [];
  
  for (const matchItem of matchList) {
    for (const mark of this.markers) {
      if (mark.start === matchItem) {
        // 找到匹配的开始标记!
        const index = this.buffer.indexOf(mark.start);
        if (index !== -1) {
          this.currentContext = {
            type: mark.type,
            config: mark,
            contentBuffer: '',
            endMark: mark.end
          };
          // 移除已识别的开始标记,准备处理内容
          this.buffer = this.buffer.slice(index + mark.start.length);
          return true;
        }
      }
    }
  }
  return false;
}边界处理:当标记被"腰斩"时
这是最精彩的部分!想象一下,结束标记 @e_text@ 被分成了两个数据包: 第一个包:"Hello World@e_te" 第二个包:"xt@"
普通解析器可能会懵圈,但我们的 StreamParser 有妙招:
            
            
              javascript
              
              
            
          
          // 检测可能的结束标记分割
let maxOverlap = 0;
for (let len = 1; len <= Math.min(this.buffer.length, this.currentContext.endMark.length); len++) {
  if (this.currentContext.endMark.startsWith(this.buffer.slice(-len))) {
    maxOverlap = len;  // 找到最大重叠部分
  }
}
if (maxOverlap > 0) {
  // 把可能的分割标记暂存起来
  const contentPart = this.buffer.slice(0, this.buffer.length - maxOverlap);
  this.pendingEndBuffer = this.buffer.slice(-maxOverlap);
  
  if (contentPart) {
    this.commitContent(contentPart, false);
  }
  this.buffer = '';
}这就好比:你收到一封信,发现最后几个字被撕掉了,你会先把完整的部分读出来,然后把残缺的部分留着等下一封信来补全。
💡 实战演练:让我们来"玩转"StreamParser
场景1:基础文本解析
            
            
              javascript
              
              
            
          
          // 创建一个文本解析器
const parser = new StreamParser((data, message) => {
  console.log('📨 收到数据:', {
    类型: data.type,
    内容: data.data,
    是否完成: data.isComplete ? '✅' : '⏳'
  });
});
// 模拟数据流入 - 就像收邮件一样
parser.feed('@s_text@你好,世界!@e_text@', { from: '用户A' });
// 输出:
// 📨 收到数据: { 类型: "TEXT", 内容: "你好,世界!", 是否完成: "✅" }场景2:流式文本处理(像看直播一样)
            
            
              javascript
              
              
            
          
          const streamParser = new StreamParser((data) => {
  if (data.isComplete) {
    console.log('🎉 完整内容:', data.data);
  } else {
    process.stdout.write(data.data); // 实时输出,像打字机效果
  }
});
// 模拟分块数据(就像网络传输)
streamParser.feed('@s_text@今天天气', null);
streamParser.feed('真好,适合出去', null);  
streamParser.feed('散步!@e_text@', null);
// 输出效果:
// 今天天气真好,适合出去散步!
// 🎉 完整内容: 今天天气真好,适合出去散步!场景3:处理JSON对象
            
            
              javascript
              
              
            
          
          const objParser = new StreamParser((data) => {
  if (data.type === 'OBJECT' && data.isComplete) {
    console.log('📊 解析出的对象:', data.data);
  }
});
objParser.feed('@s_obj@{"name":"张三","age":25,"hobbies":["篮球","编程"]}@e_obj@', null);
// 输出:
// 📊 解析出的对象: { name: "张三", age: 25, hobbies: ["篮球", "编程"] }场景4:自定义数据类型(打造专属解析器)
            
            
              javascript
              
              
            
          
          // 创建一个表情包解析器
const emojiParser = new StreamParser((data) => {
  if (data.type === 'EMOJI_PACK' && data.isComplete) {
    console.log('😄 表情包列表:', data.data);
  }
}, [
  {
    type: 'EMOJI_PACK',
    start: '@s_emoji@',
    end: '@e_emoji@',
    parse: (content) => {
      // 自定义解析逻辑:把逗号分隔的表情解析为数组
      return content.split(',').map(emoji => emoji.trim());
    }
  }
]);
emojiParser.feed('@s_emoji@😊,😂,😍,🤔,🎉@e_emoji@', null);
// 输出:
// 😄 表情包列表: ["😊", "😂", "😍", "🤔", "🎉"]🎨 高级技巧:让StreamParser更强大
技巧1:错误处理 - 给解析器装上"安全气囊"
            
            
              javascript
              
              
            
          
          const safeParser = new StreamParser((data, message) => {
  if (data.error) {
    console.error('❌ 解析错误:', data.error);
    console.log('原始数据:', data.raw);
    return;
  }
  
  if (data.isComplete) {
    console.log('✅ 成功解析:', data.data);
  }
});
// 测试错误情况
safeParser.feed('@s_obj@{invalid json@e_obj@', null);
// 输出:❌ 解析错误: Unexpected token i in JSON at position 1技巧2:消息关联 - 知道数据来自哪里
            
            
              javascript
              
              
            
          
          const contextAwareParser = new StreamParser((data, message) => {
  console.log(`📮 来自 ${message.sender} 的数据:`, data.data);
});
contextAwareParser.feed('@s_text@Hello@e_text@', { sender: '用户A', timestamp: Date.now() });
contextAwareParser.feed('@s_text@World@e_text@', { sender: '用户B', timestamp: Date.now() });🔧 性能优化建议
1. 缓冲区管理
对于长时间运行的应用,定期检查缓冲区大小,避免内存泄漏:
            
            
              javascript
              
              
            
          
          // 在StreamParser类中添加
checkBufferHealth() {
  if (this.buffer.length > 1024 * 1024) { // 超过1MB
    console.warn('⚠️ 缓冲区过大,考虑重置解析器');
    this.reset();
  }
}2. 标记设计最佳实践
- ✅ 好的标记:@s_video_list@、@e_rich_content@
- ❌ 不好的标记:@s@、@e@(太短,容易误匹配)
🌟 实际应用场景
场景1:实时聊天系统
            
            
              javascript
              
              
            
          
          // 处理混合消息:文本 + 表情 + 图片
chatParser.feed('@s_text@看看这个图片@e_text@@s_image@https://example.com/cat.jpg@e_image@', {
  userId: 'user123',
  roomId: 'general'
});场景2:实时数据仪表盘
            
            
              javascript
              
              
            
          
          // 同时接收多种数据更新
dashboardParser.feed('@s_obj@{"cpu":45,"memory":78}@e_obj@', { type: 'system' });
dashboardParser.feed('@s_text@用户登录:张三@e_text@', { type: 'event' });场景3:智AI能体内容流式返回/多媒体内容流
            
            
              javascript
              
              
            
          
          // 视频列表 + 封面图
mediaParser.feed('@s_video_list@[{"id":1,"title":"视频1"}]@e_video_list@', null);
mediaParser.feed('@s_image@https://example.com/thumb1.jpg@e_image@', { videoId: 1 });💭 总结与思考
StreamParser 的魅力在于它的简洁而强大的设计理念:
🎯 设计哲学
- 单一职责:每个标记类型只做一件事
- 开闭原则:对扩展开放,对修改关闭
- 错误隔离:一个数据类型出错,不影响其他
🚀 为什么选择StreamParser?
| 特性 | 传统解析器 | StreamParser | 
|---|---|---|
| 实时性 | ❌ 等完整数据 | ✅ 边收边处理 | 
| 灵活性 | ❌ 固定格式 | ✅ 可配置标记 | 
| 容错性 | ❌ 全有或全无 | ✅ 局部失败 | 
| 扩展性 | ❌ 需要改源码 | ✅ 配置即扩展 | 
🌈 最后的话
StreamParser 就像是一个数据世界的同声传译------它不需要等待完整的演讲,而是实时地将零散的信息片段翻译成有意义的整体。无论你是处理实时聊天、数据监控,还是复杂的多媒体流,它都能成为你得力的助手。
记住:好的工具不应该让使用者适应它的限制,而应该适应使用者的需求。StreamParser 正是这样一个"善解人意"的工具!
工具源码
            
            
              javascript
              
              
            
          
          /**
 * 解析标记类型
 * @param {String} TEXT - text
 * @param {String} OBJECT - obj
 * @param {String} IMAGE - img
 */
const TypeMarkers = {
  TEXT: 'TEXT',
  /** obj */
  OBJECT: 'OBJECT',
  /** rich */
  RICH: 'RICH',
  /** video */
  VIDEO: 'VIDEO',
  /** img */
  IMAGE: 'IMAGE',
  /** VIDEO_LIST 视频点选数据列表 */
  VIDEO_LIST: 'VIDEO_LIST'
};
function parseImgAndVideo(content, errorHandler) {
  try {
    let con = content.trim();
    const conList = con ? con.split(',') : [];
    return conList;
  } catch (e) {
    errorHandler && errorHandler({ error: e.message, raw: content });
  }
}
/**
 * 默认标记配置
 * @param {TypeMarkers[type]} type - 默认标记配置
 * @param {String} start - 开始标记
 * @param {String} end - 结束标记
 * @param {Function} parse - 自定义解析函数
 * @param {Boolean} stream - 是否流式返回结果 (每次请求解析后都会返回一次当前数据)
 */
const defaultMarkers = [
  {
    type: TypeMarkers.TEXT,
    start: '@s_text@',
    end: '@e_text@',
    stream: true
  },
  {
    type: TypeMarkers.OBJECT,
    start: '@s_obj@',
    end: '@e_obj@',
    parse: null
  },
  {
    type: TypeMarkers.VIDEO,
    start: '@s_video@',
    end: '@e_video@',
    parse: parseImgAndVideo
  },
  {
    type: TypeMarkers.IMAGE,
    start: '@s_image@',
    end: '@e_image@',
    parse: parseImgAndVideo
  },
  { type: TypeMarkers.RICH, start: '@s_rich@', end: '@e_rich@', stream: true },
  {
    type: TypeMarkers.VIDEO_LIST,
    start: '@s_video_list@',
    end: '@e_video_list@',
    parse(content, errorHandler) {
      try {
        return JSON.parse(`${content}`);
      } catch (e) {
        errorHandler && errorHandler({ error: e.message, raw: content });
      }
    },
    stream: false
  }
  // {
  //   type: 'obj',
  //   start: '@start_obj@',
  //   end: '@end_obj@',
  //   /**
  //    * 自定义格式化函数
  //    * @param {String} content - 待格式化内容
  //    * @param {Function} errorHandler - 错误处理函数
  //    */
  //   parse(content, errorHandler) {
  //     try {
  //       return JSON.parse(`{${content}}`)
  //     } catch (e) {
  //       errorHandler && errorHandler({ error: e.message, raw: content })
  //       return
  //     }
  //   }
  // },
];
/**
 * 根据定义规则解析流式返回的数据
 * @param {Function} callback - 处理解析结果的回调函数
 * @param {Array} markers - 初始化传入的自定义配置标记
 * @param {Array} this.markers - 解析规则
 * @param {String} this.buffer - 待解析的流式数据字符串
 * @param {Object} this.currentContext - 当前解析上下文
 * @param {String} this.currentContext.type - 解析类型
 * @param {String} this.currentContext.config - 解析marker对象
 * @param {String} this.currentContext.contentBuffer -
 * @param {String} this.currentContext.endMark - 解析对象结束标记
 *
 */
export class StreamParser {
  constructor(callback, markers = []) {
    // 有自定义配置,取自定义配置
    if (Array.isArray(markers) && markers.length) {
      this.markers = markers;
    } else {
      this.markers = [...defaultMarkers];
    }
    // 数据处理默认会将start和end匹配上的内容移除,如需自定义markers,可以通过添加parse函数来处理检索的内容
    this.buffer = '';
    this.message = null; // 存储当前正在处理的消息
    this.currentContext = null;
    this.pendingEndBuffer = ''; // 专门存储可能的分割结束标记
    this.callback = callback || function () {};
  }
  /**
   * 数据流入入口
   * @param {string} chunk 字符串数据-移除换行空格
   */
  feed(chunk, message) {
    this.message = message;
    if (!chunk) {
      return;
    }
    this.buffer += chunk;
    this.processBuffer();
  }
  /** 重置数据 */
  reset() {
    this.buffer = '';
    this.currentContext = null;
    this.pendingEndBuffer = '';
    this.callback = function () {};
  }
  /** 数据处理 */
  processBuffer() {
    while (this.buffer.length > 0) {
      if (!this.currentContext) {
        if (!this.detectStartMark()) {
          break;
        }
      } else {
        if (!this.processCurrentContext()) {
          break;
        }
      }
    }
  }
  /** 匹配开始标记 */
  detectStartMark() {
    const matchList = this.buffer.match(/@[^@]+@/gi) || [];
    for (const matchItem of matchList) {
      for (const mark of this.markers) {
        if (mark.start === matchItem) {
          const index = this.buffer.indexOf(mark.start);
          if (index !== -1) {
            this.currentContext = {
              type: mark.type,
              config: mark,
              contentBuffer: '',
              endMark: mark.end
            };
            this.buffer = this.buffer.slice(index + mark.start.length);
            return true;
          }
          break;
        }
      }
    }
    return false;
  }
  /** 根据开始匹配标记内容处理
   * @returns {Boolean} - 是否处理完成
   */
  processCurrentContext() {
    // 优先处理之前保留的结束标记片段
    if (this.pendingEndBuffer) {
      const combined = this.pendingEndBuffer + this.buffer;
      const endIndex = combined.indexOf(this.currentContext.endMark);
      if (endIndex !== -1) {
        const contentPart = combined.slice(0, endIndex);
        this.commitContent(contentPart);
        this.buffer = combined.slice(
          endIndex + this.currentContext.endMark.length
        );
        this.pendingEndBuffer = '';
        this.currentContext = null;
        return true;
      }
      this.buffer = combined;
    }
    const endIndex = this.buffer.indexOf(this.currentContext.endMark);
    if (endIndex !== -1) {
      this.commitContent(this.buffer.slice(0, endIndex));
      this.buffer = this.buffer.slice(
        endIndex + this.currentContext.endMark.length
      );
      this.currentContext = null;
      return true;
    }
    // 检测可能的结束标记分割
    let maxOverlap = 0;
    for (
      let len = 1;
      len <= Math.min(this.buffer.length, this.currentContext.endMark.length);
      len++
    ) {
      if (this.currentContext.endMark.startsWith(this.buffer.slice(-len))) {
        maxOverlap = len;
      }
    }
    if (maxOverlap > 0) {
      // 分离内容和潜在结束标记
      const contentPart = this.buffer.slice(0, this.buffer.length - maxOverlap);
      this.pendingEndBuffer = this.buffer.slice(-maxOverlap);
      if (contentPart) {
        this.commitContent(contentPart, false);
      }
      this.buffer = '';
    } else {
      this.commitContent(this.buffer, false);
      this.buffer = '';
    }
    return false;
  }
  /** 回调内容
   * @param {String} content - 待处理的内容
   * @param {Boolean} isFinal - 是否是当前匹配标记最后一次调用
   */
  commitContent(content, isFinal = true) {
    this.currentContext.contentBuffer += content;
    if (this.currentContext.config.stream) {
      this.callback(
        {
          type: this.currentContext.type,
          config: this.currentContext.config,
          data: content,
          isComplete: false
        },
        this.message
      );
    }
    if (isFinal) {
      let error = null;
      let con = this.parseContent(
        this.currentContext.contentBuffer,
        function (res) {
          error = res;
        }
      );
      this.callback(
        {
          type: this.currentContext.type,
          config: this.currentContext.config,
          data: con,
          error,
          isComplete: true
        },
        this.message
      );
    }
  }
  /**
   * 格式化函数
   * @param {String} content - 待格式化内容
   * @param {Function} errorHandler - 错误处理函数
   */
  parseContent(content, errorHandler) {
    try {
      // 存在自定义解析函数
      if (
        this.currentContext?.config?.parse &&
        typeof this.currentContext?.config?.parse === 'function'
      ) {
        return this.currentContext?.config?.parse(content, errorHandler);
      }
      // 不纯在自定义解析函数根据类型解析
      switch (this.currentContext.type) {
        case TypeMarkers.OBJECT:
          return JSON.parse(content);
        case TypeMarkers.IMAGE:
        case TypeMarkers.VIDEO:
          return { url: content.trim() };
        default:
          return content;
      }
    } catch (e) {
      errorHandler && errorHandler({ error: e.message, raw: content });
    }
  }
}使用示例
            
            
              javascript
              
              
            
          
          // 流式返回格式化处理
const parser = new StreamParser((result, message) => {
    // 获取解析后的结果,并过滤解析错误的内容
    if (result && !result.error) {
      // 此处执行内容监听的回调方法
    }
});
// 将流式返回的内容调用feed方法进行解析
/*
    answer 获取的文本内容
    context 获取的上下文对象,比如会话的id信息之类的,传入后,监听回调内容时会将内容原本内容返回
*/
parser.feed(answer, context);互动时间:你在项目中遇到过哪些有趣的数据解析挑战?欢迎在评论区分享你的故事! 🎉