如何在 JavaScript 中将文本分割为单词?
使用 Intl.Segmenter 可从任何语言的文本中提取单词,包括没有单词间空格的语言。
简介
当你需要从文本中提取单词时,常见的方法是使用 split(" ") 按空格分割。这种方式适用于英文,但对于不使用空格分隔单词的语言则完全无效。中文、日文、泰文等语言的文本是连续书写的,没有单词分隔符,但用户依然能感知到其中的独立单词。
Intl.Segmenter API 解决了这个问题。它根据 Unicode 标准和各语言的语言学规则识别单词边界。无论目标语言是否使用空格,你都可以从文本中提取单词,分词器会自动处理单词起止的复杂性。
本文将解释为什么基础的字符串分割方法无法处理国际化文本,不同书写系统中的单词边界如何定义,以及如何使用 Intl.Segmenter 正确地将文本分割为所有语言的单词。
为什么按空格分割会失败
split() 方法会在每次遇到分隔符时将字符串断开。对于英文文本,按空格分割可以提取单词。
const text = "Hello world";
const words = text.split(" ");
console.log(words);
// ["Hello", "world"]
这种方法假设单词之间有空格分隔。但许多语言并不遵循这种模式。
中文文本中单词之间没有空格。
const text = "你好世界";
const words = text.split(" ");
console.log(words);
// ["你好世界"]
用户看到的是两个独立的单词,但 split() 返回的却是整个字符串作为一个元素,因为没有空格可供分割。
日文文本混合了多种书写体系,也没有单词间的空格。
const text = "今日は良い天気です";
const words = text.split(" ");
console.log(words);
// ["今日は良い天気です"]
这个句子包含多个单词,但用空格分割时只会得到一个元素。
泰语文本同样是单词连续书写,没有空格分隔。
const text = "สวัสดีครับ";
const words = text.split(" ");
console.log(words);
// ["สวัสดีครับ"]
该文本包含两个单词,但 split() 返回的是一个元素。
对于这些语言,需要采用不同的方法来识别词语边界。
为什么正则表达式无法处理词语边界
正则表达式的词语边界使用 \b 模式来匹配单词字符与非单词字符之间的位置。这种方式适用于 English。
const text = "Hello world!";
const words = text.match(/\b\w+\b/g);
console.log(words);
// ["Hello", "world"]
这种模式对于没有空格的语言会失效,因为正则引擎无法识别 Chinese、Japanese 或 Thai 等文字系统中的词语边界。
const text = "你好世界";
const words = text.match(/\b\w+\b/g);
console.log(words);
// ["你好世界"]
正则表达式会把整个字符串当作一个单词处理,因为它无法理解 Chinese 的词语边界。
即使在 English 中,正则表达式模式在遇到标点符号、缩写或特殊字符时也可能产生错误结果。正则表达式并不是为处理所有书写系统的语言学词语分割而设计的。
各语言中的词语边界是什么
词语边界是文本中一个单词结束、另一个单词开始的位置。不同的书写系统采用不同的词语边界标记方式。
以空格分隔的语言(如 English、Spanish、French 和 German)使用空格来标记词语边界。单词 "hello" 与 "world" 之间用空格分隔。
Scriptio continua 语言(如 Chinese、Japanese 和 Thai)不在单词之间使用空格。词语边界基于语义和形态规则存在,但在文本中没有视觉标记。Chinese 读者通过对语言的熟悉来识别词语的起止,而不是依靠视觉分隔符。
有些语言采用混合书写规范。例如,日语结合了汉字、平假名和片假名,词语边界通常出现在不同字符类型的切换处,或根据语法结构确定。
Unicode 标准在 UAX 29 中定义了词边界规则。这些规则规定了如何为所有书写系统识别词边界。规则会根据字符属性、书写类型和语言模式来判断词语的起止位置。
使用 Intl.Segmenter 将文本分割为单词
Intl.Segmenter 构造函数会创建一个分段器对象,按照 Unicode 规则对文本进行分割。你需要指定 locale 和 granularity。
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world!";
const segments = segmenter.segment(text);
第一个参数是 locale 标识符。第二个参数是一个 options 对象,其中 granularity: "word" 用于指定分段器按词边界分割。
segment() 方法会返回一个可迭代对象,包含所有分段。你可以使用 for...of 遍历这些分段。
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world!";
for (const segment of segmenter.segment(text)) {
console.log(segment);
}
// { segment: "Hello", index: 0, input: "Hello world!", isWordLike: true }
// { segment: " ", index: 5, input: "Hello world!", isWordLike: false }
// { segment: "world", index: 6, input: "Hello world!", isWordLike: true }
// { segment: "!", index: 11, input: "Hello world!", isWordLike: false }
每个分段对象包含以下属性:
segment:该分段的文本index:该分段在原始字符串中的起始位置input:被分割的原始字符串isWordLike:该分段是单词还是非单词内容
理解 isWordLike 属性
当你按单词分割文本时,分段器会返回单词分段和非单词分段。单词分段包括字母、数字和表意字符。非单词分段包括空格、标点和其他分隔符。
isWordLike 属性用于指示分段是否为单词。如果分段包含单词字符,该属性为 true;如果只包含空格、标点或其他非单词字符,则为 false。
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello, world!";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "Hello" true
// "," false
// " " false
// "world" true
// "!" false
使用 isWordLike 属性可以将单词片段与标点和空白字符区分开来。这样可以只获取单词,不包含分隔符。
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello, world!";
const segments = segmenter.segment(text);
const words = Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment);
console.log(words);
// ["Hello", "world"]
此模式适用于任何语言,包括没有空格的语言。
从无空格文本中提取单词
分词器能够在不使用空格的语言中正确识别单词边界。对于中文文本,分词器会根据 Unicode 规则和语言学模式在单词边界进行分割。
const segmenter = new Intl.Segmenter("zh", { granularity: "word" });
const text = "你好世界";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "你好" true
// "世界" true
分词器在此文本中识别出两个单词。虽然没有空格,但分词器能够理解中文的单词边界,并正确分割文本。
对于日文文本,分词器能够处理混合书写体系的复杂性,并识别单词边界。
const segmenter = new Intl.Segmenter("ja", { granularity: "word" });
const text = "今日は良い天気です";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "今日" true
// "は" true
// "良い" true
// "天気" true
// "です" true
分词器将该句子分为五个单词片段。它能识别像“は”这样的助词为独立单词,以及像“天気”这样的复合词为单一单元。
对于泰文文本,分词器也能在没有空格的情况下识别单词边界。
const segmenter = new Intl.Segmenter("th", { granularity: "word" });
const text = "สวัสดีครับ";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "สวัสดี" true
// "ครับ" true
分词器能正确识别该问候语中的两个单词。
构建单词提取函数
创建一个可以从任意语言文本中提取单词的函数。
function getWords(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
return Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment);
}
该函数适用于以空格分隔和非空格分隔的语言。
getWords("Hello, world!", "en");
// ["Hello", "world"]
getWords("你好世界", "zh");
// ["你好", "世界"]
getWords("今日は良い天気です", "ja");
// ["今日", "は", "良い", "天気", "です"]
getWords("Bonjour le monde!", "fr");
// ["Bonjour", "le", "monde"]
getWords("สวัสดีครับ", "th");
// ["สวัสดี", "ครับ"]
该函数无论语言或书写系统如何,都会返回单词数组。
跨语言准确统计单词数
通过统计类单词片段,构建一个适用于所有语言的单词计数器。
function countWords(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
return Array.from(segments).filter(s => s.isWordLike).length;
}
该函数可为任何语言的文本生成准确的单词计数。
countWords("Hello world", "en");
// 2
countWords("你好世界", "zh");
// 2
countWords("今日は良い天気です", "ja");
// 5
countWords("Bonjour le monde", "fr");
// 3
countWords("สวัสดีครับ", "th");
// 2
计数方式与每种语言中用户对单词边界的认知一致。
查找某个位置属于哪个单词
containing() 方法用于查找字符串中包含特定索引的分段。这对于确定光标所在的单词或被点击的单词非常有用。
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world";
const segments = segmenter.segment(text);
const segment = segments.containing(7);
console.log(segment);
// { segment: "world", index: 6, input: "Hello world", isWordLike: true }
索引 7 位于单词 "world" 内,该单词从索引 6 开始。该方法会返回该单词对应的分段对象。
如果索引位于空白或标点符号内,方法会返回包含 isWordLike: false 的分段。
const segment = segments.containing(5);
console.log(segment);
// { segment: " ", index: 5, input: "Hello world", isWordLike: false }
可用于文本编辑器的功能,例如双击选中单词、基于光标位置的上下文菜单或高亮当前单词。
处理标点符号和缩写词
分段器会将标点符号视为独立的分段。英语中的缩写词通常会被拆分为多个分段。
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "I can't do it.";
for (const { segment, isWordLike } of segmenter.segment(text)) {
console.log(segment, isWordLike);
}
// "I" true
// " " false
// "can" true
// "'" false
// "t" true
// " " false
// "do" true
// " " false
// "it" true
// "." false
缩写 "can't" 会被拆分为 "can"、"'" 和 "t"。如果需要将缩写作为单个单词处理,则需通过额外逻辑根据撇号合并分段。
对于大多数场景,即使缩写被拆分,统计类单词分段的数量也能得到有意义的单词计数。
语言区域如何影响单词分段
传递给分段器的语言区域会影响单词边界的判定。不同的语言区域对同一文本可能有不同的规则。
对于具有明确单词边界规则的语言,语言区域可确保应用正确的分段规则。
const segmenterEn = new Intl.Segmenter("en", { granularity: "word" });
const segmenterZh = new Intl.Segmenter("zh", { granularity: "word" });
const text = "你好世界";
const wordsEn = Array.from(segmenterEn.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
const wordsZh = Array.from(segmenterZh.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
console.log(wordsEn);
// ["你好世界"]
console.log(wordsZh);
// ["你好", "世界"]
英文区域无法识别中文的单词边界,会将整个字符串视为一个单词。中文区域会应用中文的分词规则,正确识别出两个单词。
始终为被分词的文本选择合适的 locale。
创建可复用的分词器以提升性能
创建分词器的开销不大,但可以在多字符串间复用分词器以获得更好的性能。
const enSegmenter = new Intl.Segmenter("en", { granularity: "word" });
const zhSegmenter = new Intl.Segmenter("zh", { granularity: "word" });
const jaSegmenter = new Intl.Segmenter("ja", { granularity: "word" });
function getWords(text, locale) {
const segmenter = locale === "zh" ? zhSegmenter
: locale === "ja" ? jaSegmenter
: enSegmenter;
return Array.from(segmenter.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
}
这种方法只需创建一次分词器,并在所有 getWords() 调用中复用。分词器会缓存 locale 数据,因此复用实例可避免重复初始化。
实用示例:构建词频分析器
结合分词与计数功能,对文本进行词频分析。
function getWordFrequency(text, locale) {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
const segments = segmenter.segment(text);
const words = Array.from(segments)
.filter(s => s.isWordLike)
.map(s => s.segment.toLowerCase());
const frequency = {};
for (const word of words) {
frequency[word] = (frequency[word] || 0) + 1;
}
return frequency;
}
const text = "Hello world! Hello everyone in this world.";
const frequency = getWordFrequency(text, "en");
console.log(frequency);
// { hello: 2, world: 2, everyone: 1, in: 1, this: 1 }
此函数将文本分割为单词,统一为小写并统计出现次数。适用于任意语言。
const textZh = "你好世界!你好大家!";
const frequencyZh = getWordFrequency(textZh, "zh");
console.log(frequencyZh);
// { "你好": 2, "世界": 1, "大家": 1 }
同样的逻辑无需修改即可处理中文文本。
检查浏览器支持情况
Intl.Segmenter API 于 2024 年 4 月达到 Baseline 状态。该 API 已在当前版本的 Chrome、Firefox、Safari 和 Edge 中可用,旧版浏览器不支持。
在使用该 API 前请先检查支持情况。
if (typeof Intl.Segmenter !== "undefined") {
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
// Use segmenter
} else {
// Fallback for older browsers
}
对于面向旧版浏览器的生产环境应用,请提供降级实现。简单的降级方案是对英文文本使用 split(),其他语言则返回整个字符串。
function getWords(text, locale) {
if (typeof Intl.Segmenter !== "undefined") {
const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
return Array.from(segmenter.segment(text))
.filter(s => s.isWordLike)
.map(s => s.segment);
}
// Fallback: only works for space-separated languages
return text.split(/\s+/).filter(word => word.length > 0);
}
这样可确保代码在旧版浏览器中运行,尽管对于非空格分隔语言功能会有所降低。
常见错误及规避方法
不要对多语言文本直接按空格或正则分割。这些方法只适用于部分语言,对于中文、日文、泰文等无空格语言会失效。
提取单词时不要忘记按 isWordLike 过滤,否则结果中会包含空格、标点及其他非单词片段。
在对文本进行分词时,请勿使用错误的 locale。locale 决定了适用的分词规则。对中文文本使用英文 locale 会导致分词结果错误。
不要假设所有语言对“词”的定义都相同。不同书写系统和语言习惯下,词的边界各不相同。请使用支持 locale 的分词方法来处理这些差异。
不要用 split(" ").length 来统计国际化文本的词数。该方法只适用于以空格分隔的语言,对于其他语言会导致统计错误。
何时使用分词
在以下场景需要使用分词:
- 统计多语言用户生成内容的词数
- 实现支持任意书写系统的搜索和高亮功能
- 构建可处理国际化文本的文本分析工具
- 在文本编辑器中创建基于词的导航或编辑功能
- 从多语言文档中提取关键词或术语
- 在支持任意语言的表单中校验词数限制
仅需统计字符数时,不要使用分词。字符级操作应采用字素(grapheme)分割。
不要用分词来进行句子切分。句子切分应使用句子粒度的分割方法。
分词在国际化中的作用
Intl.Segmenter API 是 ECMAScript 国际化 API 的一部分。该系列的其他 API 处理国际化的不同方面:
Intl.DateTimeFormat:根据 locale 格式化日期和时间Intl.NumberFormat:根据 locale 格式化数字、货币和单位Intl.Collator:根据 locale 排序和比较字符串Intl.PluralRules:确定不同语言下数字的复数形式
这些 API 共同为开发者提供了构建全球用户适用应用所需的工具。当你需要识别单词边界时,请使用 Intl.Segmenter 并设置为单词粒度;而在格式化和比较时,则应使用其他 Intl API。