并不简单的中文姓名校验

谈到中文姓名校验,大家是既熟悉又陌生,茫茫然中使用面向百度编程大法找到一个正则表达式,放到项目中。输入张三,验证通过,完美!这就是我要的校验啦(😀)。
各位请跟C罗一起来看这个看似简单的问题,首先,下面给出一个目前项目中的场景,非常常见,基本满大街的项目可能都会遇到的。

姓名校验的表单

姓名校验的核心代码如下

const validChineseName = (name) => {
    return /^[\u3400-\u9fa5]$/g.test(name)
}

问题的转折点往往从但是开始的,上面简洁的实现貌似并不完美,有一天,来了一位名字为𥖄(xian)的用户,验证无法通过(😂)。咱们的故事由此开始讲起,整个故事中涉及到2个关键的知识点:

  • Unicode及其编码算法的一些知识
  • JS正则中对于字符相关的规则判断编写

先学习以下几个基本概念

Coded Character Set: 编码字符集,给字符表里的抽象字符编上一个数字,这些数字对跟字符集中的字符一一映射。Unicode字符集是一种编码字符集。

Character encoding form:,字符编码表,将编码字符集中的字符对应的码点转换成一定长度的二进制序列,便于计算机处理,此二进制序列与码点的映射关系称为字符编码表。我们经常提到的utf8、utf16都是指字符编码表的不同算法。

Code point:,码点,一个字符集一般 可以用一张或多张由多个行和多个列所构成的二位表来表示。二维表中的行和列的交叉点,称之为码点,码点拥有一个唯一的编号,称之为码点值或码点编号。

它们之间的关系可用下图来笼统地表达

字符编码表与编码字符集的关系

经常会有同学跟我讨论的时候把utf8utf16unicode混为一谈,结合上文的几个概念和示意图,不难发现,所谓的utf8只是基于unicode字符集及其码点的概念,提供一个码点寻址的算法,将其转换为计算机理解的二进制串,划一下重点。

Unicode对于互联网的巨大意义不言而喻,堪称信息互联的基石。Unicode流行起来之前,很多非英文字符国家会使用自己的一套玩法。比如GB2312,如果你没有安装相应的解码器,对不起,只能欣赏艺术感极强的乱码符号。

回到上面说到的生僻字校验的问题,任何一家尊重用户的企业,对于自己忠实的客户都不能将之拒之门外,哪怕这样的用户在巨大的群体中零星的存在。

初始时,考虑到以下2个问题

  • 用户姓名的生僻字很难枚举,不确定边界
  • 生僻字和emoji表情均是使用高、低代理区的方式表示
    基于以上问题,考虑使用用户客诉后收集生僻字构建平台自有的特殊字符集的方案。对于发生客诉后,强烈要求系统解决校验问题的客户,我们认为是忠诚度或者信任度较高的用户,构建此方案是利于我们留存这些用户,虽然体验不是那么完美,但至少能让此类用户有一个途径进一步触达产品的其他层面。
在配置平台拉取生僻字符集

此方案的核心代码如下

const validChineseName = (n) => {
    const excludedChars = ["𥖄","𤰉"];
    const excludedCharsStr = excludedChars.join('');
    const reg = new RegExp(`^([\u3400-\u9fa5${excludedCharsStr}]){2,15}$`, 'g');
    return reg.test(n);
};

使用mocha做一下单元测试,验证一下校验方法

// 功能示例代码
const validChineseName = (n) => {
    const excludedChars = ["𥖄","𤰉"];
    const excludedCharsStr = excludedChars.join('');
    const reg = new RegExp(`^([\u3400-\u9fa5${excludedCharsStr}]){2,15}$`, 'g');
    return reg.test(n);
};

module.exports = {
    validChineseName
};
// 单元测试示例代码
const expect = require('chai').expect;
const mocha = require('mocha');
const validator = require('./validator');

describe('中文姓名校验', function() {
  it('罗 应该是 false', function() {
    expect(validator.validChineseName('罗')).to.be.equal(false);
  });
});

describe('中文姓名校验', function() {
  it('𥖄 应该是 false', function() {
    expect(validator.validChineseName('𥖄')).to.be.equal(false);
  });
});

describe('中文姓名校验', function() {
  it('罗𥖄 应该是 true', function() {
    expect(validator.validChineseName('罗𥖄')).to.be.equal(true);
  });
});

describe('中文姓名校验', function() {
  it('罗超 应该是 true', function() {
    expect(validator.validChineseName('超')).to.be.equal(true);
  });
});

第一轮单元测试的结果截图如下


单元测试结果截图

第二个用例未通过单元测试,从上面的代码看出来,第二个只有一个生僻字𥖄,理论上我们预期它的校验结果应该是false,但实际这一个生僻字校验的结果居然是true,something went wrong。

字符的正则校验,本质上可以理解为使用unicode来做匹配,因此,通过线上unicode与汉字的转算工具,查看𥖄对应的unicode\ud855\udd84。问题看来是出在高低代理对上。

为了验证我们的猜测,可以使用如下的正则来类比,本质上是一致的。

/[ab]{2,10}/g.test('ab')
// true

也就是说,在正则匹配的时候底层会把字符转换为unicode的utf-16的编码,然后进行匹配

定位到问题后,只需要把连续的生僻字的高低代理队结合起来再动态构造正则表达式,JavaScript中字符串提供了一个方法charCodeAt,对于我们处理这个问题是一个很重要的函数。

The charCodeAt() method returns an integer between 0 and 65535 representing the UTF-16 code unit at the given index.
The UTF-16 code unit matches the Unicode code point for code points that can be represented in a single UTF-16 code unit. If the Unicode code point cannot be represented in a single UTF-16 code unit (because its value is greater than 0x10000) then the code unit returned will be the first part of a surrogate pair for the code point. If you want the entire code point value, use codePointAt().
通过调用charCodeAt可以获取对应utf16编码,其中原则如下

  • 可以用1个编码单元表示的时候直接用1个编码单元来表示字符的码点
  • 如果1个编码单元无法表示的时候,使用2个编码单元,构成高低代理位的形式,通过一定的算法来表示字符的码点
    可以用代码和相应的结果来直观感受
const aCharCode = 'a'.charCodeAt(0).toString(16).padStart(4, '0')
// aCharCode = \u0061
const hCode = '𥖄'.charCodeAt(0).toString(16).padStart(4, '0')
const lCode = '𥖄'.charCodeAt(1).toString(16).padStart(4, '0')
const code = `\\u${hCode}\\u${lCode}`
// code = \ud855\udd84

经过万般折腾后,调整后的校验示例代码如下

// 获取给定字符的utf-16编码
const getCharCode = (c) => {
    const h = c.charCodeAt(0);
    const l = c.charCodeAt(1);
    let hStr = '', lStr = '';
    if (h) {
        let hCode = h.toString(16).padStart(4, '0');
        hStr = `\\u${hCode}`;
    }
    if (l) {
        let lCode = l.toString(16).padStart(4, '0');
        lStr = `\\u${lCode}`;
    }
    const charCode = `${hStr}${lStr}`;
    return charCode;
}
// 校验
const validChineseName = (n) => {
    const excludedChars = ["𥖄","𤰉"];
    const charCodeArr = excludedChars.map(c => {
        return getCharCode(c)
    });
    const excludedCharsStr = charCodeArr.join(')|(');
    const reg = new RegExp(`^([\u3400-\u9fa5]|(${excludedCharsStr})){2,15}`, 'g');
    return reg.test(n);
};

运行单元测试,用例结果如下图所示


单元测试结果截图

到此,一个前端兼容生僻字的方案有了初步的实现,方案不完美,还有以下问题需要同步考虑

欢迎留言讨论 (by 前端cluo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容

  • 前言 最先接触编程的知识是在大学里面,大学里面学了一些基础的知识,c语言,java语言,单片机的汇编语言等;大学毕...
    oceanfive阅读 3,080评论 0 7
  • 字符是用户可以读写的最小单位。计算机所能支持的字符组成的集合,就叫做字符集。字符集通常以二维表的形式存在。二维表的...
    刘惜有阅读 8,116评论 2 14
  • 转载须注明出处:简书@Orca_J35 以下内容直接翻译自 Unicode 术语表:Glossary of Uni...
    import_hello阅读 3,159评论 0 0
  • 坚持二字说着简单,做着确实很难。 以前总说要为小宝每天的成长做些记录,以便长大后讲给她听。但每天总...
    淘气珂珂阅读 239评论 0 0
  • 时常感到心情抑郁,我想这是孤独导致的吧。也或许是身体中某种激素的缺乏,让我激动不起来。 最近老是想到死亡,感觉我和...
    桃白白_0796阅读 123评论 0 0