如何将文本拆分为句子?

使用 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 构造函数创建一个基于特定语言规则分割文本的分段器。您可以指定一个语言环境,并将 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 属性包含完整的原始文本。当您需要跟踪句子位置或重建原始文本时,这些元数据非常有用。

在不同语言中拆分句子

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

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

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);
// ["यह एक वाक्य है। ", "यह दूसरा वाक्य है।"]

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

为多语言文本使用正确的语言环境

当处理包含多种语言的文本时,请选择与文本主要语言相匹配的语言环境。分段器会使用指定的语言环境来确定适用的边界规则。

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" 的通用语言环境作为后备选项,但这会降低对非英语文本的准确性。

处理没有句子边界的文本

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

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);
// []

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

重用分段器以提高性能

创建分段器会有一些开销。当需要对多个具有相同语言环境和选项的文本进行分段时,可以创建一次分段器并重复使用。

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");
}

对于不支持的环境,您需要提供一个回退方案。一个简单的回退方法是使用基本的正则表达式分割,但这会失去基于语言环境的分割准确性。

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);
  }

  // 旧环境的回退方案
  return text.split(/[.!?]\s+/).filter(s => s.length > 0);
}

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

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