Как разбить текст на слова в JavaScript?
Используйте Intl.Segmenter, чтобы выделять слова из текста на любом языке, даже если в языке нет пробелов между словами.
Введение
Когда нужно выделить слова из текста, обычно используют разделение по пробелам с помощью split(" "). Для английского это работает, но полностью проваливается для языков, где слова не разделяются пробелами. В китайском, японском, тайском и других языках текст пишется сплошняком, без разделителей, хотя пользователи всё равно воспринимают отдельные слова.
API Intl.Segmenter решает эту проблему. Он определяет границы слов по стандартам 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 для поиска позиций между словесными и несловесными символами. Это работает для английского.
const text = "Hello world!";
const words = text.match(/\b\w+\b/g);
console.log(words);
// ["Hello", "world"]
Этот шаблон не работает для языков без пробелов, потому что движок регулярных выражений не распознаёт границы слов в таких письменностях, как китайская, японская или тайская.
const text = "你好世界";
const words = text.match(/\b\w+\b/g);
console.log(words);
// ["你好世界"]
Регулярка воспринимает всю строку как одно слово, потому что не понимает границы слов в китайском.
Даже для английского регулярные выражения могут давать неверные результаты из-за пунктуации, сокращений или спецсимволов. Регулярки не предназначены для лингвистического разбиения слов во всех письменностях.
Что такое границы слов в разных языках
Граница слова — это место в тексте, где заканчивается одно слово и начинается другое. В разных письменностях используются разные правила для границ слов.
В языках с пробелами, таких как английский, испанский, французский и немецкий, пробелы отделяют слова друг от друга. Например, слово «hello» отделено от «world» пробелом.
В языках scriptio continua, таких как китайский, японский и тайский, пробелов между словами нет. Границы слов определяются по смыслу и морфологии, но визуально в тексте они не отмечены. Носитель языка понимает, где заканчивается одно слово и начинается другое, благодаря знанию языка, а не визуальным разделителям.
В некоторых языках используются смешанные системы. Например, в японском сочетаются кандзи, хирагана и катакана, а границы слов определяются переходами между типами символов или грамматической структурой.
Стандарт Unicode определяет правила границ слов в UAX 29. Эти правила описывают, как находить границы слов для всех письменностей. При этом учитываются свойства символов, типы письменности и языковые особенности, чтобы определить, где начинаются и заканчиваются слова.
Использование Intl.Segmenter для разбиения текста на слова
Конструктор Intl.Segmenter создаёт объект-сегментатор, который разбивает текст по правилам Unicode. Вы указываете локаль и уровень детализации.
const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world!";
const segments = segmenter.segment(text);
Первый аргумент — это идентификатор локали. Второй аргумент — объект с опциями, где 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);
// ["你好", "世界"]
Английская локаль не распознаёт границы слов в китайском и считает всю строку одним словом. Китайская локаль применяет свои правила и правильно определяет два слова.
Всегда используйте подходящую локаль для языка текста, который сегментируете.
Создание переиспользуемых сегментаторов для повышения производительности
Создание сегментатора не требует больших затрат, но вы можете использовать один сегментатор для нескольких строк, чтобы повысить производительность.
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(). Сегментатор кэширует данные локали, поэтому повторное использование экземпляров позволяет избежать повторной инициализации.
Практический пример: создание анализатора частоты слов
Объедините сегментацию слов с подсчётом, чтобы анализировать частоту слов в тексте.
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 }
Та же логика работает с китайским текстом без изменений.
Проверка поддержки браузерами
API Intl.Segmenter получил статус Baseline в апреле 2024 года. Он работает в актуальных версиях 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
}
Для production-приложений, рассчитанных на старые браузеры, предусмотрите запасной вариант. Простой fallback использует 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 при извлечении слов. Без этого фильтра в результат попадут пробелы, знаки препинания и другие несловесные сегменты.
Не используйте неправильную локаль при сегментации текста. Локаль определяет, какие правила разделения слов применяются. Если использовать английскую локаль для китайского текста, результат будет некорректным.
Не думайте, что во всех языках слова определяются одинаково. Границы слов зависят от письменности и языковых норм. Используйте сегментацию с учётом локали, чтобы правильно обрабатывать эти различия.
Не считайте слова с помощью split(" ").length для интернационального текста. Это работает только для языков с разделением слов пробелами и даёт неверные результаты для других языков.
Когда использовать сегментацию слов
Используйте сегментацию слов, если вам нужно:
- Считать слова в пользовательском контенте на разных языках
- Реализовать поиск и подсветку, которые работают с любой письменностью
- Создавать инструменты анализа текста для интернационального текста
- Делать навигацию или редактирование по словам в текстовых редакторах
- Извлекать ключевые слова или термины из многоязычных документов
- Проверять лимиты по количеству слов в формах для любого языка
Не используйте сегментацию слов, если нужно только посчитать символы. Для операций на уровне символов используйте сегментацию по графемам.
Не используйте сегментацию слов для разбиения на предложения. Для этого используйте сегментацию по предложениям.
Как сегментация слов связана с интернационализацией
API Intl.Segmenter входит в ECMAScript Internationalization API. Другие API из этого семейства отвечают за разные аспекты интернационализации:
Intl.DateTimeFormat: форматирование дат и времени по локалиIntl.NumberFormat: форматирование чисел, валют и единиц по локалиIntl.Collator: сортировка и сравнение строк по локалиIntl.PluralRules: определение форм множественного числа для чисел на разных языках
Вместе эти API предоставляют все необходимые инструменты для создания приложений, которые корректно работают для пользователей по всему миру. Используйте Intl.Segmenter с разбивкой на слова, когда нужно определить границы слов, а остальные API Intl используйте для форматирования и сравнения.