字符编码的前世今生

老板,给我上一窝锟斤拷

想必我们在刚刚接触代码开发时,都会接触到一个叫做ASCII的东西,而初代的Java工程师们,可能在用eclipse的时候,会经常需要手动切换代码编码格式(从UTF-8到GBK),为的就是能够看清别人的代码注释,又或者,你知道手持两把锟斤拷,口中疾呼烫烫烫的由来么

甚至出现了下图的一个场景

在这里插入图片描述

你知道为什么会 ”烫“ 会直击程序员本质么?

今天我们就来给大家讲讲关于字符编码的前世今生。

摩尔斯电码

话说公元前1837年(公元个鬼啊没有公元),为了实现远距离传输信息这个需求(发电报),美国有位科学家发明了一种叫做摩尔斯电码的东西。它是一种时通时断的信号代码,通过不同的排列顺序来表达不同的英文字母、数字和标点符号。

在这里插入图片描述

摩尔斯电码由短的和长的电脉冲(称为点和划)所组成。点和划的时间长度都有规定,以一点为一个基本单位,一划等于三个点的长度。正好对应上电报的"滴"和"答"。正如上图所示,正是因为有人对摩尔斯电码进行了如图中的规定。某一种组合方式代表一定的数字或字母,并且大家都承认,才让这种方式得以广泛使用。

我们经常看到的一些谍战剧,或者一些电影大片中(比如之前韩国很火的那部《寄生虫》),主角&配角急中生智,为了不轻易暴露自己的信息,或者迫于环境无法使用声音进行沟通,就会通过一些媒介向别人传播神秘的摩斯电码。

其中的发报员需要将要表达的信息转换为对应的摩斯密码(这个过程称为字符编码),然后收报员再将收到的电码解释为原本的信息(字符解码),倘若他们没有使用一份完全相同的摩斯电码表,那么就会出现信息传递出错的严重问题。

现在我们知道了上上个世纪是可以通过电报来进行传递信息的。那么到了我们的电脑时代呢?根据冯诺依曼体系:数字计算机的数制采用二进制;计算机应该按照程序顺序执行。

而二进制,就是只有0和1两个数值,类似于摩尔斯电码的滴和答,但是没有那种停顿或者长度的表示,一切只能利用这两个数值去组装。

于是乎,科学家们把一个0或者1占用的空间称为一个比特位。那么为了表示我们的十进制0-9,就用了4个比特位来进行表示。举个例子(实际不是这样好):用0000表示0,0001表示1,0010表示2…1001表示9,中间的也是依次类推,于是乎便有了二进制的说法。

但是我们的计算机当然不会说仅仅只是用来做简单的数字加减,还希望能够存储一些字母,程序符号,控制字符等(合起来共计128个,用0~127表示)的功能。这个时候科学家就需要更多的比特位来表示这些信息。

此时,美国科学家经过它们的初步统计,大概是有128个字符需要存储,那么根据二进制来算,7bit便足够代表以上所有的信息(2的7次方=128)。但是还有一个符号位(奇偶校验位)要放在哪里呢?就放在最高位吧,这样就组成了8个比特位。

于是乎,在上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定,这被称为 ASCII 码,一直沿用至今。在ASCII表出来后,为了统一方便,此时大家就说把 8bit表示出来的值叫做字节(byte) 吧,于是就有了字节这个单位。这个时候便有了1Byte=8bit的定义。

这个地方要提一下,ASCII是由95个可打印字符(0x20-0x7E)和33个控制字符(0x00-0x1F,0x7F)组成共计128个字符组成。

控制字符是什么呢?就是当年用来直接控制一些设备去执行指定命令工作的,比如一旦终端、打印机遇上这些字节被传过来时,就要做一些约定的动作。于是打印机会打印反白的字了(0x1b…),会换行(0x10)了,终端会嘟嘟(0x07)叫了,有色彩(0x1b…)了。

可打印字符就包括了所有的空格、标点符号、数字、大小写字母。

ASCII码

在这里插入图片描述

ASCII 码表被发明之后,美国的科学家认为挺完美的了,毕竟计算机是美国人发明的,他们当时哪里用得着去管世界上其他国家的文字。在整个欧洲的拉丁语系中,没有一种语言的字母会超过128个,所以当时看确实是足够了。后来计算机在全世界的范围内得到了极大发展和普及,可是计算机却只能使用英语,人们不能使用自己的母语,所以各个国家和地区便根据自己的国家语言,制定了不同的标准,这个就是ANSI编码

ANSI编码

​ ANSI编码是一种对ASCII码的拓展:ANSI编码用0x00~0x7f (即十进制下的0到127)表示 原有的ASCII字符,超出127的 0x80~0xFFFF 范围来表示其他语言的其他字符。也就是说,ANSI码仅在前128(0-127)个与ASCII码相同,之后的字符全是某个国家语言的所有字符。值得注意的是,上面的0xFFFF是涉及到第二个字节的空间的了,两个字节最多可以存储的字符数目是2的16次方,即65536个字符,这对于大部分的语言字符来说,绝对够了。

​ 值得注意的是, ANSI 编码之间是互不兼容,比如在日本的Windows系统中,ANSI 编码代表 Shift_JIS 编码,它是无法与中国的GB2312等兼容的。

​ 在我们国家,中国汉字博大精深, 当代的《汉语大字典》(2010年版)就收字60,370个,所以在遵循ANSI编码的情况下,一个字节是完全无法满足我们的汉字要求的,但是两个字节就非常够了,所以中国人就开始了定义自己的字符编码。

GB2312

中国国家标准简体中文字符集

​ 在1980年,中国推出了GB2312,这是16位字符集,即采用双字节编码。收录有6763个简体汉字,682个符号,共7445个字符;

​ 在设计GB2312时,中国也不敢说完全脱离老美的ASCII码表,毕竟系统什么的都还是人家的。所以GB2312设计时在第一个字节的低位,即ASCII码表中0~127的编号得保留下来,具体的中文字符还得从往后开始排。

​ GB2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率,属于中国国家标准,新加坡地区也可使用此种编码。但对于人名、古汉语等方面出现的罕用字和繁体字不能很好的支持

GBK

汉字内码扩展规范

​ 聪明的中国人当然很快就意识到了GB2312的不足之处,于是在1995年,又制定了GBK编码,它是16位字符集,即双字节编码,收录有21003个汉字,883个符号,共21886个字符;其主要就是利用了GB2312中还未使用到的区间,继续增添字符,也就是说,其可以完全兼容以前的GB2312。

GB18030

信息技术 中文编码字符集

到了2000年的时候,为了同时能够支持我们国家各种少数名族的语言,国人又推出了新的编码:GB18030

  • 采用变长多字节编码,每个字可以由1个、2个或4个字节组成。
  • 编码空间庞大,最多可定义161万个字符。
  • 它兼容 GB2312,基本兼容 GBK(只有很少几处不同)
  • 完全支持Unicode,无需动用造字区即可支持中国国内少数民族文字、中日韩和繁体汉字以及emoji等字符。

兼容的代码示例如下:

String origin = "chackca 与你一起进步";
String gbk2gb18030 = new String(origin.getBytes("GBK"), "GB18030");
System.out.println(gbk2gb18030 + ":" + origin.equals(gbk2gb18030));
//输出:chackca 与你一起进步

​ 嗯?上面好像说到了Unicode,那么Unicode是个什么东西呢?

​ 这里我们来铺垫一个背景:

​ 上文我们说了美国制定了Ascii表,用0~127位来表示它们用到的大部分的字符,当时美国制定完这个规则后,首先传入计算机的是欧洲等地区,我们知道欧洲的一些国家语言可不是英文,这个时候,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。由此产生了适用于各个国家的编码方式,也就是上文说的ANSI编码。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0–127表示的符号是一样的,不一样的只是128–255的这一段。

​ 在这种情况下,每个国家都出现了不同的编码,我们常见的电子邮件的乱码,就是因为发信人和收信人使用的编码方式不一样。那么如何解决这种情况呢?如果有一种编码方式,能够支持所有国家的字符,让大家都使用,是不是就不会出现这种情况了?

​ 在这种情况下,Unicode就应运而生了。

Unicode

Unicode(中文:万国码、国际码、统一码、单一码)是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得计算机可以用更为简单的方式来呈现和处理文字。

​ Unicode至今仍在不断增修,每个新版本都加入更多新的字符。目前最新的版本为2020年3月公布的13.0.0。

​ 需要注意的是,Unicode 只是一个符号集/字符集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。这么做是有一定的考虑的。

​ 我们假设,Unicode规定所有字符必须使用4个字节进行存储(因为字符太多,只能用这么多字节才能将所有字符都表示起来),那么原本的Ascii码表的字符,本来可以直接使用1个字节表示的,现在就都得变成使用4个字节,其他字节位置需要补充0,这对于存储来说是极大的浪费。因此,为了解决这个问题,市场上就出现了很多中间格式的字符集,他们被称为通用转换格式,即 UTF(Unicode Transformation Format),而我们常见的有:UTF-7, UTF-7.5, UTF-8,UTF-16, 以及 UTF-32。我们可以说,UTF-8、UTF-16等都是 Unicode 的一种实现方式。

这里我们对常见的三种进行介绍:

UTF-8

  • 使用1~4个字节为每个字符编码

  • 当字符在ASCII中可以被表示时,UTF-8编码方式就用一个字节来表示它。

  • 在UTF-8中汉字用3个字符来表示。

  • 自同步编码,在传输过程中如果有字节序列丢失,并不会造成任何乱码现象,或者存在错误的字节序列也不会影响其他字节的正常读取。例如读取了一个 10xxxxxx 开头的字节,但是找不到首字节,就可以将这个后续字节丢弃,因为它没有意义,但是其他的编码方式,这种情况下就很可能读到一个完全不同或者错误的字符。

UTF-16

  • 使用2~4个字节为每个字符编码

  • 由于UTF-16固定使用两个字节表示一个字符,所以UTF-16不能与ASCII兼容。

  • 在不同的机器中UTF-16存在因存储方式不同(大端法和小端法)导致数据有误,因此存在UTF16-LE和UTF16-BE两种UTF16的变体。

  • 相比较UTF-8,在存储中文方面,UTF16更加节省空间(仅用两个字节存储中文)。

  • UTF-16容错情况比UTF-8好,因为UTF-16大部分情况下稳定使用两个字节编码,如果数据错误不会连代其他数据被读错,而UTF-8是变长编码,可能导致后面的字符全部错误。
    UTF-16广泛应用在各种系统中。

UTF-32

  • 使用4个字节为每个字符编码

​ UTF-32可以说是“真正”的unicode编码,unicode用四个字节表示一个字符的特点在UTF-32中实现了,理论上这样根本不需要复杂的分配字节的方法,只需要每个字符一一对应即可,而且UTF-32的超大容量装得下任何的字符。但是问题也就在这里,一个字符需要四个字节太过于奢侈,因此,UTF-32并不是一个很常用的编码方法。

​ 看到这里,你大概就会明白,为什么我们常见的UTF转换格式是UTF-8了吧,因为其使用变长字节来储存 Unicode字符,对于原本的ASCII字母,继续让其使用第一字节的位置存储,也就是可以兼容Ascii表,而对于重音文字、希腊字母或西里尔字母等则使用2字节来储存,而常用的汉字就要使用3字节,辅助平面字符则使用4字节。通过此种方式,UTF-8基本上就可以表示了所有的文字。而如果我们选择了使用UTF-16,则注定了无法直接兼容ASCII。

​ 或许你会好奇UTF-8是怎么实现变长字符的,你可以点击下面链接进行了解

为什么UTF-8比UTF-16更费空间

或者直接看下面的总结:

1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
2)对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。

Unicode符号范围     |        UTF-8编码方式
(十六进制)        	|             (二进制)
----------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

在实际的解码过程中:
情况 1:读取到一个字节的首位为 0,表示这是一个单字节编码的 ASCII 字符;
情况 2:读取到一个字节的首位为 1,表示这是一个多字节编码的字符,如继续读到 1,则确定这是首字节,在继续读取直到遇到 0 为止,一共读取了几个 1,就表示该字符为几个字节的编码;
情况 3:当读取到一个字节的首位为 1,紧接着读取到一个 0,则该字节是多字节编码的后续字节。

​ 通过上面的介绍,我们知道了UTF-8是Unicode的实现,也就代表了它可以支持几乎所有的字符,那么,既然它这么强,为什么还会出现其他的编码格式呢?也就是说它肯定是存在缺点的。

​ 而其实,它的优点即是它的缺点

​ 对于那些优先纳入UTF-8的字符,它们可以采用1-2个字节便定义了自己的字符,但是对于后来者,由于前面的位置已经被占满了,只能同时使用后面3-4字节的位置,那么它的存储空间就会相对大一些,这就造成了某种“不公平”的现象。

​ 而实际上,对于大部分的网站服务器,它们基本都是服务于一个国家或地区的,比如一个中国的网站,一般来说只会出现简体字和繁体字和一些英文字符,很少会出现日语或者韩文、法语等等。

​ 而我们知道在UTF-8中,对于中文是采用3字节存储的,如果有一种编码用2个字节即存储了中文,那么对我们的中国的应用将是非常有利的,因此,我们迫切需要一种采用双字节来编码中文的编码格式。因此就出现了上文的GB2312、GBK,然后还有GB18030。

​ 等等,GB18030?

​ 这里你可能会问:“我记得上文说GB18030是一种变长的多字节编码啊?为什么也放在这里了?”

​ 实际上,GB18030也是Unicode的一种实现,我们可以理解为它是一种UTF,只是为了支持原有的GBK映射,所以显得比较反常,它使用的是不同于UTF-8或UTF-16生成的字节序列进行编码。

​ 从上面我们可以知道,GB18030是兼容GBK的,且是可变长度的,也就是说使用它的话大部分的中文字符都还是双字节编码的,嗯~还可以接受

​ 这里要注意的是,UTF-8中的中文是使用3字节存储的,而我们上文说道的GB2312、GBK等由于是使用双字节存储,所以他们是无法兼容使用的。因此,假设一个外国人要访问你的GBK网页,则ta需要去下载中文语言包支持。而访问UTF-8编码的网页则不会出现这问题,可以直接访问。

​ 所以其中的区别就是

GBK包含全部中文字符;

UTF-8则包含全世界所有国家需要用到的字符。

​ 因此,经常开发网页前端的人可能会遇到这个问题:网页编写UTF-8和GBK哪个编码好?

这种情况就应该根据个人需要选择了

  1. 如果你主要做中文程序的开发,客户也主要是中国人的话就用GBK吧,因为UTF-8编码的中文使用了三个字节,比GBK的双字节多,所以用GBK可以节省些空间。

  2. 如果是做英文网站开发,那就用UTF-8吧,因为UTF-8中的英文只占一个字节。虽然GBK中的英文也是一个字节的,但是国外客户访问GBK编码的网页有些时候是需要下载语言包的,所以为了用户的方便,还是使用UTF-8吧。

  3. 如果你的网站是中文的,但国外用户也不少,最好也用UTF-8的吧。

Mysql 之 utf8mb4

​ 那么,我们知道了基本的UTF格式,但是你肯定见过在mysql大行其道的utf8mb4,正如文章开头的数据库错乱例子,为什么别人给了你一份数据库数据,说是utf-8格式的,你按照它说的格式创建了,导入数据后却还出现了乱码?

在这里插入图片描述

​ 实际上,MySQL是 从 4.1 版本开始支持 UTF-8,也就是 2003 年,那时mysql还是很不错的,知道业内有这个编码格式,而且使用的人还挺多,便快速地跟上时代发展的步伐,给用户支持了UTF-8的编码格式,那会的UTF-8规定使用一至六个字节为每个字符编码。而Mysql 中的 utf8 定义了支持最长3个字节。

​ 至于为什么人家UTF-8要求1-6个字节,而mysql只支持1-3个字节,业界也都是猜测,大家也可以看下面这篇文章

​ 殊不知,刚刚好在它支持UTF-8的那一年,也就是2003年11月,UTF-8便被RFC 3629重新规范,要求最多支持的字节变成了四个。后来,新的UTF-8格式被越来越多的人接受,使用者也越来越多。

​ World的天,正因为如此,导致mysql在utf-8的定义上被挖了个大坑,你说吧,用户都已经接入了,数据都已经存进来了,这可是万万不能动的,况且,互联网中软件的更新可都是提倡向上兼容的,Mysql总不能把这utf-8的编码格式给直接删了吧

​ 因此MySQL只好妥协了,它在5.5.3(2010年)之后增加了这个utf8mb4的编码,mb4就是most bytes 4的意思,专门用来兼容四字节的unicode。好在utf8mb4是utf8的超集(4字节>3字节),除了将编码改为utf8mb4外不需要做其他转换。当然,如果你为了节省空间,一般情况下使用utf8也就够了。(这里大家也可以想一下,为什么mysql不直接在新版本把原来的utf8改成支持1-4字节的)

​ 嗯?节省空间?不是说了UTF-8是变长的嘛?那么我平时存储3字节的字符,在utf8和utf8mb4应该都是一样的啊,怎么会节省呢?

​ 其实,在mysql上,对于 CHAR 类型数据,utf8mb4 会多消耗一些空间,所以,根据 Mysql 的官方建议,使用 VARCHAR 替代 CHAR。

​ 存储时,CHAR不管实际存储数据的长度,直接按照CHAR规定的长度分配存储空间;而后者会根据实际存储的数据分配最终的存储空间。所以通常情况下,varchar能够节约磁盘空间。

char(n) 无论存储了多少个字符,实际所占存储长度都为n
varchar(n) 占用存储长度仅取决于你实际存放了几个字符
上面的n表示n个字符,无论汉字和英文,Mysql都能存入n个字符,仅是实际字节长度有所区别

正因为char类型的字符数是从定义下来的时候就固定的,所以,MySQL必须事先为它准备好相应的存储空间。

比如为 utf8mb4字符集的char类型的列每一个字符保留四字节的空间,因为其最大长度可能是四字节。
而为 utf8字符集的列的每一个字符则仅需保留三字节的空间,因为其最大长度只可能是三个字节。
例如,MySQL必须为一个使用 utf8mb4 字符集的 char(10)的列保留40字节空间。

​ 好了我们回来,其实,一般情况下,如果你选了utf8来存储你的中文信息也是不会有问题的,但是当你遇到了一些比较新鲜的字符,或者一些不常见的汉字,任何新增的 Unicode 字符,就会发生插库异常,这个时候你就得考虑把数据库编码换成utf8mb4了

在这里插入图片描述

​ 在这里也建议大家,以后在选择数据库编码的时候,如果是要使用utf-8这个编码,则直接选择utf8mb4吧,防止未来出现任何不可控力导致乱码或者插库异常问题。

乱码(锟斤拷)是怎样炼成的

​ 讲了这么多,那么接下来回到我们文章开头提到的乱码问题吧:

在这里插入图片描述

​ 相信你可能曾经也在某乎上搜索过以上相似的内容,虽然结果和我的不太一样,这本书是我小时候的启蒙书,它对我的编程习惯影响至今,使我练就了钢铁般的编码意识,奥斯勒洛夫斯基就是我为数不多的男神…之一。🐶

​ 以至于谈到如何炼成一份乱码,我就手到擒来:

public static void main(String[] args) { 
    String origin = "chackca 与你一起进步";
    System.out.println("GB2312编码,GB2312解码:" + new String(origin.getBytes("GB2312"), "GB2312"));
    System.out.println("GB2312编码,GBK解码:" + new String(origin.getBytes("GB2312"), "GBK"));
    System.out.println("GB2312编码,GB18030解码:" + new String(origin.getBytes("GB2312"), "GB18030"));
    System.out.println("GB2312编码,UTF-8解码:" + new String(origin.getBytes("GB2312"), "UTF-8"));
    System.out.println("GBK编码,UTF-8解码:" + new String(origin.getBytes("GBK"), "UTF-8"));
}

输出结果

GB2312编码,GB2312解码:chackca 与你一起进步
GB2312编码,GBK解码:chackca 与你一起进步
GB2312编码,GB18030解码:chackca 与你一起进步
GB2312编码,UTF-8解码:chackca ����һ�����
GBK编码,UTF-8解码:chackca ����һ�����

​ 如上,一份热腾腾的乱码就出来了

​ 其中,前三行由于我们上文说了GB2312、GBK、GB18030之间的兼容性问题,所以我们不会看到乱码,而后面,由于GB2312编码的字符集和UTF-8解码的字符集不一致,才会导致了乱码的问题

​ 那么锟斤拷是如何产生的呢?

​ 在Unicode和老编码体系的转化过程中,肯定会有一些字,是用Unicode是没法表示的,而这个时候,Unicode官方用了一个占位符来表示这些文字,这个字符就是 � ,他也是Unicode中定义的一个特殊字符,所有无法表示的字符都会通过这个字符来表示。这就是:U+FFFD REPLACEMENT CHARACTER。

在这里插入图片描述

在Unicode官方文档中,它的10进制表示是65533,当我们使用UTF-8编码时,那么他的16进制形式就是

String origin = "�";
System.out.println(bytesToHex(origin.getBytes("UTF-8")));

输出:

0xEF 0xBF 0xBD (三个字节)

这里可能你会有点疑惑,因为65533,用二进制转16进制,转出来的是FFFD,但是UTF-8(16进制)却是:0xEF 0xBF 0xBD,其实这里面正是应用了上文提到的UTF-8为了兼容1字节存储而使用的变换二进制变换手法,如下:

65533  				->  11111111	11111101       			//十进制转二进制
11111111 11111101   ->  11101111	10111111	10111101	//普通二进制转UTF-8二进制存储
					->  ef 			bf 			 bd			//转换为相应的hex(3个byte)

转换过程如上,那么我们继续下去

如果刚刚好出现两个连续的字符都无法显示,那么其就是

0xEF 0xBF 0xBD 0xEF 0xBF 0xBD

这个时候,按照我们中文的编码格式GBK来解码的话,因为GBK中一个汉字占用两个字节,那么就可以归类为:

0xEF 0xBF, 0xBD 0xEF, 0xBF 0xBD   ---->  即: 0xEFBF	0xBDEF	0xBFBD

那么,等GBK展示给用户,就是锟(0xEFBF),斤(0xBDEF),拷(0xBFBD)

String origin = "��";
System.out.println("GBK编码,UTF-8解码:" + new String(origin.getBytes("UTF-8"), "GBK"));

String str = "激极尽致,求真品诚";
String gbk2UtfString = new String(str.getBytes("GBK"), "UTF-8");
System.out.println("GBK转换成UTF-8:" + gbk2UtfString);

String gbk2Utf2GbkString = new String(gbk2UtfString.getBytes("UTF-8"), "GBK");
System.out.println("GBK转换成UTF-8再转成GBK:" + gbk2Utf2GbkString);

输出:

GBK编码,UTF-8解码:锟斤拷
GBK转换成UTF-8:�������£�����Ʒ��
GBK转换成UTF-8再转成GBK:锟斤拷锟斤拷锟斤拷锟铰o拷锟斤拷锟斤拷品锟斤拷

以上,我们可以知道,以后如果再见到锟斤拷,肯定是UTF-8和GBK的转换问题。

扩展阅读:
https://mp.weixin.qq.com/s/C3iazvqdI8lfxBcr7GbwiA

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值