Как разбить текст на слова в 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" пробелом.

Языки с непрерывным письмом, такие как китайский, японский и тайский, не используют пробелы между словами. Границы слов определяются на основе семантических и морфологических правил, но эти границы не отмечены визуально в тексте. Читатель китайского языка распознает, где заканчивается одно слово и начинается другое, благодаря знанию языка, а не визуальным разделителям.

Некоторые языки используют смешанные правила. Японский язык сочетает кандзи, хирагану и катакану, и границы слов возникают на переходах между типами символов или на основе грамматической структуры.

Стандарт 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" });
  // Используйте segmenter
} else {
  // Альтернатива для старых браузеров
}

Для производственных приложений, нацеленных на старые браузеры, предоставьте альтернативную реализацию. Простая альтернатива использует 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);
  }

  // Альтернатива: работает только для языков с разделением пробелами
  return text.split(/\s+/).filter(word => word.length > 0);
}

Это гарантирует, что ваш код будет работать в старых браузерах, хотя с уменьшенной функциональностью для языков без разделения пробелами.

Распространённые ошибки, которых следует избегать

Не разделяйте текст по пробелам или шаблонам регулярных выражений для многоязычного текста. Эти подходы работают только для ограниченного числа языков и не подходят для китайского, японского, тайского и других языков без пробелов.

Не забывайте фильтровать по isWordLike при извлечении слов. Без этого фильтра в результатах будут пробелы, знаки препинания и другие сегменты, не являющиеся словами.

Не используйте неправильную локаль при сегментации текста. Локаль определяет правила границ слов. Использование английской локали для китайского текста приведёт к некорректным результатам.

Не предполагайте, что все языки определяют слова одинаково. Границы слов зависят от системы письма и лингвистических традиций. Используйте сегментацию с учётом локали, чтобы учитывать эти различия.

Не считайте слова с помощью split(" ").length для международного текста. Это работает только для языков с разделением пробелами и даёт неверные результаты для других языков.

Когда использовать сегментацию слов

Используйте сегментацию слов, когда вам нужно:

  • Считать слова в пользовательском контенте на нескольких языках
  • Реализовать функции поиска и выделения, работающие с любой системой письма
  • Создавать инструменты анализа текста для обработки международного текста
  • Разрабатывать функции навигации или редактирования текста на основе слов в текстовых редакторах
  • Извлекать ключевые слова или термины из многоязычных документов
  • Проверять ограничения на количество слов в формах, принимающих текст на любом языке

Не используйте сегментацию слов, если вам нужно только подсчитать количество символов. Для операций на уровне символов используйте сегментацию графем.

Не используйте сегментацию слов для разделения предложений. Для этого используйте сегментацию на уровне предложений.

Как сегментация слов вписывается в интернационализацию

API Intl.Segmenter является частью API интернационализации ECMAScript. Другие API в этом семействе обрабатывают различные аспекты интернационализации:

  • Intl.DateTimeFormat: форматирование дат и времени в соответствии с локалью
  • Intl.NumberFormat: форматирование чисел, валют и единиц измерения в соответствии с локалью
  • Intl.Collator: сортировка и сравнение строк в соответствии с локалью
  • Intl.PluralRules: определение форм множественного числа для чисел на разных языках

Вместе эти API предоставляют инструменты, необходимые для создания приложений, которые корректно работают для пользователей по всему миру. Используйте Intl.Segmenter с гранулярностью слов, когда нужно определить границы слов, а другие API Intl — для форматирования и сравнения.