Как разбить текст на предложения?
Используйте Intl.Segmenter для разбивки текста на предложения с учётом языка, пунктуации, сокращений и языковых правил.
Введение
Когда вы обрабатываете текст для перевода, анализа или отображения, часто нужно разбить его на отдельные предложения. Простой способ с регулярными выражениями не работает, потому что границы предложений сложнее, чем просто точки с пробелами. Предложения могут заканчиваться вопросительными или восклицательными знаками, многоточиями. Точки встречаются в сокращениях вроде «Dr.» или «Inc.», но не всегда означают конец предложения. В разных языках используются разные знаки препинания для завершения предложений.
API Intl.Segmenter решает эту задачу, предоставляя определение границ предложений с учётом языка. Он понимает правила для разных языков и автоматически обрабатывает сокращения, числа и сложную пунктуацию.
Проблема с разбиением по точкам
Можно попробовать разбить текст на предложения, разделяя по точкам с пробелами.
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);
// ["私は猫です。", "名前はまだない。"]
Текст корректно разбивается по японским терминаторам предложений. Сегментатор, настроенный на английский, не распознает эти границы правильно.
В хинди предложения могут заканчиваться вертикальной чертой ।, которая называется пурна вирама.
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, если индекс вне диапазона.
Проверка поддержки в браузере и среде выполнения
API Intl.Segmenter доступен в современных браузерах и Node.js. Он стал частью web platform baseline в апреле 2024 года и поддерживается всеми основными браузерными движками.
Перед использованием можно проверить, доступен ли этот 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);
}
// 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, если он доступен, и возвращается к разбиению с помощью регулярных выражений в старых средах. В этом случае теряются такие возможности, как обработка аббревиатур и языковые особенности определения границ, но сохраняется базовая функциональность.