《npm包》文本流分割器

使用场景

  • 在后端开发中,服务器通过SSE向客户端发送响应时,文本内容常常是分块传输的。例如,一个长文本可能被拆分成多个片段,逐块发送给客户端。然而,这些片段在传输过程中可能会被截断,导致语义不完整或标点符号被错误分割。为了将这些分块的文本重新组合成完整且有意义的段落,需要一个工具来智能地收集和处理这些片段。

安装

使用 pnpm 进行安装

bash 复制代码
pnpm install @minto-ai/text-stream-slicer

使用

引入库

javascript 复制代码
import TextStreamSlicer from '@minto-ai/text-stream-slicer'

创建应用实例

javascript 复制代码
const textStreamSlicer = new TextStreamSlicer()

可用方法

分割文本

处理输入文本,返回分段后的文本数组。

javascript 复制代码
console.log(textStreamSlicer.processText('你好呀。'))

console.log(textStreamSlicer.processText(`抱我吗?`, true))
参数
  • text (string): 输入文本内容。
  • [includeRemaining=false] (boolean): 是否包含未完成的段落。
返回

(Array): 分段后的文本数组。

示例代码

javascript 复制代码
import TextStreamSlicer from '@minto-ai/text-stream-slicer'

const textStreamSlicer = new TextStreamSlicer()

const arrText = [
  `你要抱`,
  `抱我吗?`,
  `我好`,
  `喜欢你呀!`,
]

let index = 0

const timer = setInterval(() => {
  console.log(textStreamSlicer.processText(arrText[index], index === arrText.length - 1))

  if (index === arrText.length - 1) {
    clearInterval(timer)
  }
  index += 1
}, 100)

实现源码

typescript 复制代码
enum TokenTypeEnum {
  SPACE_CHAR = 'spaceChar',
  CHINESE_CHAR = 'chineseChar',
  CHINESE_PUNCTUATION = 'chinesePunctuation',
  ENGLISH_WORD = 'englishWord',
  ENGLISH_PUNCTUATION = 'englishPunctuation',
  UNKNOWN_CHAR = 'unknownChar',
}

interface Token {
  type: TokenTypeEnum
  value: string
}

/**
 * 文本流分段器 - 用于将连续文本按语言规则智能分割成有意义的段落和片段
 * 支持中文、英文混合文本处理,正确识别单词、标点符号和数字,避免小数点等特殊符号导致的错误分段
 */
class TextStreamSplitter {
  // 中文字符
  private readonly CHINESE_CHAR_REGEX = /[\u4E00-\u9FA5]/
  // 中文标点符号
  private readonly CHINESE_PUNCTUATION_REGEX = /[。?!,;:""''()---...、·]/
  // // 中文段落结束符号(句末标点)
  private readonly CHINESE_PARAGRAPH_END_REGEX = /[。?!;]/
  // 英文单词起始字符
  private readonly ENGLISH_WORD_START_REGEX = /[a-z]/i
  // 英文单词延续字符(含缩写和连字符)
  private readonly ENGLISH_WORD_CONTINUE_REGEX = /[a-z''-]/i
  // 英文标点符号
  private readonly ENGLISH_PUNCTUATION_REGEX = /[.,!?;:'"()-]/
  // 英文段落结束符号(句末标点)
  private readonly ENGLISH_PARAGRAPH_END_REGEX = /[.!?;]/
  //  空白字符(统一转为单个空格)
  private readonly SPACE_CHAR_REGEX = /\s/
  // 最小段落长度
  private readonly MIN_PARAGRAPH_LENGTH = 10

  // 待处理的剩余文本标记
  private pendingTokens: Token[] = []

  /**
   * 将输入文本转换为标记流。
   * @param text 原始文本内容。
   * @returns 标记化后的文本标记数组。
   */
  private tokenizeText(text: string): Token[] {
    const tokens: Token[] = []
    let currentIndex = 0

    while (currentIndex < text.length) {
      const char = text[currentIndex]

      // 识别中文字符
      if (this.CHINESE_CHAR_REGEX.test(char)) {
        tokens.push({
          type: TokenTypeEnum.CHINESE_CHAR,
          value: char,
        })
      }
      // 识别英文单词
      else if (this.ENGLISH_WORD_START_REGEX.test(char)) {
        const startIndex = currentIndex
        let endIndex = currentIndex + 1

        // 提取完整英文单词
        while (endIndex < text.length && this.ENGLISH_WORD_CONTINUE_REGEX.test(text[endIndex])) {
          endIndex++
        }

        const word = text.substring(startIndex, endIndex)
        tokens.push({
          type: TokenTypeEnum.ENGLISH_WORD,
          value: word,
        })

        // 跳过已处理的字符
        currentIndex += word.length - 1
      }
      // 识别中文标点符号
      else if (this.CHINESE_PUNCTUATION_REGEX.test(char)) {
        tokens.push({
          type: TokenTypeEnum.CHINESE_PUNCTUATION,
          value: char,
        })
      }
      // 识别英文标点符号
      else if (this.ENGLISH_PUNCTUATION_REGEX.test(char)) {
        tokens.push({
          type: TokenTypeEnum.ENGLISH_PUNCTUATION,
          value: char,
        })
      }
      // 识别空白字符
      else if (this.SPACE_CHAR_REGEX.test(char)) {
        tokens.push({
          type: TokenTypeEnum.SPACE_CHAR,
          value: char,
        })
      }
      // 未知字符类型
      else {
        tokens.push({
          type: TokenTypeEnum.UNKNOWN_CHAR,
          value: char,
        })
      }

      currentIndex++
    }

    return tokens
  }

  /**
   * 文本净化预处理 - 去除无效字符和格式。
   * @param text 原始文本。
   * @returns 净化后的文本。
   */
  private sanitizeText(text: string): string {
    return text
      .trim() // 去除首尾空白
      .replace(/\s+/g, ' ') // 将连续空白字符转为单个空格
      .replace(/(\d+)\s+/g, '$1') // 去除数字后的空白
      .replace(/\*+/g, '') // 移除星号
      .replace(/[\u{1F600}-\u{1F64F}]/gu, '') // 移除表情符号
  }

  /**
   * 判断标记是否为段落结束符号。
   * @param token 文本标记。
   * @returns 是否为段落结束符号。
   */
  private isParagraphEndToken(token: Token): boolean {
    return (
      this.CHINESE_PARAGRAPH_END_REGEX.test(token.value)
      || this.ENGLISH_PARAGRAPH_END_REGEX.test(token.value)
    )
  }

  /**
   * 将标记数组转换回文本。
   * @param tokens 文本标记数组。
   * @returns 组合后的文本。
   */
  private tokensToString(tokens: Token[]): string {
    return tokens.map(token => token.value).join('')
  }

  /**
   * 将标记流分割成多个段落。
   * @param tokens 文本标记数组。
   * @returns 分段后的标记数组集合。
   */
  private splitToParagraphs(tokens: Token[]): Token[][] {
    const paragraphs: Token[][] = []
    let currentParagraph: Token[] = []

    for (const token of tokens) {
      currentParagraph.push(token)

      // 满足最小长度且遇到段落结束符号时分割段落
      if (
        currentParagraph.length >= this.MIN_PARAGRAPH_LENGTH
        && this.isParagraphEndToken(token)
      ) {
        paragraphs.push(currentParagraph)
        currentParagraph = []
      }
    }

    // 添加剩余的文本
    if (currentParagraph.length > 0) {
      paragraphs.push(currentParagraph)
    }

    return paragraphs
  }

  /**
   * 合并因小数点导致的错误分段。
   * @param paragraphs 分段后的标记数组集合。
   * @returns 合并后的标记数组集合。
   */
  private mergeDecimalParagraphs(paragraphs: Token[][]): Token[][] {
    return paragraphs.reduce((mergedParagraphs: Token[][], currentParagraph: Token[]) => {
      if (mergedParagraphs.length === 0) {
        mergedParagraphs.push(currentParagraph)
      }
      else {
        const lastParagraph = mergedParagraphs[mergedParagraphs.length - 1]
        const lastParagraphText = this.tokensToString(lastParagraph)
        const currentParagraphText = this.tokensToString(currentParagraph)

        // 如果上一段以数字加小数点结尾,且当前段以数字开头,则合并两段
        if (/\d+\.$/.test(lastParagraphText) && /^\d+/.test(currentParagraphText)) {
          mergedParagraphs[mergedParagraphs.length - 1] = [...lastParagraph, ...currentParagraph]
        }
        else {
          mergedParagraphs.push(currentParagraph)
        }
      }

      return mergedParagraphs
    }, [])
  }

  /**
   * 处理输入文本,返回分段后的文本数组。
   * @param text 输入文本内容。
   * @param includeRemaining 是否包含未完成的段落。
   * @returns 分段后的文本数组。
   */
  public processText(text: string, includeRemaining: boolean = false): string[] {
    // 净化并标记化文本
    const tokens = this.tokenizeText(this.sanitizeText(text))

    // 合并历史遗留的未完成段落
    const combinedTokens = [...this.pendingTokens, ...tokens]

    // 分段并处理小数点问题
    const paragraphs = this.mergeDecimalParagraphs(this.splitToParagraphs(combinedTokens))

    // 处理未完成的段落
    if (paragraphs.length > 0) {
      const lastParagraph = paragraphs[paragraphs.length - 1]
      const lastToken = lastParagraph[lastParagraph.length - 1]

      if (includeRemaining) {
        // 返回所有内容,清空待处理标记
        this.pendingTokens = []
      }
      else {
        // 检查最后一段是否完整
        if (this.isParagraphEndToken(lastToken)) {
          // 完整段落但以数字加小数点结尾,视为不完整
          if (/\d+\.$/.test(this.tokensToString(lastParagraph))) {
            this.pendingTokens = paragraphs.pop()!
          }
          else {
            this.pendingTokens = []
          }
        }
        else {
          // 不完整段落,保留待处理
          this.pendingTokens = paragraphs.pop()!
        }
      }
    }

    // 返回分段后的文本
    return paragraphs.map(paragraph => this.tokensToString(paragraph))
  }
}

export default TextStreamSplitter
相关推荐
yuren_xia2 小时前
Spring Boot中保存前端上传的图片
前端·spring boot·后端
普通网友3 小时前
Web前端常用面试题,九年程序人生 工作总结,Web开发必看
前端·程序人生·职场和发展
站在风口的猪11085 小时前
《前端面试题:CSS对浏览器兼容性》
前端·css·html·css3·html5
青莳吖6 小时前
使用 SseEmitter 实现 Spring Boot 后端的流式传输和前端的数据接收
前端·spring boot·后端
CodeCraft Studio7 小时前
PDF处理控件Aspose.PDF教程:在 C# 中更改 PDF 页面大小
前端·pdf·c#
拉不动的猪7 小时前
TS常规面试题1
前端·javascript·面试
再学一点就睡8 小时前
实用为王!前端日常工具清单(调试 / 开发 / 协作工具全梳理)
前端·资讯·如何当个好爸爸
Jadon_z8 小时前
vue2 项目中 npm run dev 运行98% after emitting CopyPlugin 卡死
前端·npm
一心赚狗粮的宇叔8 小时前
web全栈开发学习-01html基础
前端·javascript·学习·html·web
IT瘾君8 小时前
JavaWeb:前端工程化-ElementPlus
前端·elementui·node.js·vue