Сегментация текста

Работа с текстом на естественных языках требует учета того, что «символ» в привычном смысле не всегда соответствует минимальной единице отображения или смыслового деления. В Unicode один видимый символ может состоять из нескольких кодовых точек: базового символа и комбинируемых диакритических знаков, эмодзи могут образовываться последовательностями, а границы слов и предложений зависят от языка и контекста.

В JavaScript для корректной сегментации текста используется объект Intl.Segmenter, входящий в состав Internationalization API. Он предоставляет стандартизированный механизм разбиения текста на:

  • графемные кластеры (grapheme clusters)
  • слова (words)
  • предложения (sentences)

Основное преимущество подхода — соответствие стандарту Unicode Text Segmentation (UAX #29), что обеспечивает корректную обработку сложных языковых конструкций.


Архитектура сегментации в Intl API

Объект сегментации создается через конструктор:

const segmenter = new Intl.Segmenter(locale, options);

Параметры конструктора

locale Определяет языковую локаль, влияющую на правила разбиения. Например:

  • "en" — английский
  • "ru" — русский
  • "ja" — японский

Если передано "und" или значение не указано, используется нейтральная локаль по умолчанию.

options — ключевой объект конфигурации:

  • granularity — уровень сегментации:

    • "grapheme"
    • "word"
    • "sentence"

Графемная сегментация

Графемная сегментация разделяет текст на минимальные визуальные единицы, которые пользователь воспринимает как один символ.

Пример создания сегментера

const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });

Поведение на сложных символах

const text = "á??";

const segments = segmenter.segment(text);

for (const { segment } of segments) {
  console.log(segment);
}

Результат будет учитывать:

  • как единый графемный кластер (буква + диакритика)
  • эмодзи-флаги как единые единицы

Особенности графемного разбиения

  • не разделяет комбинируемые символы
  • корректно обрабатывает emoji sequence
  • учитывает правила Unicode Extended Grapheme Clusters

Это критично для UI-компонентов, где индексация по символам может ломать отображение текста.


Словарная сегментация

Сегментация на слова учитывает языковые правила разделения слов и знаков препинания.

const segmenter = new Intl.Segmenter("ru", { granularity: "word" });

const text = "JavaScript — мощный язык.";
const segments = segmenter.segment(text);

Структура результата

Каждый сегмент представляет объект:

{
  segment: "JavaScript",
  index: 0,
  isWordLike: true
}

Поле isWordLike

  • true — сегмент является словом или числовым токеном
  • false — пробелы, знаки пунктуации, символы-разделители

Пример обработки:

for (const part of segments) {
  if (part.isWordLike) {
    console.log(part.segment);
  }
}

Локализационные различия

Разные языки имеют разные правила:

  • В немецком составные слова не разделяются
  • В китайском и японском отсутствуют пробелы между словами
  • В английском апострофы влияют на границы токенов

Сегментация предложений

Сегментация на предложения используется для анализа текста, NLP-задач и разбиения длинных текстов.

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });

const text = "Hello world. How are you doing today? Great!";
const segments = segmenter.segment(text);

Результат работы

Каждый элемент соответствует целому предложению:

  • “Hello world.”
  • “How are you doing today?”
  • “Great!”

Особенности обработки

  • учитываются сокращения (например, “Dr.”, “e.g.”)
  • учитываются кавычки и многоточия
  • применяется языковая модель сегментации

Итерация по сегментам

Результат segmenter.segment() является итерируемым объектом:

const segmenter = new Intl.Segmenter("ru", { granularity: "word" });
const text = "Привет, мир!";

for (const { segment, index } of segmenter.segment(text)) {
  console.log(index, segment);
}

Индексация

Поле index указывает позицию начала сегмента в исходной строке в UTF-16 единицах.

Это важно при:

  • подсветке текста
  • построении редакторов
  • реализации поиска

Практическое применение сегментации

Подсветка слов в тексте

function highlightWords(text) {
  const segmenter = new Intl.Segmenter("ru", { granularity: "word" });

  return [...segmenter.segment(text)]
    .map(part => part.isWordLike ? `[${part.segment}]` : part.segment)
    .join("");
}

Ограничение длины текста по графемам

Обычное slice ломает emoji и комбинированные символы. Корректный вариант:

function truncateByGraphemes(text, max) {
  const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });

  const segments = [...segmenter.segment(text)];
  return segments.slice(0, max).map(s => s.segment).join("");
}

Разбиение текста для NLP

function splitSentences(text, locale = "en") {
  const segmenter = new Intl.Segmenter(locale, { granularity: "sentence" });

  return [...segmenter.segment(text)].map(s => s.segment);
}

Внутренние особенности реализации

Unicode Text Segmentation (UAX #29)

Механизм основан на стандарте Unicode, который определяет:

  • границы слов
  • границы предложений
  • графемные кластеры

Отсутствие зависимости от регулярных выражений

В отличие от самодельных решений на RegExp:

  • корректно работает с любыми языками
  • учитывает сложные emoji sequences
  • поддерживает расширяемые правила Unicode

Производительность

  • реализуется на уровне движка JavaScript
  • оптимизирован для больших текстов
  • не требует ручного кэширования правил

Сравнение с альтернативными подходами

Регулярные выражения

text.split(/\s+/)

Ограничения:

  • ломается на языках без пробелов
  • не учитывает Unicode-графемы
  • не различает punctuation и слова

Ручная обработка символов

[...text]

Проблема:

  • разделяет surrogate pairs
  • ломает emoji
  • игнорирует комбинируемые символы

Intl.Segmenter

  • учитывает локаль
  • работает по стандарту Unicode
  • возвращает структурированные сегменты

Поддержка браузеров и окружений

Поддержка зависит от версии движка:

  • современные версии V8 (Chrome, Node.js)
  • SpiderMonkey (Firefox)
  • JavaScriptCore (Safari)

В старых окружениях возможна необходимость полифиллов, однако точная эмуляция UAX #29 требует значительных ресурсов и часто реализуется частично.


Типичные сценарии использования

Редакторы текста

  • корректное перемещение курсора
  • выделение текста по словам
  • подсчет символов без ошибок Unicode

Поисковые системы

  • разбиение запроса на токены
  • нормализация слов
  • построение индексов

UI-компоненты

  • обрезка текста с сохранением emoji
  • отображение счетчиков символов
  • адаптивная типографика

NLP-пайплайны

  • сегментация предложений
  • токенизация слов
  • предварительная обработка текста

Поведение с различными языками

Китайский и японский

Отсутствие пробелов требует:

  • морфологического анализа
  • словарных правил сегментации

Арабский

  • сложные формы слов
  • влияние диакритики
  • контекстные формы букв

Европейские языки

  • более предсказуемая структура
  • разделение по пробелам и пунктуации

Ограничения API

Несмотря на мощность, существуют ограничения:

  • не выполняет лемматизацию
  • не определяет части речи
  • не анализирует семантику
  • не заменяет полноценные NLP-библиотеки

Итоговая модель поведения сегментера

Поведение можно обобщить следующим образом:

  • вход: строка UTF-16
  • конфигурация: локаль + гранулярность
  • выход: последовательность структурированных сегментов
  • основа: стандарты Unicode Text Segmentation

Механизм обеспечивает единообразную обработку текста в разных языках и сценариях, устраняя необходимость ручной реализации токенизации и учета сложных Unicode-особенностей.