Как разбить текст на предложения?
Используйте Intl.Segmenter, чтобы разбить текст на предложения с учетом локализованных границ, обрабатывающих пунктуацию, аббревиатуры и языковые правила.
Введение
Когда вы обрабатываете текст для перевода, анализа или отображения, часто возникает необходимость разделить его на отдельные предложения. Наивный подход с использованием регулярных выражений не работает, потому что границы предложений сложнее, чем просто точки, за которыми следуют пробелы. Предложения могут заканчиваться вопросительными знаками, восклицательными знаками или многоточиями. Точки встречаются в сокращениях, таких как "д-р" или "Инк.", не заканчивая предложения. Разные языки используют разные знаки препинания в качестве разделителей предложений.
API Intl.Segmenter решает эту проблему, предоставляя определение границ предложений с учетом локали. Он понимает правила определения границ предложений на разных языках и автоматически обрабатывает сложные случаи, такие как сокращения, числа и сложные знаки препинания.
Проблема разделения по точкам
Вы можете попытаться разделить текст на предложения, разделяя его по точкам, за которыми следуют пробелы.
const text = "Привет, мир. Как дела? У меня всё хорошо.";
const sentences = text.split(". ");
console.log(sentences);
// ["Привет, мир", "Как дела? У меня всё хорошо."]
Этот подход имеет множество проблем. Во-первых, он не обрабатывает вопросительные или восклицательные знаки. Во-вторых, он ломается на сокращениях, содержащих точки. В-третьих, он удаляет точку из каждого предложения, кроме последнего. В-четвертых, он не работает, если после точек идут несколько пробелов.
const text = "Д-р Смит работает в Acme Inc. Он начинает в 9 утра.";
const sentences = text.split(". ");
console.log(sentences);
// ["Д-р", "Смит работает в Acme Inc", "Он начинает в 9 утра."]
Текст неправильно разделяется на "Д-р" и "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. Он стал частью базовой платформы веба в апреле 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 не поддерживается");
}
Для сред без поддержки необходимо предоставить резервное решение. Простое резервное решение использует базовое разбиение с помощью регулярных выражений, хотя это теряет точность сегментации, учитывающей локаль.
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, если он доступен, и переходит на разбиение с помощью регулярных выражений в старых средах. Резервное решение теряет такие функции, как обработка сокращений и определение границ, зависящих от языка, но обеспечивает базовую функциональность.