吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3524|回复: 4
收起左侧

[Java 原创] 【JavaScript文本截取】该死的Emoji表情

[复制链接]
15774211127 发表于 2019-11-21 14:11
本帖最后由 15774211127 于 2019-11-21 14:55 编辑

【JavaScript文本截取】该死的Emoji表情

emmm...公司微信小程序项目要求限制输入框的可输入文本长度!但是要求按字节数限制,而不是按字符数限制(可难为死我了😫)

一、什么是Emoji表情😆

绘文字(日语:絵文字/えもじ emoji)是日本在无线通信中所使用的视觉情感符号,绘指图画,文字指的则是字符,可用来代表多种表情,如笑脸表示笑、蛋糕表示食物等。在中国大陆,emoji通常叫做“小黄脸”,或者直称emoji

二、文字截取函数substr()、substring()

1.substr(start,length)

substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符。

2.substring(start,stop)

substring() 方法用于提取字符串中介于两个指定下标之间的字符。

三、使用文字截取函数截取文本(常规方式)

function test1(str, len) {
        return str.substr(0, len);
}
function test2(str, len) {
        return str.substring(0, len);
}
let str = "123你好";
console.log(test1(str, 4)); // 123你
console.log(test2(str, 4)); // 123你

从上面的代码上来看是完全没有问题的,一但加上emoji表情就炸了

str = "123👩‍🦱你好";
console.log(test1(str, 4)); // 123�
console.log(test2(str, 4)); // 123�
console.log(test1(str, 6)); // 123👩‍
console.log(test1(str, 8)); // 123👩‍🦱

再加个特殊中文试试“𠮷”

str = "123𠮷你好";
console.log(test1(str, 4)); // 123�
console.log(test2(str, 4)); // 123�
console.log(str.length); // 7

是的,就是这么奇葩。
由于javascript使用但是usc-2的编码,对于2个字节字符的Unicode,js是解析起来66的。但是会有以下问题  

  1. 不常用字符,比如"𠮷"或者某些emoji表情(或者组合表情👩+🦱=👩‍🦱)占用3个或更多字节。  
  2. javascript会将一个多字节字符识别成多个字符😱(看上面代码的最后一句str.length等于7)。  
  3. 在某些场景下如果字符截取不完整会直接抛出异常😳😮😯😦😧😨😰😱😇

四、解决思路

秉持面向百度编程的宗旨。啊呸...

  1. 将文本转换成Unicode十六进制
  2. 手动分割字符
  3. 完成

遇到的问题1

String.charCodeAt()和String.fromCharCode()也只支持处理两个字节的字符😏

str = "𠮷";
console.log(str.charCodeAt(0)); // 55362
console.log(String.fromCharCode(str.charCodeAt(0))); // �

解决办法

使用ES6的新特性String.fromCodePoint()和String.codePointAt()代替String.charCodeAt()和String.fromCharCode()

str = "𠮷";
console.log(str.codePointAt(0)); // 134071
console.log(String.fromCodePoint(str.codePointAt(0))); // 𠮷

遇到的问题2

对于组合字符如何区分

str = "𠮷👩‍🦱";
function strToUnicode(str) {
var result = "";
for (var i = 0; i < str.length; i++) {
var code = str.codePointAt(i).toString(16);
result += `\\u\{${code}\}`;
if (code.length > 4) {
i++;
}
}
return result;
}
console.log(strToUnicode(str)); // \u{20bb7}\u{1f469}\u{200d}\u{1f9b1}

从上面的代码中可以看到输入两个字符但是输出4个字符的Unicode编码,因为组合表情本质上就是多个表情
秉持面向百度编程的宗旨。啊呸...

解决办法  

由于👩+🦱=👩‍🦱,通过对比下面这个图可以发现 \u{1f469}+\u{1f9b1}=\u{1f469}\u{200d}\u{1f9b1}
然后 秉持面向百度编程的宗旨。啊呸...得出结论,组合表情会通过\u{200d}进行连接,
也就是说如果遇到\u{200d}表示\u{200d}的前一个字符和\u{200d}和\u{200d}的后一个字符是一个符号组合(特别注意连接符的前一个和后一个字符一定是大于两个字节的字符)

解决思路:

  1. 将文本转换成Unicode十六进制(可以存为数组,也可以存储为“\u{十六进制}”,后面这种方法js的表示转义表示方法)
  2. 循环数组,如果十六进制长度是1-2表示是一个字节,是3-4则表示是2个字节大于4表示3个字节(一般不会有5个字节,组合字符除外)
  3. 如果在第2步循环中遇到200d则表示下一次循环字符和上一次循环字符是组合字符(将他们存储到一起)
  4. 在第2步循环时统计长度,将长度和得到的十六进制存储到数组
  5. 循环得到的数组,如果长度相加小于给定的长度则将16进制转成文本追加到结果变量,否则跳出循环

五、完整代码

公用代码(因为我为了简化代码,最终写了几个版本)

        /* 通过文字二进制得到文字字节数 */
        function getByteByBinary(binaryCode) {
                        /**
                         * 二进制 Binary system,es6表示时以0b开头
                         * 八进制 Octal number system,es5表示时以0开头,es6表示时以0o开头
                         * 十进制 Decimal system
                         * 十六进制 Hexadecimal,es5、es6表示时以0x开头
                         */
                        var byteLengthDatas = [0, 1, 2, 3, 4];
                        var len = byteLengthDatas[Math.ceil(binaryCode.length / 8)];
                        return len;
                }
        /* 通过文字十六进制得到文字字节数 */
                function getByteByHex(hexCode) {
                        return getByteByBinary(parseInt(hexCode, 16).toString(2));
                }

第一版写法 (不推荐,这里是我最开始的思路,也就是上面说的思路)  

        function strToUnicodeArray(str) {
                        var result = [];
                        var flag = false;
                        var temp = "";
                        var len = 0;
                        for (var i = 0; i < str.length; i++) {
                                var code = str.codePointAt(i).toString(16); // 转为十六进制
                                if (code.length > 4) { // 判断是否是两个字节以上
                                        i++;
                                        if ((i + 1) < str.length) { // 判断下一个字符是否是连接符200d
                                                flag = str.codePointAt(i + 1).toString(16) == "200d";
                                        }
                                }
                                if (flag) { // 如果下一个字符是连接符200d,将当前字符保存到临时变量
                                        temp = temp + `\\u{` + code + `}`;
                                        len += getByteByHex(code);
                                        if (i == str.length - 1) { // 这里是为了防止最后一个字符是组合字符时出现结果未存储的问题
                                                result.push({
                                                        value: temp,
                                                        length: len
                                                });
                                        }
                                } else { // 如果下一个字符不是连接符200d
                                        if (temp != "") { // 当temp不等于空时,代表当前字符是组合字符的一部分,所以需要将当前字符加入临时变量,又因为下一个字符不是连接符200d,所以表示组合字符完毕,将结果存储,清空临时变量
                                                temp = temp + `\\u{` + code + `}`;
                                                len += getByteByHex(code);
                                                result.push({
                                                        value: temp,
                                                        length: len
                                                });
                                                temp = "";
                                                len = 0;
                                        } else { // 将结果存储,当前字符又不是组合字符的一部分所以直接存储
                                                result.push({
                                                        value: `\\u{` + code + `}`,
                                                        length: getByteByHex(code)
                                                });
                                        }
                                }
                        }
                        return result;
                }

                function substringByByte(str, maxLength) {
                        var datas = strToUnicodeArray(str);
                        console.log(datas)
                        var len = 0;
                        var result = "";
                        for (var i in datas) {
                                len += datas[i].length;
                                if (len <= maxLength) {
                                        if (datas[i].length >= 5) {
                                                var value = datas[i].value.replace(/{/g, "").replace(/}/g, "").split("\\u").slice(1);
                                                for (var j in value) {
                                                        value[j] = parseInt(value[j], 16)
                                                }
                                                result += String.fromCodePoint(...value);
                                        } else {
                                                var value = datas[i].value.replace(/\\u/g, "").replace(/{/g, "").replace(/}/g, "");
                                                result += String.fromCodePoint(parseInt(value, 16));
                                        }
                                } else {
                                        break;
                                }
                        }
                        return result;
                }

第二版写法 (推荐,这里是我第一版的优化)
这一版我使用js的转义表示方法存储数据,然后直接通过正则表达式分割每个字符,这样就不用在循环里写很多处理组合字符的逻辑了

        /* 字符串转Unicode十六进制 */
        function strToUnicode(str) {
                        var result = "";
                        for (var i = 0; i < str.length; i++) {
                                var code = str.codePointAt(i).toString(16);
                                result += `\\u\{${code}\}`;
                                if (code.length > 4) {
                                        i++; // 由于str.length也只能处理两个字节的文字,所以这里需要判断如果codePointAt得到多字符就得跳过一次循环
                                }
                        }
                        return result;
                }
        /* 截取指定字符数长度的文本 如果后一个字符截取后超出指定的长度,将不会截取该字符 */
                function substringByByte2(str, maxLength) {
                        var data = strToUnicode(str);
                        var reg = new RegExp(/\\u\{[A-z0-9]+\}(\\u\{200d\}{1}\\u\{[A-z0-9]+\})*/, 'g');// 使用正则表达式分割每个完整字符
                        var datas = reg[Symbol.match](data);
                        var result = "";
                        var length = 0;
                        for (var i in datas) {
                                var value = datas[i].split("\\u").slice(1);
                                // var len = 0;
                                value = value.map(str => {
                                        var value = str.replace(/\\u/g, "").replace(/{/g, "").replace(/}/g, "");
                                        length += getByteByHex(value);
                                        return parseInt(value, 16);
                                });
                                if (length <= maxLength) {
                                        result += String.fromCodePoint(...value);
                                } else break;
                        }
                        return result;
                }

第三版写法 (推荐)
由于前两种写法需要现将文本转为十六进制,然后再循环截取
在遇到原始文本较大而截取长度较小时,性能上就会特别差(如: 10000长度的字符串截取5个长度)
所以第三版我直接一边转十六进制一边判断长度并截取,这样超出需要截取长度的文本就不会被处理

        function substringByByte3(str, maxLength) {
                        var result = "";
                        var flag = false;
                        var len = 0;
                        var length = 0;
                        var length2 = 0;
                        for (var i = 0; i < str.length; i++) {
                                var code = str.codePointAt(i).toString(16);
                                if (code.length > 4) {
                                        i++;
                                        if ((i + 1) < str.length) {
                                                flag = str.codePointAt(i + 1).toString(16) == "200d";
                                        }
                                }
                                if (flag) {
                                        len += getByteByHex(code);
                                        if (i == str.length - 1) {
                                                length += len;
                                                if (length <= maxLength) {
                                                        result += str.substr(length2, i - length2 + 1);
                                                } else {
                                                        break
                                                }
                                        }
                                } else {
                                        if (len != 0) {
                                                length += len;
                                                length += getByteByHex(code);
                                                if (length <= maxLength) {
                                                        result += str.substr(length2, i - length2 + 1);
                                                        length2 = i + 1;
                                                } else {
                                                        break
                                                }
                                                len = 0;
                                                continue;
                                        }
                                        length += getByteByHex(code);
                                        if (length <= maxLength) {
                                                if (code.length <= 4) {
                                                        result += str[i]
                                                } else {
                                                        result += str[i - 1] + str[i]
                                                }
                                                length2 = i + 1;
                                        } else {
                                                break
                                        }
                                }
                        }
                        return result;
                }

六、运行测试

let str = "123👩‍🦱你好";
console.log(substringByByte(str, 4)); // 123
console.log(substringByByte2(str, 6)); // 123
console.log(substringByByte2(str, 10)); // 123👩‍🦱
console.log(substringByByte2(str, 11)); // 123👩‍🦱
console.log(substringByByte3(str, 13)); // 123👩‍🦱你

程序员最讨厌的四件事:写注释、写文档、别人不写注释、别人不写文档(所以我的代码也没有注释😜).
当然还有很多失败的版本我就不放出来了😭

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

jack88996 发表于 2019-11-21 14:16
{:17_1065:这个图标包还不错哟
XiaoBaizzZ 发表于 2019-11-21 14:17
mokson 发表于 2019-11-21 16:21
 楼主| 15774211127 发表于 2019-11-21 17:24
mokson 发表于 2019-11-21 16:21
楼主,你的JS学到位了。

木有木有,就是面向百度编程
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-5-2 22:42

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表