如何将文本分割为句子?

使用 Intl.Segmenter 通过支持本地化的边界检测,将文本分割为句子,自动处理标点符号、缩写和语言特定规则。

简介

在处理文本以进行翻译、分析或显示时,通常需要将其分割为单独的句子。仅用正则表达式进行分割的方法并不可靠,因为句子边界远比句点加空格复杂。句子可能以问号、感叹号或省略号结尾。缩写如 "Dr." 或 "Inc." 中的句点并不表示句子结束。不同语言还会使用不同的标点符号作为句子终止符。

Intl.Segmenter API 解决了这个问题,能够根据本地化规则检测句子边界。它理解不同语言中识别句子边界的规则,并能自动处理缩写、数字和复杂标点等特殊情况。

仅用句点分割存在的问题

你可以尝试通过句点加空格来分割文本为句子。

const text = "Hello world. How are you? I am fine.";
const sentences = text.split(". ");
console.log(sentences);
// ["Hello world", "How are you? I am fine."]

这种方法存在多个问题。首先,它无法处理问号或感叹号。其次,它会在包含句点的缩写处错误分割。第三,除了最后一句外,每个句子的句点都会被移除。第四,当句点后有多个空格时,该方法也无法正确处理。

const text = "Dr. Smith works at Acme Inc. He starts at 9 a.m.";
const sentences = text.split(". ");
console.log(sentences);
// ["Dr", "Smith works at Acme Inc", "He starts at 9 a.m."]

在 "Dr." 和 "Inc." 处文本被错误分割,因为这些缩写中包含句点。你需要一种更智能的方法,能够理解句子边界规则。

使用更复杂的正则表达式

你可以通过改进正则表达式来处理更多情况。

const text = "Hello world. How are you? I am fine!";
const sentences = text.split(/[.?!]\s+/);
console.log(sentences);
// ["Hello world", "How are you", "I am fine", ""]

这种方法会在句点、问号和感叹号后跟空白字符时进行分割。它能处理更多情况,但在缩写处仍然会出错,并且会产生空字符串。同时,它也会移除每个句子的标点符号。

const text = "Dr. Smith works at Acme Inc. He starts at 9 a.m.";
const sentences = text.split(/[.?!]\s+/);
console.log(sentences);
// ["Dr", "Smith works at Acme Inc", "He starts at 9 a", "m", ""]

正则表达式方法无法可靠地区分句末的句点和缩写中的句点。要构建一个能处理所有边界情况的完整正则表达式几乎不现实。你需要一个能够理解语言规则的解决方案。

使用 Intl.Segmenter 进行句子分割

Intl.Segmenter 构造函数会根据本地化规则创建一个分段器,用于按区域设置分割文本。你需要指定 locale,并将 granularity 选项设置为 "sentence"

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world. How are you? I am fine!";
const segments = segmenter.segment(text);

for (const segment of segments) {
  console.log(segment.segment);
}
// "Hello world. "
// "How are you? "
// "I am fine!"

segment() 方法会返回一个可迭代对象,依次生成分段对象。每个分段对象都包含一个 segment 属性,存储该分段的文本。分段器会保留每个句子末尾的标点和空白字符。

你可以使用 Array.from() 将分段转换为数组。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world. How are you? I am fine!";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["Hello world. ", "How are you? ", "I am fine!"]

这样会创建一个数组,每个元素都是带有原始标点和空格的句子。

Intl.Segmenter 如何处理缩写

分段器能够识别常见的缩写模式,不会在缩写中的句点处分割。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Dr. Smith works at Acme Inc. He starts at 9 a.m.";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["Dr. Smith works at Acme Inc. ", "He starts at 9 a.m."]

文本会被正确分割为两句话。"Dr."、"Inc." 和 "a.m." 中的句点不会触发断句,因为分段器能识别这些是缩写。正是这种对边界情况的自动处理,使 Intl.Segmenter 优于正则表达式方法。

去除句子中的空白字符

分段器会将句子末尾的空白字符包含在每个句子中。如有需要,你可以去除这些空白。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world. How are you? I am fine!";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment.trim());
console.log(sentences);
// ["Hello world.", "How are you?", "I am fine!"]

trim() 方法会移除每个句子的首尾空白字符。当你需要干净的句子边界且不希望有多余空格时,这非常有用。

获取分段元数据

每个分段对象都包含该分段在原始文本中的位置信息元数据。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world. How are you?";
const segments = segmenter.segment(text);

for (const segment of segments) {
  console.log({
    text: segment.segment,
    index: segment.index,
    input: segment.input
  });
}
// { text: "Hello world. ", index: 0, input: "Hello world. How are you?" }
// { text: "How are you?", index: 13, input: "Hello world. How are you?" }

index 属性表示该分段在原始文本中的起始位置。input 属性包含完整的原始文本。当你需要追踪句子位置或重建原始文本时,这些元数据非常有用。

不同语言的句子切分

不同语言有不同的句子边界规则。分段器会根据指定的 locale 自动调整其行为。

在日语中,句子可以以全角句号 (称为 kuten)结尾。

const segmenter = new Intl.Segmenter("ja", { granularity: "sentence" });
const text = "私は猫です。名前はまだない。";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["私は猫です。", "名前はまだない。"]

文本会在日语句子终止符处正确切分。如果分段器配置为英文,则无法正确识别这些边界。

在印地语中,句子可以以竖线 (称为 purna viram)结尾。

const segmenter = new Intl.Segmenter("hi", { granularity: "sentence" });
const text = "यह एक वाक्य है। यह दूसरा वाक्य है।";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["यह एक वाक्य है। ", "यह दूसरा वाक्य है।"]

分段器能够识别天城文句号作为句子边界。这种基于 locale 的行为对于国际化文本处理至关重要。

多语言文本使用正确的 locale

处理包含多种语言的文本时,应选择与文本主要语言相匹配的 locale。分段器会根据指定的 locale 判断适用的边界规则。

const englishText = "Hello world. How are you?";
const japaneseText = "私は猫です。名前はまだない。";

const englishSegmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const japaneseSegmenter = new Intl.Segmenter("ja", { granularity: "sentence" });

const englishSentences = Array.from(
  englishSegmenter.segment(englishText),
  s => s.segment
);

const japaneseSentences = Array.from(
  japaneseSegmenter.segment(japaneseText),
  s => s.segment
);

console.log(englishSentences);
// ["Hello world. ", "How are you?"]

console.log(japaneseSentences);
// ["私は猫です。", "名前はまだない。"]

为每种语言分别创建分段器可以确保边界检测的准确性。如果处理的文本语言未知,可以使用 "en" 这样的通用 locale 作为回退,但这会降低非英文文本的准确性。

处理无句子边界的文本

当文本中没有句子终止符时,分段器会将整个文本作为一个分段返回。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["Hello world"]

这种行为是正确的,因为文本中没有任何句子边界。分段器不会人为地拆分构成单个句子的文本。

处理空字符串

分段器通过返回一个空迭代器来处理空字符串。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// []

这会生成一个空数组,这是空输入时的预期结果。

复用分段器以提升性能

创建分段器有一定的开销。当你需要用相同的 locale 和选项分段多个文本时,应只创建一次分段器并复用。

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });

const texts = [
  "First text. With two sentences.",
  "Second text. With three sentences. And more.",
  "Third text."
];

texts.forEach(text => {
  const sentences = Array.from(segmenter.segment(text), s => s.segment);
  console.log(sentences);
});
// ["First text. ", "With two sentences."]
// ["Second text. ", "With three sentences. ", "And more."]
// ["Third text."]

复用分段器比为每个文本都新建一个分段器更高效。

构建句子计数函数

你可以使用分段器统计文本中的句子数。

function countSentences(text, locale = "en") {
  const segmenter = new Intl.Segmenter(locale, { granularity: "sentence" });
  const segments = segmenter.segment(text);
  return Array.from(segments).length;
}

console.log(countSentences("Hello world. How are you?"));
// 2

console.log(countSentences("Dr. Smith works at Acme Inc. He starts at 9 a.m."));
// 2

console.log(countSentences("Single sentence"));
// 1

console.log(countSentences("私は猫です。名前はまだない。", "ja"));
// 2

该函数会创建分段器,分割文本,并返回分段数量。它能正确处理缩写和特定语言的边界。

构建句子提取函数

你可以创建一个函数,通过索引从文本中提取指定的句子。

function getSentence(text, index, locale = "en") {
  const segmenter = new Intl.Segmenter(locale, { granularity: "sentence" });
  const segments = Array.from(segmenter.segment(text), s => s.segment);
  return segments[index] || null;
}

const text = "First sentence. Second sentence. Third sentence.";

console.log(getSentence(text, 0));
// "First sentence. "

console.log(getSentence(text, 1));
// "Second sentence. "

console.log(getSentence(text, 2));
// "Third sentence."

console.log(getSentence(text, 3));
// null

该函数会返回指定索引处的句子,如果索引越界,则返回 null

检查浏览器和运行时支持情况

Intl.Segmenter API 在现代浏览器和 Node.js 中均可用。它自 2024 年 4 月起成为 Web 平台基线的一部分,并已被所有主流浏览器引擎支持。

你可以在使用前检查该 API 是否可用。

if (typeof Intl.Segmenter !== "undefined") {
  const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
  const text = "Hello world. How are you?";
  const sentences = Array.from(segmenter.segment(text), s => s.segment);
  console.log(sentences);
} else {
  console.log("Intl.Segmenter is not supported");
}

对于不支持的环境,你需要提供降级方案。简单的降级方式是用基础的正则表达式分割,但这会失去 locale 感知分段的准确性。

function splitSentences(text, locale = "en") {
  if (typeof Intl.Segmenter !== "undefined") {
    const segmenter = new Intl.Segmenter(locale, { granularity: "sentence" });
    return Array.from(segmenter.segment(text), s => s.segment);
  }

  // Fallback for older environments
  return text.split(/[.!?]\s+/).filter(s => s.length > 0);
}

console.log(splitSentences("Hello world. How are you?"));
// ["Hello world. ", "How are you?"]

此函数在可用时会使用 Intl.Segmenter,在较旧的环境下则回退为正则表达式分割。回退方案会丢失如缩写处理和特定语言边界检测等功能,但能提供基本功能。