前言:当数据流"说话"时,我们需要一个"翻译官"
想象一下这样的场景:数据像瀑布一样源源不断地涌来,里面混杂着文本、图片、视频、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);
互动时间:你在项目中遇到过哪些有趣的数据解析挑战?欢迎在评论区分享你的故事! 🎉