大家好,我是 ConardLi。
假设我们现在有这样一个需求,把一段话拆分成有意义的句子:
你好,我是 ConardLi。我来了!你是谁?你在哪?
你可能会第一时间想到,用 split
按所有可能断句的标点符号分割就好了,比如下面的代码:
var txt = '你好,我是 ConardLi。我来了!你是谁?你在哪?'
txt.split(/[。?!]/); // ['你好,我是 ConardLi', '我来了', '你是谁', '你在哪', '']
看起来结果还不错,但是可以断句的中文标点符号只有这三个吗?显然不是,如果我们想要处理更复杂的文本,需要持续完善这个正则,另外这样分割还有一个最大的问题是标点符号会在分割后的结果中丢失。
如果我们想要按词语进行分割,而不是语句呢?
如果我们想要分割的文本是英语、阿拉伯语呢...
// 中文
const cn = '你好,我是 ConardLi。我来了!你是谁?你在哪?';
// 英文
const en = 'Hello, I'm ConardLi. I'm coming! Who are you? Where are you?';
// 阿拉伯语
const ar = 'مرحبا، أنا كوناردلي. أنا قادم! من أنت؟ أين أنت؟';
这时候 split
可能就会表示无能为力了!
Intl
是 ECMAScript
国际化 API
的一个命名空间,它提供了精确的字符串对比、数字格式化,和日期时间格式化能力。我们今天主要来看一下它提供的字符串分割能力!
Intl.Segmenter
对象专门为语言敏感的文本分割而生,它允许你将一个字符串分割成有意义的片段(字、词、句),下面我们看看它对以上三种语言的分割结果:
中文
const segmenter = new Intl.Segmenter(
'zh', { granularity: 'sentence' }
);
console.log(
Array.from(
segmenter.segment(你好,我是 ConardLi。我来了!你是谁?你在哪?),
s => s.segment
)
);
// ['你好,我是 ConardLi。', '我来了!', '你是谁?', '你在哪?']
英语
const segmenter = new Intl.Segmenter(
'en', { granularity: 'sentence' }
);
console.log(
Array.from(
segmenter.segment(`Hello, I'm ConardLi. I'm coming! Who are you? Where are you?`),
s => s.segment
)
);
// ["Hello, I'm ConardLi. ", "I'm coming! ", 'Who are you? ', 'Where are you?']
阿拉伯语
const segmenter = new Intl.Segmenter(
'ar', { granularity: 'sentence' }
);
console.log(
Array.from(
segmenter.segment(`مرحبا، أنا كوناردلي. أنا قادم! من أنت؟ أين أنت؟`),
s => s.segment
)
);
// ['مرحبا، أنا كوناردلي. ', 'أنا قادم! ', 'من أنت؟ ', 'أين أنت؟']
Intl
的兼容性也还不错,除了 Firefox 目前还没有对它提供支持,其他的各大浏览器均已支持。
下面我们来了解一些 Intl.Segmenter
的细节。
在上面的示例中,我们在 Intl.Segmenter
的构造函数传入了两个参数。
第一个参数是语言地域编码,结构是:'语言编码-地区编码',因为同样的语言在不同的地区也可能会有区别,比如下面的一些常见示例:
zh
:中文zh-CN
:简体中文zh-HK
:中国香港地区的中文(繁体中文)en
:英语en-US
:美式英语en-CB
:英式英语第二个参数是一些更详细的配置参数,我们主要关注 granularity
,它有三个值,分别表示我们要将字符串分割为句、词、还是字:
const segmenter = new Intl.Segmenter(
'zh', { granularity: 'sentence' } // 句
);
// ['你好,我是 ConardLi。', '我来了!', '你是谁?', '你在哪?']
const segmenter = new Intl.Segmenter(
'zh', { granularity: 'word' } // 词
);
// ['你好', ',', '我是', ' ', 'ConardLi', '。', '我来', '了', '!', '你是', '谁', '?', '你在', '哪', '?']
const segmenter = new Intl.Segmenter(
'zh', { granularity: 'grapheme' } // 字
);
// ['你', '好', ',', '我', '是', ' ', 'C', 'o', 'n', 'a', 'r', 'd', 'L', 'i', '。', '我', '来', '了', '!', '你', '是', '谁', '?', '你', '在', '哪', '?']
在上面的例子中可以发现,我们使用 Array.from
对 segment
的返回值进行了处理:
console.log(
Array.from(
segmenter.segment(你好,我是 ConardLi。我来了!你是谁?你在哪?),
s => s.segment
)
);
这是因为它返回的并不是一个数组,而是一个 iterable
对象,如果访问里面的字段,我可以用 for-of
或者使用 Array.from
函数转换为数组。
const segmenter = new Intl.Segmenter('zh', {
granularity: 'sentence'
});
const segments = segmenter.segment('...');
// 结构转为数组
console.log([...segments]);
// Array.from 转为数组
console.log(Array.from(segments));
// for-of 遍历
for (let segment of segments) {
console.log(segment);
}
完整的返回值包括分割后的字符、字符所在位置、输入的完整内容:
另外,在前面的示例中,当我们将文字分割为词时,可以发现标点符号、空格等都被分割出来了:
const segmenter = new Intl.Segmenter(
'zh', { granularity: 'word' } // 词
);
const result = segmenter.segment(`你好,我是 ConardLi。', '我来了!', '你是谁?', '你在哪?`)
Array.from( result , s => s.segment)
// ['你好', ',', '我是', ' ', 'ConardLi', '。', '我来', '了', '!', '你是', '谁', '?', '你在', '哪', '?']
这时返回值里还会包括一个 isWordLike
属性,可以用于过滤是否真的为词语:
Array.from(result).filter(s => s.isWordLike).map(s => s.segment)
// ['你好', '我是', 'ConardLi', '我来', '了', '你是', '谁', '你在', '哪']
一般我们要处理的文本里如果包括了 emojis
,那问题就可能变得麻烦起来了...
我们来看下面的示例:
const str1 = '12345';
const str2 = '12345😵💫';
console.log(str1.length); // 5
console.log(str2.length); // 10
str1.split('')
// ['1', '2', '3', '4', '5']
str2.split('')
// ['1', '2', '3', '4', '5', '\uD83D', '\uDE35', '', '\uD83D', '\uDCAB']
str2.replaceAll(/\D/g, '0')
// '1234500000'
为啥会出现这种现象呢?我们先来回顾一下计算机最基础的概念:字符集与编码:
字符集 (Character Set
) 是字符的集合,定义系统能处理哪些字符;编码( Encoding
)则规定这些字符在计算机内部的表示方式。
Unicode
是一套标准,包含多语言统一的字符集及其相关编码,以及在这个字符集上进行文本处理的相关规则。
在 Unicode
中,每个字符被分配了一个数值 (Code Point
,代码点) 和一个名称。比如字母 A 的名称是 LATIN CAPITAL LETTER A
(大写拉丁字母A)。它对应的数值是 65
,通常写作 U+0041
( 41
是十六进制数,等于 10
进制的 65
)。
字素是文本在书写时最小的单位,可以被理解为单独的“字”。
在 Unicode
标准中,字符(Character
)一般指代码点(Code Point
)。通常,一个字素就是一个字符。但是,也有些字素是由多个字符序列组合而成的。比如字母 é
可以用字母 e (U+0065)
加上重音符 (U+0301
) 组合而成。像重音符这样用于修饰前一个字符的字符,被称为组合字符(Combining Character
)。
比如你看到的这个字符:啊̮̮̮̮̮̮̮̮̮̮̮̑̑̑̑̑̑̑̑̑̑̑ ,就是个组合字符...
现在对于上面的 emojis
出现的字符串分割问题是不是就容易理解了,因为很多 emojis
都是下面这样的组合字符:
🏻 + ✋ = ✋🏻
🏻 - U+1F3FB
✋ - U+270B
✋🏻 - U+270B U+1F3FB
'✋🏻'.split('') // ['✋', '\uD83C', '\uDFFB']
那么下面回归正题了,Intl.Segmenter
的出现也可以解决这样的组合字符的分割问题:
'12345😵💫'.split('')
// ['1', '2', '3', '4', '5', '\uD83D', '\uDE35', '', '\uD83D', '\uDCAB']
const segmenter = new Intl.Segmenter('en', {
granularity: 'grapheme'
});
Array.from(
segmenter.segment('12345😵💫'),
s => s.segment
)
// ['1', '2', '3', '4', '5', '😵💫']
最后在多提一下,最近有一个新的 ECMAScript
提案,为正则表达式新增了一个新的标识符 '/v'
(目前已经到达 stage 3
)就是用来解决正则中的组合字符匹配的问题的:
'12345😵💫'.replaceAll(/\D/g, '0')
// '1234500000'
'12345😵💫'.replaceAll(/\D/gv, '0')
// '123450'
本来是讲字符串分割的,写着写着有点跑偏了,大家凑合着看吧 ...
✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧✧ʕ̢̣̣̣̣̩̩̩̩·͡˔·ོɁ̡̣̣̣̣̩̩̩̩✧
参考链接: