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

Объект Intl.Segmenter предназначен для разбиения текста на логические сегменты с учётом языковых правил. В отличие от обычных методов работы со строками (split, substring, регулярные выражения), сегментация учитывает особенности письменности, пробелов, пунктуации и Unicode-символов.

API особенно важен для:

  • корректной обработки текста на разных языках;
  • работы с эмодзи и составными символами;
  • анализа слов и предложений;
  • подсчёта слов;
  • создания редакторов текста;
  • реализации поиска и выделения фрагментов.

Поддерживаются три уровня сегментации:

  • grapheme — отдельные визуальные символы;
  • word — слова;
  • sentence — предложения.

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

Синтаксис:

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

Параметры

Параметр Описание
locales Язык или массив языков
options Настройки сегментации

Главная настройка:

{
  granularity: "grapheme" | "word" | "sentence"
}

Пример:

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

Метод segment()

После создания сегментатора вызывается метод:

segmenter.segment(text)

Он возвращает специальный итерируемый объект Segments.

Пример:

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

const segments = segmenter.segment("Привет мир");

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

Использование for...of

Наиболее распространённый способ перебора сегментов:

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

const text = "JavaScript очень удобен.";

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

Результат:

{
  segment: 'JavaScript',
  index: 0,
  input: 'JavaScript очень удобен.',
  isWordLike: true
}

{
  segment: ' ',
  index: 10,
  input: 'JavaScript очень удобен.',
  isWordLike: false
}

Структура объекта сегмента

Каждый элемент содержит несколько свойств.

segment

Текущий фрагмент текста.

item.segment

Пример:

console.log(item.segment);

index

Позиция сегмента в исходной строке.

console.log(item.index);

Пример:

const text = "Привет мир";

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

Результат:

Привет 0
 7
мир 8

input

Исходная строка.

console.log(item.input);

Полезно при работе с несколькими источниками текста.


isWordLike

Свойство доступно только при granularity: "word".

Оно показывает, является ли сегмент словом.

console.log(item.isWordLike);

Фильтрация слов

При сегментации по словам пробелы и знаки пунктуации тоже становятся сегментами.

Пример:

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

const text = "Привет, мир!";

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

Результат:

Привет true
, false
  false
мир true
! false

Получение только слов:

const words = [];

for (const item of segmenter.segment(text)) {
  if (item.isWordLike) {
    words.push(item.segment);
  }
}

console.log(words);

Результат:

["Привет", "мир"]

Итерирование по графемам

Что такое графема

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

В Unicode один символ может состоять из нескольких кодовых точек.

Пример:

const text = "??";

Этот символ состоит из:

  • базового эмодзи;
  • модификатора оттенка кожи.

Обычная работа со строками может разбить его неправильно.


Сегментация по графемам

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

const text = "A??Б";

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

Результат:

A
??
Б

Сравнение с обычным перебором строки

Обычный перебор:

const text = "??";

console.log(text.length);

Результат:

4

Перебор по индексам:

for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}

Даёт некорректное разбиение.

Использование Intl.Segmenter решает эту проблему.


Итерирование по предложениям

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

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

const text = `
JavaScript очень популярен.
Intl API упрощает локализацию.
Segmenter работает с Unicode.
`;

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

Результат:

JavaScript очень популярен.

Intl API упрощает локализацию.

Segmenter работает с Unicode.

Особенности сегментации предложений

Алгоритм учитывает:

  • точки;
  • восклицательные знаки;
  • вопросительные знаки;
  • сокращения;
  • правила конкретного языка.

Пример:

const text = "г. Москва находится в России.";

Разбиение будет корректным, несмотря на сокращение г..


Преобразование сегментов в массив

Использование Array.from

Объект Segments итерируемый, поэтому его можно преобразовать в массив.

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

const segments = Array.from(
  segmenter.segment("Привет мир")
);

console.log(segments);

Извлечение только текста сегментов

const parts = Array.from(
  segmenter.segment("Привет мир"),
  item => item.segment
);

console.log(parts);

Результат:

["Привет", " ", "мир"]

Использование оператора расширения

const result = [
  ...segmenter.segment("JavaScript")
];

console.log(result);

Работа с итератором вручную

Метод next()

Можно получить итератор напрямую.

const iterator =
  segmenter.segment("Привет")[Symbol.iterator]();

console.log(iterator.next());
console.log(iterator.next());

Результат:

{
  value: {
    segment: 'Привет',
    index: 0,
    input: 'Привет',
    isWordLike: true
  },
  done: false
}

Использование break и continue

Прерывание перебора

for (const item of segmenter.segment(text)) {
  if (item.segment === ".") {
    break;
  }

  console.log(item.segment);
}

Пропуск сегментов

for (const item of segmenter.segment(text)) {
  if (!item.isWordLike) {
    continue;
  }

  console.log(item.segment);
}

Поиск слов в тексте

Проверка существования слова

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

  for (const item of segmenter.segment(text)) {
    if (
      item.isWordLike &&
      item.segment === target
    ) {
      return true;
    }
  }

  return false;
}

console.log(
  hasWord("JavaScript прекрасен", "прекрасен")
);

Подсчёт количества слов

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

  let count = 0;

  for (const item of segmenter.segment(text)) {
    if (item.isWordLike) {
      count++;
    }
  }

  return count;
}

console.log(
  countWords("JavaScript очень удобен")
);

Результат:

3

Получение координат слов

Свойство index позволяет находить позиции слов.

const text = "Привет мир";

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

Результат:

Привет 0
мир 8

Извлечение первого предложения

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

const iterator =
  segmenter.segment(text)[Symbol.iterator]();

const first = iterator.next().value;

console.log(first.segment);

Сегментация текста без пробелов

Некоторые языки не используют пробелы между словами.

Например:

  • китайский;
  • японский;
  • тайский.

Intl.Segmenter умеет корректно выделять слова.

Пример для японского языка:

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

const text = "私は学生です";

for (const item of segmenter.segment(text)) {
  if (item.isWordLike) {
    console.log(item.segment);
  }
}

Возможный результат:

私
は
学生
です

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

Повторное использование сегментатора

Создание объекта Intl.Segmenter может быть относительно затратным.

Лучше создавать его один раз:

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

function parse(text) {
  return Array.from(segmenter.segment(text));
}

Избегание лишних преобразований

Менее эффективно:

const result = [
  ...segmenter.segment(text)
];

Более эффективно:

for (const item of segmenter.segment(text)) {
  // обработка
}

Без промежуточного массива уменьшается расход памяти.


Совместимость

Intl.Segmenter поддерживается в:

  • современных версиях Chrome;
  • Firefox;
  • Safari;
  • Edge;
  • Node.js.

Проверка поддержки:

if (Intl.Segmenter) {
  console.log("Поддерживается");
}

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

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

  const tokens = [];

  for (const item of segmenter.segment(text)) {
    if (item.isWordLike) {
      tokens.push({
        word: item.segment,
        position: item.index
      });
    }
  }

  return tokens;
}

console.log(
  tokenize("JavaScript очень мощный язык")
);

Результат:

[
  { word: 'JavaScript', position: 0 },
  { word: 'очень', position: 11 },
  { word: 'мощный', position: 17 },
  { word: 'язык', position: 25 }
]