正则解决Markdown流式输出不完整图片、表格、数学公式

Markdown碎片处理

在大模型SSE流式输出的时候,往往返回的是Markdown字符串。其他类型比如 # * -等,实时渲染的时候抖动是比较小的,但是像图片链接、表格、块级数学公式在渲染的时候往往会造成剧烈的页面抖动,用户体验不友好。接下来我们就一一这三个场景。

不完整图片链接

md 复制代码
//完整图片链接
![Vue Logo](https://img0.baidu.com/it/u=736188794,4119241415&fm=253&fmt=auto&app=120&f=JPEG?w=1140&h=760 "Vue.js Logo")

//不完整图片链接
![Vue Logo](https://img0.baidu.com/i

渲染效果:

处理这种不完整的链接我们可以直接正则匹配替换掉不完整的图片链接为空,等链接完整后再做渲染

js 复制代码
/**
 * 处理图片流式碎片
 * @param {string} markdown - 原始 Markdown 字符串
 * @returns {string} 清理后的 Markdown 字符串
 */
function stripBrokenImages(md) {
  if(typeof(md) !== 'string') {
    console.log('%c v3-markdown-stream:请传正确的md字符串~ ','background:#ea2039;color:#ffffff;padding:2px 5px;')
    return '';
  }
  if(!md) {
    return '';
  }
  md = md.replace(
    /^\s*\[([^\]]+)\]:[ \t]*(\S+)(?:[ \t]+(["'])(?:(?!\3)[\s\S])*?)?$/gm,
    (s, id, src, quote) => {
      // 如果捕获到开启引号却没闭合,或者 src 后直接换行(缺引号),都认为不完整
      if (quote && !s.endsWith(quote)) return ""; // 引号没闭合
      if (!quote && /["']$/.test(src)) return ""; // src 结尾多余引号,也视为异常
      return s; // 完整定义,保留
    }
  );
  md = md.replace(
    /!\[([^\]]*)\]\(([^)]*(?:\([^)]*\)[^)]*)*)\)/g,
    (s, alt, body) => {
      const open = (body.match(/\(/g) || []).length;
      const close = (body.match(/\)/g) || []).length;
      if (open !== close) return ""; // 括号不匹配 → 不完整
      if (body.includes('"') && (body.match(/"/g) || []).length % 2) return "";
      if (body.includes("'") && (body.match(/'/g) || []).length % 2) return "";
      return s; // 完整,保留
    }
  );
  return md.replace(/!\[[^\]]*\]\([^)]*$/g, "");
}

不完整表格字符串

md 复制代码
//完整表格字符串
| 姓名 | 年龄 | 职业 |
|------|------|------|
| 张三 | 25   | 工程师 |
| 李四 | 30   | 设计师 |
| 王五 | 28   | 产品经理 |

//不完整表格字符串
| 姓名 | 年龄 | 职业 |
|------|-----

渲染效果:

处理这种不完整的表格字符串,我们也可以使用正则替换掉不完整的表格字符串

注意:一旦分隔符和表头数量一致后就可以放行渲染,避免等待时间过长

js 复制代码
/**
 * 过滤流式输出中结构不完整的表格字符串
 * @param {string} content - 流式输出的原始内容
 * @returns {string} 过滤后的内容(仅保留合法表格,非法表格替换为空)
 */
function filterInvalidTables(content) {
  // 表头加载完成后过滤
  // const tableRegex = /(?:^\|(?:\s*.+?\s*)?\|?$[\n\r]?)+(?:^\|(?:\s*[-:]+)+(?:\s*\|\s*[-:]+)*\s*\|?$[\n\r]?)+(?:^\|(?:\s*.+?\s*)?\|?$[\n\r]?)*(?=\n|$)/gm;
  //宽松模式过滤
  const tableRegex = /^\|(?:\s*.+?\s*)?\|?$(?:\r?\n^\|(?:\s*[-:]+)+(?:\s*\|\s*[-:]+)*\s*\|?$(?:\r?\n^\|(?:\s*.+?\s*)?\|?$)*)?/gm;
  return content.replace(tableRegex, (match) => {
    // 分割表头行和分隔符行
    const lines = match.trim().split(/[\r\n]+/).filter(line => line.trim());
    if (lines.length < 2) return ''; // 至少需要表头行 + 分隔符行
    // 最后一行表头(处理多行表头场景)
    const headerLine = lines[0].trim();
    // 分隔符行
    const separatorLine = lines[1].trim();

    // 提取表头列数:分割 | 后,过滤空字符串(处理前后 | 的情况)
    const headerColumns = headerLine.split('|').map(col => col.trim()).filter(col => col);
    const headerCount = headerColumns.length;

    // 提取分隔符列数:分割 | 后,过滤空字符串,且必须包含至少1个 -
    const separatorColumns = separatorLine.split('|')
      .map(col => col.trim())
      .filter(col => col && /-/.test(col)); // 分隔符必须包含 -
    const separatorCount = separatorColumns.length;

    // 仅当列数完全一致时保留表格,否则替换为空
    return (headerCount === separatorCount && headerCount>0 && separatorCount>0) ? match : '';
  });
}

不完整的数学公式

md 复制代码
//完整的数学公式
$$
\\frac{n!}{k!(n-k)!} = \\binom{n}{k}
$$

//不完整的数学公式
$$
\\frac{n!}{k!(n-k)!

渲染效果:

针对这种也可以使用正则替换不完整的代码块为空

js 复制代码
/**
 * 清除 Markdown 中未闭合的块级公式($$ 开头未闭合)
 * @param {string} markdown - 原始 Markdown 字符串
 * @returns {string} 处理后的 Markdown 字符串
 */
function clearUnclosedBlockMath(markdown) {
  // 正则说明:
  // 1. /\$\$(?!.*?\$\$).*$/s - 核心正则
  // 2. \$\$ - 匹配块级公式开始标记
  // 3. (?!.*?\$\$) - 正向否定预查:确保后面没有 $$ 闭合(非贪婪匹配任意字符)
  // 4. .*$ - 匹配从 $$ 开始到字符串结束的所有内容
  // 5. s 修饰符 - 让 . 匹配换行符(支持多行公式)
  // 6. g 修饰符 - 全局匹配(处理多个未闭合公式的极端情况)
  return markdown.replace(/\$\$(?!.*?\$\$).*$/gs, '');
}

结语

正则在处理这种问题的时候,简单粗暴但有用,有点俄式美学的味道~ 最后,如果你觉得这个文章对你有帮助,不妨点个赞并分享给更多的开发者朋友,让我们一起让 Markdown 解析变得更简单、更强大!

GitHub源码仓库地址 如果觉得好用,欢迎给个Star ⭐️ 支持一下!

相关推荐
0思必得022 分钟前
[Web自动化] Selenium处理动态网页
前端·爬虫·python·selenium·自动化
东东5161 小时前
智能社区管理系统的设计与实现ssm+vue
前端·javascript·vue.js·毕业设计·毕设
catino1 小时前
图片、文件的预览
前端·javascript
2501_920931702 小时前
React Native鸿蒙跨平台实现推箱子游戏,完成玩家移动与箱子推动,当所有箱子都被推到目标位置时,玩家获胜
javascript·react native·react.js·游戏·ecmascript·harmonyos
layman05283 小时前
webpack5 css-loader:从基础到原理
前端·css·webpack
半桔3 小时前
【前端小站】CSS 样式美学:从基础语法到界面精筑的实战宝典
前端·css·html
AI老李3 小时前
PostCSS完全指南:功能/配置/插件/SourceMap/AST/插件开发/自定义语法
前端·javascript·postcss
_OP_CHEN3 小时前
【前端开发之CSS】(一)初识 CSS:网页化妆术的终极指南,新手也能轻松拿捏页面美化!
前端·css·html·网页开发·样式表·界面美化
啊哈一半醒3 小时前
CSS 主流布局
前端·css·css布局·标准流 浮动 定位·flex grid 响应式布局
PHP武器库3 小时前
ULUI:不止于按钮和菜单,一个专注于“业务组件”的纯 CSS 框架
前端·css