统计字数方案

62 阅读2分钟

npm方案

word-counter 采用 apple page的规范,统计对应字数,支持cjk和emoji

实现原理

对传入的string调用normalize进行处理

为什么进行normalize处理,normalize在mdn中的解释是这样的

The normalize() method of String values returns the Unicode Normalization Form of this string. 字符串在 JavaScript 中可能包含不同的 Unicode 规范形式,例如 NFC(NFC 将字符组合成最简洁的形式,例如将一个字母和一个音调符号合并为一个字符) 和 NFD(NFD 将组合字符分解为基础字符和附加符号)。这些不同形式的字符在视觉上看起来相同,但底层编码可能不同,比如

const name1 = '\u0041\u006d\u00e9\u006c\u0069\u0065';
const name2 = '\u0041\u006d\u0065\u0301\u006c\u0069\u0065';

console.log(`${name1}, ${name2}`);
// Expected output: "Amélie, Amélie"
console.log(name1 === name2);
// Expected output: false
console.log(name1.length === name2.length);
// Expected output: false

const name1NFC = name1.normalize('NFC');
const name2NFC = name2.normalize('NFC');

console.log(`${name1NFC}, ${name2NFC}`);
// Expected output: "Amélie, Amélie"
console.log(name1NFC === name2NFC);
// Expected output: true
console.log(name1NFC.length === name2NFC.length);
// Expected output: true

通过match方法匹配对应的正则,计算出现的字数

const wordPattern = new RegExp(
  `${PatternString.emoji}|${PatternString.cjk}|${PatternString.number}|${PatternString.word}`,
  "gu"
);

const cjkPattern =
  "\\p{Script=Han}|\\p{Script=Kana}|\\p{Script=Hira}|\\p{Script=Hangul}";
const PatternString = {
  emoji: emojiPattern,
  cjk: cjkPattern,
  word: `((?!${cjkPattern})[\\p{Alphabetic}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])+`,
  number: "(?:[\\p{Decimal_Number}](?:\\.?\\p{Decimal_Number})+)",
};

正则中的u

正则表达式的 "u" 标志表示启用 Unicode 模式。也就是说正则表达式可以正确处理 Unicode 字符,包括那些在基本多语言平面之外的字符(即 U+10000 及以上的字符)。使用 "u" 标志后,正则表达式的元字符(如 \w,\d 等)会根据 Unicode 定义进行匹配,从而能够匹配更广泛的字符集。

/^.$/.test('👻')
// -> false

/^.$/u.test('👻')
// -> true

那么js本身采用什么编码方式进行匹配呢?是UTF-16

UTF-16 最小的码元是两个字节,即使第一个字节可能都是 0 也要占位,这是固定的。不定是对于基本平面(BMP)的字符只需要两个字节,表示范围 U+0000 ~ U+FFFF,而对于补充平面则需要占用四个字节 U+010000~U+10FFFF。 UTF-16 的编码逻辑

对于给定一个 Unicode 码点 cp(CodePoint 也就是这个字符在 Unicode 中的唯一编号):

  1. 如果码点小于等于 U+FFFF(也就是基本平面的所有字符),不需要处理,直接使用。
  2. 否则,将拆分为两个部分 ((cp – 65536) / 1024) + 0xD800((cp – 65536) % 1024) + 0xDC00 来存储。
const str = "𠮷"; // 注意"𠮷"并非中文,是一个超过基本平面的字符
const str1 = "吉";

function strLength (str:string) {
  const utf16Codes = [];
  for (let i = 0; i < str.length; i++) {
    utf16Codes.push(str.codePointAt(i));
  }
  return utf16Codes.length
}

console.log(strLength(str)) // 2
console.log(strLength(str1)) // 1

js中,string的length为什么会不准确

'𠮷'.length // 2

Where ECMAScript operations interpret String values, each element is interpreted as a single UTF-16 code unit. '𠮷'字符实际上占用了两个 UTF-16 的码元,也就是两个元素,所以它的 length 属性就是 2

如何正确识别字符串的长度?

参考 async-validator 的做法

const spRegexp = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;

if (str) {
  val = value.replace(spRegexp, '_').length;
}

/[\uD800-\uDBFF][\uDC00-\uDFFF]/ 这个正则表达的是,用于匹配 UTF-16 编码中代理对的组合,也就是那些表示超出基本多语言平面(BMP)字符的代码点 代理对(Surrogate Pair)是 UTF-16 编码中的一种机制,用来表示 Unicode 中超过 U+FFFF 范围的字符。这些字符位于所谓的补充平面(Supplementary Planes)

代理对由两个部分组成:

  1. 高位代理(High Surrogate):编码范围是 \uD800\uDBFF,它表示字符的高位部分。
  2. 低位代理(Low Surrogate):编码范围是 \uDC00\uDFFF,它表示字符的低位部分。 这两个 16 位单元组合在一起,形成了一个完整的字符。

将码点范围在补充平面的字符全部替换为下划线,这样长度判断就和实际显示的一致了

es6对unicode的支持

  1. for of 和 for
var str = '𠮷'
for (var i = 0; i < str.length; i ++) {
  console.log(str[i])
}
// �
// �
for (const char of str) {
  console.log(char)
}
// 𠮷

字符串会按照 JS 理解的每个“元素”遍历,辅助平面的字符将会被识别成两个“元素”,于是出现“乱码” 2. 展开语法

'𠮷'.slice().length // 2
'𠮷'.split('').length // 2
[...'𠮷'].length // 1
  1. codePointAt
'𠮷'.codePointAt(0)
// 134071
'𠮷'.charCodeAt(0)
// 55362

charCodeAt 来获取 Code Point,对于 BMP 平面的字符是可以适用的,但是如果字符是辅助平面字符 charCodeAt 返回结果就只会是编码后第一个码元对于的数字,而codePointAt 可以将字符正确识别,并返回正确的码点

cjkPattern解析

什么是cjk?

CJK(Chinese-Japanese-Korean)文字是对中文、日文文字和韩文的统称,这些语言全部含有汉字及其变体,某些会与其他文字混合使用

"\\p{Script=Han}|\\p{Script=Kana}|\\p{Script=Hira}|\\p{Script=Hangul}";

\p{...}:Unicode 属性转义的语法,表示使用 Unicode 属性进行匹配 {Script=..}:表示作用范围

  • Script=Han 代表汉字脚本,也就是中文、日文、韩文中使用的汉字字符
  • Script=Kana 代表片假名脚本,日文中的一种音节文字
  • Script=Hira 代表平假名脚本,日文中的一种音节文字
  • Script=Hangul 代表韩字脚本,韩文使用的字符系

wordPattren解析

 const word = `((?!${cjkPattern})[\\p{Alphabetic}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}])+`
  • \p{Alphabetic} 匹配任何被 Unicode 标记为字母的字符,包括大部分语言的字母字符,而不仅仅是英语的 A-Z。它包括 Unicode 标准中认为是字母的所有字符 例如 拉丁字母(A-Z,a-z),希腊字母(α, β, γ 等),西里尔字母(К, Д, ж 等),汉字中的一些字母符号
  • \p{Decimal_Number} 匹配任何被标记为十进制数字的字符。这不仅包括常见的 0-9 数字,还包括其他语言或文化中的数字字符,如印度数字、泰米尔数字等。
  • \p{Connector_Punctuation} 匹配连接符号字符,连接符号是用于将单词或部分单词连接在一起的符号,最常见的例子是下划线(_)
  • \p{Join_Control}匹配连接控制字符,这些字符用于处理涉及复杂书写系统(如阿拉伯文、波斯文、印地语等)时,确保正确处理这些不可见的连接控制符等书写系统中字符的连接方式,但它们本身是不可见的控制字符

numberPattern解析

cosnt number= "(?:[\\p{Decimal_Number}](?:\\.?\\p{Decimal_Number})+)"

匹配十进制数字,包括支持多语言数字和带有小数点的数字。它能处理不同文化或语言中的数字系统,如阿拉伯数字、印度数字等,并且能够识别小数点分隔的数字形式。

emojiPattern解析

一些 Emoji 在 Unicode 字符集中并没有位置,而是由多个 Unicode 字符组合而成的。

参考

juejin.cn/post/702540…

github.com/curly210102…

juejin.cn/post/701888…