談到中文姓名校驗(yàn),大家是既熟悉又陌生,茫茫然中使用面向百度編程大法找到一個(gè)正則表達(dá)式饭弓,放到項(xiàng)目中双饥。輸入張三
,驗(yàn)證通過(guò)弟断,完美咏花!這就是我要的校驗(yàn)啦(??)。
各位請(qǐng)跟C羅一起來(lái)看這個(gè)看似簡(jiǎn)單的問(wèn)題阀趴,首先迟螺,下面給出一個(gè)目前項(xiàng)目中的場(chǎng)景,非常常見(jiàn)舍咖,基本滿大街的項(xiàng)目可能都會(huì)遇到的矩父。
姓名校驗(yàn)的核心代碼如下
const validChineseName = (name) => {
return /^[\u3400-\u9fa5]$/g.test(name)
}
問(wèn)題的轉(zhuǎn)折點(diǎn)往往從但是
開(kāi)始的,上面簡(jiǎn)潔的實(shí)現(xiàn)貌似并不完美排霉,有一天窍株,來(lái)了一位名字為??(xian)
的用戶,驗(yàn)證無(wú)法通過(guò)(??)攻柠。咱們的故事由此開(kāi)始講起球订,整個(gè)故事中涉及到2個(gè)關(guān)鍵的知識(shí)點(diǎn):
- Unicode及其編碼算法的一些知識(shí)
- JS正則中對(duì)于字符相關(guān)的規(guī)則判斷編寫(xiě)
先學(xué)習(xí)以下幾個(gè)基本概念
Coded Character Set: 編碼字符集,給字符表里的抽象字符編上一個(gè)數(shù)字瑰钮,這些數(shù)字對(duì)跟字符集中的字符一一映射冒滩。Unicode字符集是一種編碼字符集。
Character encoding form:浪谴,字符編碼表开睡,將
編碼字符集
中的字符對(duì)應(yīng)的碼點(diǎn)轉(zhuǎn)換成一定長(zhǎng)度的二進(jìn)制序列,便于計(jì)算機(jī)處理苟耻,此二進(jìn)制序列與碼點(diǎn)的映射關(guān)系稱為字符編碼表篇恒。我們經(jīng)常提到的utf8、utf16都是指字符編碼表的不同算法凶杖。
Code point:胁艰,碼點(diǎn),一個(gè)字符集一般 可以用一張或多張由多個(gè)行和多個(gè)列所構(gòu)成的二位表來(lái)表示智蝠。二維表中的行和列的交叉點(diǎn)腾么,稱之為碼點(diǎn),碼點(diǎn)擁有一個(gè)唯一的編號(hào)杈湾,稱之為碼點(diǎn)值或碼點(diǎn)編號(hào)解虱。
它們之間的關(guān)系可用下圖來(lái)籠統(tǒng)地表達(dá)
經(jīng)常會(huì)有同學(xué)跟我討論的時(shí)候把
utf8
、utf16
跟unicode
混為一談毛秘,結(jié)合上文的幾個(gè)概念和示意圖饭寺,不難發(fā)現(xiàn)阻课,所謂的utf8
只是基于unicode
字符集及其碼點(diǎn)的概念,提供一個(gè)碼點(diǎn)尋址的算法艰匙,將其轉(zhuǎn)換為計(jì)算機(jī)理解的二進(jìn)制串限煞,劃一下重點(diǎn)。
Unicode對(duì)于互聯(lián)網(wǎng)的巨大意義不言而喻员凝,堪稱信息互聯(lián)的基石署驻。Unicode流行起來(lái)之前,很多非英文字符國(guó)家會(huì)使用自己的一套玩法健霹。比如GB2312旺上,如果你沒(méi)有安裝相應(yīng)的解碼器,對(duì)不起糖埋,只能欣賞藝術(shù)感極強(qiáng)的亂碼符號(hào)宣吱。
回到上面說(shuō)到的生僻字校驗(yàn)的問(wèn)題,任何一家尊重用戶的企業(yè)瞳别,對(duì)于自己忠實(shí)的客戶都不能將之拒之門(mén)外征候,哪怕這樣的用戶在巨大的群體中零星的存在。
初始時(shí)祟敛,考慮到以下2個(gè)問(wèn)題
- 用戶姓名的生僻字很難枚舉疤坝,不確定邊界
- 生僻字和emoji表情均是使用高、低代理區(qū)的方式表示
基于以上問(wèn)題馆铁,考慮使用用戶客訴后收集生僻字構(gòu)建平臺(tái)自有的特殊字符集
的方案跑揉。對(duì)于發(fā)生客訴后,強(qiáng)烈要求系統(tǒng)解決校驗(yàn)問(wèn)題的客戶埠巨,我們認(rèn)為是忠誠(chéng)度或者信任度較高的用戶历谍,構(gòu)建此方案是利于我們留存這些用戶,雖然體驗(yàn)不是那么完美乖订,但至少能讓此類(lèi)用戶有一個(gè)途徑進(jìn)一步觸達(dá)產(chǎn)品的其他層面扮饶。
此方案的核心代碼如下
const validChineseName = (n) => {
const excludedChars = ["??","??"];
const excludedCharsStr = excludedChars.join('');
const reg = new RegExp(`^([\u3400-\u9fa5${excludedCharsStr}]){2,15}$`, 'g');
return reg.test(n);
};
使用mocha
做一下單元測(cè)試具练,驗(yàn)證一下校驗(yàn)方法
// 功能示例代碼
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
};
// 單元測(cè)試示例代碼
const expect = require('chai').expect;
const mocha = require('mocha');
const validator = require('./validator');
describe('中文姓名校驗(yàn)', function() {
it('羅 應(yīng)該是 false', function() {
expect(validator.validChineseName('羅')).to.be.equal(false);
});
});
describe('中文姓名校驗(yàn)', function() {
it('?? 應(yīng)該是 false', function() {
expect(validator.validChineseName('??')).to.be.equal(false);
});
});
describe('中文姓名校驗(yàn)', function() {
it('羅?? 應(yīng)該是 true', function() {
expect(validator.validChineseName('羅??')).to.be.equal(true);
});
});
describe('中文姓名校驗(yàn)', function() {
it('羅超 應(yīng)該是 true', function() {
expect(validator.validChineseName('超')).to.be.equal(true);
});
});
第一輪單元測(cè)試的結(jié)果截圖如下
第二個(gè)用例未通過(guò)單元測(cè)試乍构,從上面的代碼看出來(lái),第二個(gè)只有一個(gè)生僻字??
扛点,理論上我們預(yù)期它的校驗(yàn)結(jié)果應(yīng)該是false
哥遮,但實(shí)際這一個(gè)生僻字校驗(yàn)的結(jié)果居然是true
,something went wrong陵究。
字符的正則校驗(yàn)眠饮,本質(zhì)上可以理解為使用unicode
來(lái)做匹配,因此铜邮,通過(guò)線上unicode
與漢字的轉(zhuǎn)算工具仪召,查看??
對(duì)應(yīng)的unicode
為\ud855\udd84
寨蹋。問(wèn)題看來(lái)是出在高低代理對(duì)上。
為了驗(yàn)證我們的猜測(cè)扔茅,可以使用如下的正則來(lái)類(lèi)比已旧,本質(zhì)上是一致的。
/[ab]{2,10}/g.test('ab')
// true
也就是說(shuō)召娜,在正則匹配的時(shí)候底層會(huì)把字符轉(zhuǎn)換為unicode的utf-16的編碼运褪,然后進(jìn)行匹配
。
定位到問(wèn)題后玖瘸,只需要把連續(xù)的生僻字的高低代理隊(duì)結(jié)合起來(lái)再動(dòng)態(tài)構(gòu)造正則表達(dá)式秸讹,JavaScript
中字符串提供了一個(gè)方法charCodeAt
,對(duì)于我們處理這個(gè)問(wèn)題是一個(gè)很重要的函數(shù)雅倒。
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().
通過(guò)調(diào)用charCodeAt
可以獲取對(duì)應(yīng)utf16
編碼璃诀,其中原則如下
- 可以用1個(gè)編碼單元表示的時(shí)候直接用1個(gè)編碼單元來(lái)表示字符的碼點(diǎn)
- 如果1個(gè)編碼單元無(wú)法表示的時(shí)候,使用2個(gè)編碼單元蔑匣,構(gòu)成高低代理位的形式文虏,通過(guò)一定的算法來(lái)表示字符的碼點(diǎn)
可以用代碼和相應(yīng)的結(jié)果來(lái)直觀感受
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
經(jīng)過(guò)萬(wàn)般折騰后,調(diào)整后的校驗(yàn)示例代碼如下
// 獲取給定字符的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;
}
// 校驗(yàn)
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);
};
運(yùn)行單元測(cè)試殖演,用例結(jié)果如下圖所示
到此氧秘,一個(gè)前端兼容生僻字的方案有了初步的實(shí)現(xiàn),方案不完美趴久,還有以下問(wèn)題需要同步考慮
- 需要確認(rèn)后端校驗(yàn)規(guī)則同步修改
- 需要確認(rèn)數(shù)據(jù)庫(kù)(mysql)相應(yīng)的字段是否為
utf8mb4
編碼方式丸相,mysql的utf8最多只支持3個(gè)字節(jié),這個(gè)坑不多說(shuō)彼棍,http://www.techug.com/post/in-mysql-never-use-utf8-use-utf8mb4.html參考這篇文章 - 其他下游業(yè)務(wù)需要對(duì)此字符支持
歡迎留言討論 (by 前端cluo
)