Работа со строками в JavaScript долгое время ограничивалась
посимвольным перебором, регулярными выражениями и методами
split(), substring() или match().
Однако текст в современном мире значительно сложнее простого набора
ASCII-символов:
Для решения этих задач в Intl API появился объект
Intl.Segmenter, предназначенный для интеллектуального
разбиения текста на сегменты.
Одной из ключевых возможностей сегментатора является получение индексов границ сегментов.
Intl.SegmenterСоздание сегментатора:
const segmenter = new Intl.Segmenter("ru", {
granularity: "word"
});
Параметры:
| Параметр | Назначение |
|---|---|
locale |
локаль |
granularity |
уровень сегментации |
Возможные значения granularity:
| Значение | Описание |
|---|---|
"grapheme" |
отдельные графемы |
"word" |
слова |
"sentence" |
предложения |
segment()Основной метод:
segmenter.segment(text)
Возвращает специальный объект-итератор сегментов.
Пример:
const segmenter = new Intl.Segmenter("ru", {
granularity: "word"
});
const segments = segmenter.segment("Привет мир");
Каждый сегмент содержит несколько важных свойств:
{
segment: "Привет",
index: 0,
input: "Привет мир",
isWordLike: true
}
Описание свойств:
| Свойство | Назначение |
|---|---|
segment |
текст сегмента |
index |
индекс начала сегмента |
input |
исходная строка |
isWordLike |
является ли сегмент словом |
Главный интерес представляет именно свойство index.
Свойство index содержит позицию начала сегмента в
исходной строке.
Пример:
const segmenter = new Intl.Segmenter("ru", {
granularity: "word"
});
const text = "Привет мир";
for (const item of segmenter.segment(text)) {
console.log(item.segment, item.index);
}
Результат:
Привет 0
7
мир 8
Пробел также является сегментом.
Часто требуется получить полный список границ текста.
Пример:
const segmenter = new Intl.Segmenter("ru", {
granularity: "word"
});
const text = "JavaScript Intl API";
const indexes = [];
for (const item of segmenter.segment(text)) {
indexes.push(item.index);
}
console.log(indexes);
Результат:
[0, 10, 11, 15, 16]
Графема — визуальный символ, который может состоять из нескольких Unicode-кодов.
Пример с эмодзи:
const text = "???";
console.log(text.length);
Результат:
6
Хотя визуально отображаются только два символа.
Использование Intl.Segmenter:
const segmenter = new Intl.Segmenter("ru", {
granularity: "grapheme"
});
for (const item of segmenter.segment(text)) {
console.log(item.segment, item.index);
}
Результат:
?? 0
? 4
Индексы показывают реальные границы Unicode-последовательностей.
Стандартные строковые методы работают с UTF-16 кодовыми единицами.
Пример:
const text = "??";
console.log(text[0]);
console.log(text[1]);
Результат:
�
�
Причина:
Intl.Segmenter корректно определяет пользовательские
символы.
Пример получения массива позиций:
const segmenter = new Intl.Segmenter("ru", {
granularity: "grapheme"
});
const text = "A??B?";
const positions = [];
for (const item of segmenter.segment(text)) {
positions.push(item.index);
}
console.log(positions);
Результат:
[0, 1, 5, 6]
Сегментация слов особенно важна для языков без пробелов.
const segmenter = new Intl.Segmenter("ru", {
granularity: "word"
});
const text = "Привет, мир!";
for (const item of segmenter.segment(text)) {
console.log(item);
}
Результат:
{ segment: 'Привет', index: 0, isWordLike: true }
{ segment: ',', index: 6, isWordLike: false }
{ segment: ' ', index: 7, isWordLike: false }
{ segment: 'мир', index: 8, isWordLike: true }
{ segment: '!', index: 11, isWordLike: false }
Обычно нужны только текстовые слова.
Пример:
const words = [];
for (const item of segmenter.segment(text)) {
if (item.isWordLike) {
words.push({
word: item.segment,
index: item.index
});
}
}
console.log(words);
Результат:
[
{ word: 'Привет', index: 0 },
{ word: 'мир', index: 8 }
]
Сегментация предложений:
const segmenter = new Intl.Segmenter("ru", {
granularity: "sentence"
});
const text = "Первое предложение. Второе предложение!";
for (const item of segmenter.segment(text)) {
console.log(item.segment, item.index);
}
Результат:
Первое предложение. 0
Второе предложение! 21
Индексы границ позволяют создавать навигацию внутри текста.
Пример поиска начала слова:
const segmenter = new Intl.Segmenter("ru", {
granularity: "word"
});
const text = "JavaScript Segmenter API";
for (const item of segmenter.segment(text)) {
console.log(
`Слово "${item.segment}" начинается с ${item.index}`
);
}
Intl.Segmenter особенно полезен в текстовых
редакторах.
Пример получения диапазона слова:
const segmenter = new Intl.Segmenter("ru", {
granularity: "word"
});
const text = "Пример текста";
for (const item of segmenter.segment(text)) {
const start = item.index;
const end = start + item.segment.length;
console.log(start, end);
}
Результат:
0 7
7 8
8 14
containing()Объект сегментов поддерживает метод:
segments.containing(index)
Он позволяет найти сегмент по позиции.
Пример:
const segmenter = new Intl.Segmenter("ru", {
granularity: "word"
});
const text = "JavaScript API";
const segments = segmenter.segment(text);
console.log(segments.containing(2));
Результат:
{
segment: 'JavaScript',
index: 0,
isWordLike: true
}
Позиция внутри слова:
console.log(segments.containing(5));
Результат:
{
segment: 'JavaScript',
index: 0
}
Позиция внутри пробела:
console.log(segments.containing(10));
Результат:
{
segment: ' ',
index: 10
}
containing()function getWordAt(text, cursor) {
const segmenter = new Intl.Segmenter("ru", {
granularity: "word"
});
const segments = segmenter.segment(text);
return segments.containing(cursor);
}
Использование:
const result = getWordAt(
"JavaScript Segmenter API",
12
);
console.log(result.segment);
Результат:
Segmenter
Intl.Segmenter хранит только начало сегмента.
Конечный индекс вычисляется вручную:
const end = item.index + item.segment.length;
Пример:
for (const item of segmenter.segment(text)) {
const start = item.index;
const end = start + item.segment.length;
console.log({
segment: item.segment,
start,
end
});
}
Пример:
const ranges = [];
for (const item of segmenter.segment(text)) {
ranges.push({
start: item.index,
end: item.index + item.segment.length
});
}
console.log(ranges);
Для китайского и японского языков обычное разделение по пробелам невозможно.
const segmenter = new Intl.Segmenter("zh", {
granularity: "word"
});
const text = "我喜欢JavaScript";
for (const item of segmenter.segment(text)) {
console.log(item.segment, item.index);
}
Пример результата:
我 0
喜欢 1
JavaScript 3
const segmenter = new Intl.Segmenter("ja", {
granularity: "word"
});
const text = "私は猫です";
for (const item of segmenter.segment(text)) {
console.log(item.segment, item.index);
}
split()split()const words = text.split(" ");
Недостатки:
Intl.Segmenter| Возможность | split() |
Intl.Segmenter |
|---|---|---|
| Unicode-графемы | Нет | Да |
| Эмодзи | Плохо | Да |
| Китайский язык | Нет | Да |
| Японский язык | Нет | Да |
| Индексы сегментов | Ограниченно | Да |
| Поиск по позиции | Нет | Да |
Создание сегментатора — сравнительно дорогая операция.
Нежелательно:
for (const text of texts) {
const segmenter = new Intl.Segmenter("ru", {
granularity: "word"
});
segmenter.segment(text);
}
Лучше:
const segmenter = new Intl.Segmenter("ru", {
granularity: "word"
});
for (const text of texts) {
segmenter.segment(text);
}
Практика для крупных приложений:
const cache = new Map();
function getSegmenter(locale, granularity) {
const key = `${locale}_${granularity}`;
if (!cache.has(key)) {
cache.set(
key,
new Intl.Segmenter(locale, {
granularity
})
);
}
return cache.get(key);
}
Intl.Segmenter поддерживается:
Проверка поддержки:
if (Intl.Segmenter) {
console.log("Поддерживается");
}
const text = "Привет ?? мир!";
const segmenter = new Intl.Segmenter("ru", {
granularity: "grapheme"
});
for (const item of segmenter.segment(text)) {
console.log({
symbol: item.segment,
index: item.index
});
}
Результат:
{ symbol: 'П', index: 0 }
{ symbol: 'р', index: 1 }
{ symbol: 'и', index: 2 }
{ symbol: 'в', index: 3 }
{ symbol: 'е', index: 4 }
{ symbol: 'т', index: 5 }
{ symbol: ' ', index: 6 }
{ symbol: '??', index: 7 }
{ symbol: ' ', index: 11 }
{ symbol: 'м', index: 12 }
{ symbol: 'и', index: 13 }
{ symbol: 'р', index: 14 }
{ symbol: '!', index: 15 }