想使用宏可直接在文章第6部分的示例代码。
1.问题描述SAS设置编码为GB18030,SAS/EG在显示或导出结果数据集时报错,报错信息为:“未能将数据从U_EUC_CN_CE转码为U_UTF8_CE编码,因为它包含SAS会话编码不支持的字符。请查看您的encoding=和locale= SAS系统选项,以确保它们能够接受您要处理的数据。在十六进制表示法中的一部分源字符串为:……”
2.原因分析出现此类错误的原因,通常是SAS文本列没有足够的存储宽度,或是数据迁移脚本运用了错误的取子字符串逻辑,导致GB18030编码的文本数据不完整,出现了所谓“半个汉字”问题。由于GB18030编码设计的天然缺陷,当一段文本中出现ASCII范围以外的字符,且有字节缺失的情况下,文本部分或全部显示为乱码,且使用SAS/EG导出或显示时会报错。下图展示了正确文本丢掉首字节后的效果:
上图显示效果来自Windows自带的记事本,可以看出E03A因为E0首先被认为是一个GB18030汉字的首字节,但是因为E03A不是合法的GB18030编码,所以这两个字节被显示为?,随后从D1继续开始解析。也就是说,对MBCS的处理策略,是前边的字节来决定后边的字节,解析过程总是从左到右的。然而一旦前边有字节缺失,后续计划有可能被全部打乱,因为解析策略未曾假定字节有可能已损坏,也从不尝试从中间或后面去修复。
3.手动修复方法对于此类问题,最简单粗暴的方法是drop掉整个出问题的列。另一种比较辛苦的方法,是在全部记录中只有少部分条目有问题的情况下,通过折半法反复试验来锁定第几行存在问题,但是这种方法工作的工作量非常大,效率非常低,如果出错不只一行,基本上是不可行的。
4.尝试自动修复的策略首先,我们来看一下GB18030的编码特点,请看下图:
GBK范围内的汉字,都采用GB18030的双字节编码,GB18030四字节部分含盖的是CJK Ext-B以及后的汉字,以及汉字以外的非ASCII字符,使用频率比较低。而从双字节来看,第一字节、第二字节都有可能是81-FE(这也是GB2312的范围,即最常用的汉字所占区域)。从码位空间看起来,就81-FE来说,双字节的第一字节和第二字节编码区域重合,单从数据范围来说,出现几率相等,一旦缺少某个字节后,似乎修复变成了不可能的任务。
但是,我们还有两方面的机会。
第一是,文本不一定完全是汉字组成的,一旦内部含有单字节的ASCII部分内容,则可确定字符边界:边界一边即使是坏的,边界的另一边还可以重新开始。
第二是,即使完全只有汉字,但是其实各个字节值的分布也是有一定特点的,比如文本中经常会有中文标点,特别是逗号“,”句号“。”的出现频率是非常之高的,而标点的第一字节多属A1或A3区,也就是某字节如果为A1,或A3,它很有可能是GB18030码某字符的开始字节。
综合考虑以上两项因素,逐字符分析一个已损坏的文本,也很可能修复大部分内容正常。实在无法修复的,至少可以将试图修复过程中的问题字节,强制改为某字符(一般推荐修复为?比较利于显示)。
通过梳理各种情况,我们得到以下表:
上表中可以看出,一旦出现00-2F, 3A-3F, 7F, FF,则其肯定是单字节,其上一字节属于另一个字符的结束,下一个字节也属于另一个字符的开端。而80则也肯定是双字节的结束字节,其下一字节属于另一个字符的开端。
除了肯定是单字节的情况,且不考虑出现机率比较小的四字节部分(四字节的情况很多,会使分析变得特别复杂),对于第二条思路,我们主要需要分析某一串连续的81-FE组成的字节中,从1开始进行位置编号,对于整个文本中,奇数位置上和偶数位置上出现某些特别字节的统计,可以帮助我们尝试是否发生了字节丢失(有误判的可能)。通常一些文字材料的统计,目前暂且总结出当出现以下字节:
A1,A3,B0-B9,BB-BE,C0,C1,C3,C7,C8,CA,CC-CF,D1-D7
时,这些字节更有可能是双字节的首字节部分。
综合以上两条策略,我制作了一个SAS宏分享给大家,它可对数据集中指定,或全部文本字段,试图进行修复,代码见本文第6部分。
5.建议从源端解决乱码的问题
以上修复的宏,毕竟也不是尽善尽美,不仅增加了麻烦,还有一定几率将正确的数据修复为错误的,治本的方法还需要从数据源端去解决错误的substr逻辑和宽度设置。
6.示例代码
为了修复含有乱码的数据集,请通过SAS数据步将问题数据集复制到另一处,并在数据步的set语句后加入fix_gb18030宏,如:
/*对work中有问题的test1数据集修复,结果存在work.test2中。*/- data work.test2;
- set work.test1;
- %fix_gb18030()
- run;
fix_gb18030宏有5个参数:
参数1:要修复的列,列名之间用空格分隔,如:
- %fix_gb18030(a b c) /*修复a, b, c等3个文本列*/
- %fix_gb18030() /*修复全部文本列*/
如果省略则默认修复所有列。注意,在知道肯定是哪几列出现问题时,尽量不要指定对所有列进行修复,除了速度更慢以外,也增大了将正确数据修复为错误数据的几率。
参数2:为了避免该宏使用的内部变量与数据集原有变量重名,而给内部变量加的前缀,通常该参数请跳过,如:
- %fix_gb18030(,abcde) /*前缀为abcde*/
- %fix_gb18030() /*前缀为fix_gb18030_*/
参数3:对于错误数据存在字符串首字节就是某汉字第2个字节的情况,第3个参数设置为1修复效果会更好些,其他情况下请采用默认值(即0)。
参数4:待修复格列的最大宽度,默认为1000。
参数5:是否将修正信息输出到SAS日志,为1输出,默认为0不输出。
SAS代码:
- %macro fix_gb18030(cols_list,pre,fix_lost_first_half,max_length,putlog);
- %if %length(&cols_list.)=0 %then %let cols_list=_character_;
- %if %length(&pre.)=0 %then %let pre=fix_gb18030_;
- %if %length(&fix_lost_first_half.)=0 %then %let fix_lost_first_half=0;
- %if %length(&max_length.)=0 %then %let max_length=1000;
- %if %length(&putlog.)=0 %then %let putlog=0;
- array &pre.cols{*} &cols_list.;
- length &pre.s [ DISCUZ_CODE_47 ]amp;max_length.;
- %if &putlog.=1 %then %do;
- length
- &pre.err_obs $15 &pre.err_col_name $37
- &pre.err_old_hex &pre.err_new_hex $%eval(&max_length.*2+8)
- &pre.err_new_str $%eval(&max_length.+8);
- drop &pre.err_obs &pre.err_col_name &pre.err_old_hex &pre.err_new_hex &pre.err_new_str;
- %end;
- length &pre.b0 &pre.b1 &pre.b2 &pre.b3 &pre.err_byte 3;
- drop &pre.m &pre.s &pre.l &pre.p &pre.s0 &pre.s1 &pre.s0s1 &pre.g &pre.h &pre.i &pre.b0 &pre.b1 &pre.b2 &pre.b3 &pre.err_byte;
- &pre.err_byte=03fx;
- do &pre.g=1 to dim(&pre.cols);
- &pre.m=prxmatch('/^(?:[\x00-\x7f]|[\x81-\xfe](?:[\x40-\x7e\x80-\xfe]|[\x30-\x39][\x81-\xfe][\x30-\x39]))*$/',&pre.cols{&pre.g});
- if &pre.m=0 or &fix_lost_first_half.=1 then do;
- &pre.s0=0;
- &pre.s1=0;
- &pre.s0s1=1;
- &pre.l=lengthn(&pre.cols{&pre.g});
- &pre.p=&pre.l+1;
- do &pre.i=1 to &pre.l;
- &pre.b0=rank(substr(&pre.cols{&pre.g},&pre.i,1));
- if 081x<=&pre.b0<=0fex then do;
- if &pre.b0 in (0a1x,0a3x,0c0x,0c1x,0c3x,0c7x,0c8x,0cax) or 0b0x<=&pre.b0<=0b9x or 0bbx<=&pre.b0<=0bex or 0ccx<=&pre.b0<=0cfx or 0d1x<=&pre.b0<=0d7x then
- if &pre.s0s1=0 then &pre.s0+1;
- else &pre.s1+1;
- &pre.s0s1=mod(&pre.s0s1+1,2);
- end;
- else if 000x<=&pre.b0<=02fx or 03ax<=&pre.b0<=03fx or &pre.b0=07fx or &pre.b0=0ffx then do;
- &pre.p=&pre.i;
- &pre.i=&pre.l;
- end;
- else if 040x<=&pre.b0<=07ex or &pre.b0=080x then do;
- &pre.p=&pre.i+1;
- &pre.i=&pre.l;
- end;
- end;
- if &pre.m=0 or &fix_lost_first_half.=1 and &pre.s0>&pre.s1 then do;
- &pre.s=&pre.cols{&pre.g};
- &pre.cols{&pre.g}=' ';
- do &pre.h=1,2;
- if &pre.h=1 then do;
- &pre.l=&pre.p-1;
- if &fix_lost_first_half.=1 then do;
- if &pre.s0>&pre.s1 then do;
- &pre.i=2;
- &pre.b0=rank(substr(&pre.s,1,1));
- if 000x<=&pre.b0<=07fx then do;
- &pre.cols{&pre.g}=byte(&pre.b0);
- end;
- else do;
- &pre.cols{&pre.g}=byte(&pre.err_byte);
- end;
- end;
- else do;
- &pre.i=1;
- end;
- end;
- else do;
- &pre.i=1;
- end;
- end;
- else do;
- &pre.i=&pre.p;
- &pre.l=lengthn(&pre.s);
- end;
- do while(&pre.i<=&pre.l);
- &pre.b0=rank(substr(&pre.s,&pre.i,1));
- if 000x<=&pre.b0<=07fx then do;
- substr(&pre.cols{&pre.g},&pre.i,1)=byte(&pre.b0);
- &pre.i+1;
- end;
- else if &pre.b0=080x or &pre.b0=0ffx then do;
- substr(&pre.cols{&pre.g},&pre.i,1)=byte(&pre.err_byte);
- &pre.i+1;
- end;
- else if 081x<=&pre.b0<=0fex then do;
- if &pre.i+1>&pre.l then do;
- substr(&pre.cols{&pre.g},&pre.i,1)=byte(&pre.err_byte);
- &pre.i+1;
- end;
- else do;
- &pre.b1=rank(substr(&pre.s,&pre.i+1,1));
- if 040x<=&pre.b1<=07ex or 080x<=&pre.b1<=0fex then do;
- substr(&pre.cols{&pre.g},&pre.i,2)=byte(&pre.b0)||byte(&pre.b1);
- &pre.i+2;
- end;
- else if 030x<=&pre.b1<=039x then do;
- if &pre.i+2>&pre.l then do;
- substr(&pre.cols{&pre.g},&pre.i,2)=repeat(byte(&pre.err_byte),2);
- &pre.i+2;
- end;
- else if &pre.i+3>&pre.l then do;
- substr(&pre.cols{&pre.g},&pre.i,3)=repeat(byte(&pre.err_byte),3);
- &pre.i+3;
- end;
- else do;
- &pre.b2=rank(substr(&pre.s,&pre.i+2,1));
- &pre.b3=rank(substr(&pre.s,&pre.i+3,1));
- if 081x<=&pre.b2<=0fex and 030x<=&pre.b3<=039x then do;
- substr(&pre.cols{&pre.g},&pre.i,4)=byte(&pre.b0)||byte(&pre.b1)||byte(&pre.b2)||byte(&pre.b3);
- end;
- else do;
- substr(&pre.cols{&pre.g},&pre.i,4)=repeat(byte(&pre.err_byte),4);
- end;
- &pre.i+4;
- end;
- end;
- else do;
- substr(&pre.cols{&pre.g},&pre.i,2)=repeat(byte(&pre.err_byte),2);
- &pre.i+2;
- end;
- end;
- end;
- end;
- end;
- %if &putlog.=1 %then %do;
- if &pre.s^=&pre.cols{&pre.g} then do;
- &pre.err_obs='Obs['||put(_n_,1.)||']';
- &pre.err_col_name='Col['||vname(&pre.cols{&pre.g})||']';
- &pre.err_old_hex='OldHex['||trimn(put(trimn(&pre.s),$hex.))||']';
- &pre.err_new_hex='NewHex['||trimn(put(trimn(&pre.cols{&pre.g}),$hex.))||']';
- &pre.err_new_str='NewStr"'||trimn(&pre.cols{&pre.g})||'"';
- putlog 'GB18030 Fixed:' &pre.err_obs &pre.err_col_name;
- putlog &pre.err_old_hex;
- putlog &pre.err_new_hex;
- putlog &pre.err_new_str;
- end;
- %end;
- end;
- end;
- end;
- %mend fix_gb18030;
- %macro fix_gb18030_ds(src_ds,dst_ds,cols_list,pre,fix_lost_first_half,max_length,putlog);
- data %if %length(&dst_ds.)=0 %then &src_ds.;%else &dst_ds.;;
- set &src_ds.;
- %fix_gb18030(&cols_list.,&pre.,&fix_lost_first_half.,&max_length.,&putlog.)
- run;
- %mend fix_gb18030_ds;
- data work.test1;
- length text1 $40;
- input text1$;
- select(_n_);
- when(1)substr(text1,37,1)=byte(32);/*最末字符为汉字且次字节丢失(替换为空格)*/
- when(2)substr(text1,37,1)=byte(64);/*最末字符为汉字且次字节替换为@*/
- when(3)substr(text1,9,1)=byte(32);/*首个汉字次字节替换为空格*/
- when(4)text1=substr(text1,2);/*首个字节丢失*/
- otherwise;
- end;
- datalines4;
- GB18030编码下汉字易出现半个汉字的问题
- GB18030编码下汉字易出现半个汉字的问题
- GB18030编码下汉字易出现半个汉字的问题
- 编码下汉字易出现半个汉字的问题
- ;;;;
- data work.test2;
- set work.test1;
- %fix_gb18030()
- run;
- data work.test3;
- set work.test1;
- %fix_gb18030(,,1,,1)
- run;