使用Intl.Segmenter返回更准确的字符长度

这篇文章发布于 2025年09月30日,星期二,10:57,归类于 JS API。 阅读 171 次, 今日 169 次 一条评论

 

分词与字符长度

一、Emoji字符长度问题

我们平常获取字符串的长度都是直接使用length属性,但对于某些字符,返回的结果会在预期之外,例如:

"鑫空间".length

"🌝".length

"🇮🇳".length

"👨‍👩‍👧‍👦".length

"दरबार".length

结果如下:

"鑫空间".length // 3

"🌝".length    // 2

"🇮🇳".length    // 4

"👨‍👩‍👧‍👦".length    // 11

"दरबार".length  // 5

看到没,看起来Emoji表情字符的长度是1,但是,实际返回的Emoji长度并不是1,不仅不是1,不同的Emoji返回的长度还不一样,这就会给我们精确统计字符长度带来困扰。

怎么办呢?

二、返回字符的真实长度

我们可以使用Intl.Segmenter()返回字符串的真实长度,例如,我们不妨给字符串扩展一个名为 realLength 的方法,完整代码如下所示:

Object.defineProperty(String.prototype, 'realLength', {
  get: function() {
    return Array.from(
      new Intl.Segmenter("en", {
        // 颗粒度:字位,默认值,可省略
        granularity: "grapheme" 
      }).segment(this)
	).length;
  }
});

此时,我们就可以返回字符串真实的长度了。

"鑫空间".realLength // 3

"🌝".realLength    // 1

"🇮🇳".realLength    // 1

"👨‍👩‍👧‍👦".realLength    // 1

"दरबार".realLength  // 4

是不是很美丽?

紫灵

三、Emoji字符长度不是1的原因

JavaScript使用UTF-16编码,这种编码方式最多支持 2x 16 共 65,536 个字符,6万多个字符看起来多,但是世界上那么多语言,已经不够用了,尤其Emoji表情字符,都在65,536 个字符开外。

JS想要表示,只能组合多个特殊的 2 字节代码单元以生成人类可读的字符。

例如:

Array.from({ length: "🌝".length }, (_, i) => "🌝"[i])
// [ '\ud83c', '\udf1d' ]

于是,JS代码直接使用length属性访问的时候,返回值就是2,但是人类理解肯定是1个字符。

这就是Emoji字符长度不是1的原因。

四、关于Intl.Segmenter

Intl.Segmenter 是 JavaScript 中一个专门用于文本分段的强大工具,它能够根据不同的语言和文本规则,将字符串智能地分割成有意义的单位,如字符、单词或句子。

核心概念与语法

Intl.Segmenter 的核心作用在于进行语言敏感的分段。

传统的字符串分割方法(如 split(‘ ‘))对于英语这类使用空格分隔单词的语言勉强适用,但对于中文、日文等不依赖空格的语言就无能为力了,甚至处理包含复杂表情符号(Emoji)的文本时也会出错。

Intl.Segmenter 则依据 Unicode 标准,能够理解不同语言的语法习惯,进行精准的边界检测。

使用示意:

const segmenter = new Intl.Segmenter([locales[, options]]);

其中:

locales
表示语言,使用一个 BCP 47 语言标签的字符串,如 “zh-CN”、”en-US”、”ja”等。它指定了分段所依据的语言规则。如果省略,则会使用运行时环境的默认语言设置。
options
可选参数,包括下面这些:

granularity

表示分段的粒度,是最重要的选项。可取值为:

  • “grapheme”(默认值):在用户感知的字符(字形簇)边界分割。例如,一个基础字符加上修饰符(如 “a” + “́” 组成 “á”)会被视为一个整体,复杂的 Emoji 序列(如家庭表情 👨‍👩‍👧‍👦)也不会被拆散。
  • “word”:在单词边界分割。它能正确识别中文的词汇单元(如将“我真的很强”分割为“我”、“真的”、“很”、“强”),并能区分标点符号和空格(isWordLike 属性为 false)。
  • “sentence”:在句子边界分割。它能智能区分句号的不同用途,例如不会将数字 “100.000” 中的点号误判为句子结束。

localeMatcher

指定所使用的语言匹配算法,可以是 “best fit”(默认值,使用环境提供的可能最匹配的算法)或 “lookup”(使用特定的 BCP 47 算法)。

基本使用方法

创建分段器实例后,通过调用其 segment(inputString) 方法对文本进行分段。该方法返回一个 Segments 可迭代对象。

例如:

// 创建一个中文词段器
const segmenter = new Intl.Segmenter('zh-CN', { granularity: 'word' });
const text = "《HTML并不简单》是国内唯一一本精讲HTML的技术书籍";
const segments = segmenter.segment(text); // 返回一个 Segments 对象

// 遍历
for (const segment of segments) {
  console.log(`片段: "${segment.segment}", 起始索引: ${segment.index}, 是否像词: ${segment.isWordLike}`);
}

在我的Chrome浏览器下,输出的结果是:

// 片段: "《", 起始索引: 0, 是否像词: false
// 片段: "HTML", 起始索引: 1, 是否像词: true
// 片段: "并不", 起始索引: 5, 是否像词: true
// 片段: "简单", 起始索引: 7, 是否像词: true
// 片段: "》", 起始索引: 9, 是否像词: false
// 片段: "是", 起始索引: 10, 是否像词: true
// 片段: "国内", 起始索引: 11, 是否像词: true
// 片段: "唯一", 起始索引: 13, 是否像词: true
// 片段: "一本", 起始索引: 15, 是否像词: true
// 片段: "精", 起始索引: 17, 是否像词: true
// 片段: "讲", 起始索引: 18, 是否像词: true
// 片段: "HTML", 起始索引: 19, 是否像词: true
// 片段: "的", 起始索引: 23, 是否像词: true
// 片段: "技术", 起始索引: 24, 是否像词: true
// 片段: "书籍", 起始索引: 26, 是否像词: true

可以看到浏览器自带对中文句子进行分词了。

兼容性

目前Intl.Segmenter所有现代浏览器均已支持,前端也能自动分词分句了。

Intl.Segmenter兼容性

五、结语

Intl命名对象还有很多其他方法,多是与语言、金钱、数字、字符处理相关的。

详见我之前的这篇文章:“JS Intl对象完整简介及在中文中的应用

还是非常强大与实用的。

就是语法复杂了点,学习成本比较高,真正使用的时候,需要去搜索查找资料。

不过现在都有AI了,只需要知道有这么个东西就好了,AI会帮你搞定。

行吧,就说这么多。

明天就是国庆节了,祝大家国庆节快乐!

不说了,我要准备出发去钓鱼了!

欢迎大家关注我的钓鱼账号:“最会钓鱼的程序员”。

随时了解我国庆节的战况。

张鑫旭本人

😉😊😇
🥰😍😘

(本篇完)

分享到:


发表评论(目前一条评论)

  1. voidfeng说道:

    沙发🛋!