楼主: LiouPerng
1909 2

[程序分享] SAS下GB18030编码文本列因丢失字节而无法导出数据集的修复宏 [推广有奖]

  • 0关注
  • 1粉丝

初中生

85%

还不是VIP/贵宾

-

威望
0
论坛币
1279 个
通用积分
0
学术水平
0 点
热心指数
0 点
信用等级
0 点
经验
130 点
帖子
15
精华
0
在线时间
12 小时
注册时间
2012-6-22
最后登录
2023-9-27

+2 论坛币
k人 参与回答

经管之家送您一份

应届毕业生专属福利!

求职就业群
赵安豆老师微信:zhaoandou666

经管之家联合CDA

送您一个全额奖学金名额~ !

感谢您参与论坛问题回答

经管之家送您两个论坛币!

+2 论坛币

想使用宏可直接在文章第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中。*/
  1. data work.test2;
  2.     set work.test1;
  3.     %fix_gb18030()
  4. run;
复制代码

fix_gb18030宏有5个参数:

参数1:要修复的列,列名之间用空格分隔,如:

  1. %fix_gb18030(a b c) /*修复a, b, c等3个文本列*/
  2. %fix_gb18030() /*修复全部文本列*/
复制代码

如果省略则默认修复所有列。注意,在知道肯定是哪几列出现问题时,尽量不要指定对所有列进行修复,除了速度更慢以外,也增大了将正确数据修复为错误数据的几率。

参数2:为了避免该宏使用的内部变量与数据集原有变量重名,而给内部变量加的前缀,通常该参数请跳过,如:

  1. %fix_gb18030(,abcde) /*前缀为abcde*/
  2. %fix_gb18030() /*前缀为fix_gb18030_*/
复制代码


参数3:对于错误数据存在字符串首字节就是某汉字第2个字节的情况,第3个参数设置为1修复效果会更好些,其他情况下请采用默认值(即0)。

参数4:待修复格列的最大宽度,默认为1000。

参数5:是否将修正信息输出到SAS日志,为1输出,默认为0不输出。

SAS代码:


  1. %macro fix_gb18030(cols_list,pre,fix_lost_first_half,max_length,putlog);
  2. %if %length(&cols_list.)=0 %then %let cols_list=_character_;
  3. %if %length(&pre.)=0 %then %let pre=fix_gb18030_;
  4. %if %length(&fix_lost_first_half.)=0 %then %let fix_lost_first_half=0;
  5. %if %length(&max_length.)=0 %then %let max_length=1000;
  6. %if %length(&putlog.)=0 %then %let putlog=0;
  7. array &pre.cols{*} &cols_list.;
  8. length &pre.s [        DISCUZ_CODE_47        ]amp;max_length.;
  9. %if &putlog.=1 %then %do;
  10. length
  11. &pre.err_obs $15 &pre.err_col_name $37
  12. &pre.err_old_hex &pre.err_new_hex $%eval(&max_length.*2+8)
  13. &pre.err_new_str $%eval(&max_length.+8);
  14. drop &pre.err_obs &pre.err_col_name &pre.err_old_hex &pre.err_new_hex &pre.err_new_str;
  15. %end;
  16. length &pre.b0 &pre.b1 &pre.b2 &pre.b3 &pre.err_byte 3;
  17. 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;
  18. &pre.err_byte=03fx;
  19. do &pre.g=1 to dim(&pre.cols);
  20.     &pre.m=prxmatch('/^(?:[\x00-\x7f]|[\x81-\xfe](?:[\x40-\x7e\x80-\xfe]|[\x30-\x39][\x81-\xfe][\x30-\x39]))*$/',&pre.cols{&pre.g});
  21.     if &pre.m=0 or &fix_lost_first_half.=1 then do;
  22.         &pre.s0=0;
  23.         &pre.s1=0;
  24.         &pre.s0s1=1;
  25.         &pre.l=lengthn(&pre.cols{&pre.g});
  26.         &pre.p=&pre.l+1;
  27.         do &pre.i=1 to &pre.l;
  28.             &pre.b0=rank(substr(&pre.cols{&pre.g},&pre.i,1));
  29.             if 081x<=&pre.b0<=0fex then do;
  30.                 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
  31.                     if &pre.s0s1=0 then &pre.s0+1;
  32.                     else &pre.s1+1;
  33.                 &pre.s0s1=mod(&pre.s0s1+1,2);
  34.             end;
  35.             else if 000x<=&pre.b0<=02fx or 03ax<=&pre.b0<=03fx or &pre.b0=07fx or &pre.b0=0ffx then do;
  36.                 &pre.p=&pre.i;
  37.                 &pre.i=&pre.l;
  38.             end;
  39.             else if 040x<=&pre.b0<=07ex or &pre.b0=080x then do;
  40.                 &pre.p=&pre.i+1;
  41.                 &pre.i=&pre.l;
  42.             end;
  43.         end;
  44.         if &pre.m=0 or &fix_lost_first_half.=1 and &pre.s0>&pre.s1 then do;
  45.             &pre.s=&pre.cols{&pre.g};
  46.             &pre.cols{&pre.g}=' ';
  47.             do &pre.h=1,2;
  48.                 if &pre.h=1 then do;
  49.                     &pre.l=&pre.p-1;
  50.                     if &fix_lost_first_half.=1 then do;
  51.                         if &pre.s0>&pre.s1 then do;
  52.                             &pre.i=2;
  53.                             &pre.b0=rank(substr(&pre.s,1,1));
  54.                             if 000x<=&pre.b0<=07fx then do;
  55.                                 &pre.cols{&pre.g}=byte(&pre.b0);
  56.                             end;
  57.                             else do;
  58.                                 &pre.cols{&pre.g}=byte(&pre.err_byte);
  59.                             end;
  60.                         end;
  61.                         else do;
  62.                             &pre.i=1;
  63.                         end;
  64.                     end;
  65.                     else do;
  66.                         &pre.i=1;
  67.                     end;
  68.                 end;
  69.                 else do;
  70.                     &pre.i=&pre.p;
  71.                     &pre.l=lengthn(&pre.s);
  72.                 end;
  73.                 do while(&pre.i<=&pre.l);
  74.                     &pre.b0=rank(substr(&pre.s,&pre.i,1));
  75.                     if 000x<=&pre.b0<=07fx then do;
  76.                         substr(&pre.cols{&pre.g},&pre.i,1)=byte(&pre.b0);
  77.                         &pre.i+1;
  78.                     end;
  79.                     else if &pre.b0=080x or &pre.b0=0ffx then do;
  80.                         substr(&pre.cols{&pre.g},&pre.i,1)=byte(&pre.err_byte);
  81.                         &pre.i+1;
  82.                     end;
  83.                     else if 081x<=&pre.b0<=0fex then do;
  84.                         if &pre.i+1>&pre.l then do;
  85.                             substr(&pre.cols{&pre.g},&pre.i,1)=byte(&pre.err_byte);
  86.                             &pre.i+1;
  87.                         end;
  88.                         else do;
  89.                             &pre.b1=rank(substr(&pre.s,&pre.i+1,1));
  90.                             if 040x<=&pre.b1<=07ex or 080x<=&pre.b1<=0fex then do;
  91.                                 substr(&pre.cols{&pre.g},&pre.i,2)=byte(&pre.b0)||byte(&pre.b1);
  92.                                 &pre.i+2;
  93.                             end;
  94.                             else if 030x<=&pre.b1<=039x then do;
  95.                                 if &pre.i+2>&pre.l then do;
  96.                                     substr(&pre.cols{&pre.g},&pre.i,2)=repeat(byte(&pre.err_byte),2);
  97.                                     &pre.i+2;
  98.                                 end;
  99.                                 else if &pre.i+3>&pre.l then do;
  100.                                     substr(&pre.cols{&pre.g},&pre.i,3)=repeat(byte(&pre.err_byte),3);
  101.                                     &pre.i+3;
  102.                                 end;
  103.                                 else do;
  104.                                     &pre.b2=rank(substr(&pre.s,&pre.i+2,1));
  105.                                     &pre.b3=rank(substr(&pre.s,&pre.i+3,1));
  106.                                     if 081x<=&pre.b2<=0fex and 030x<=&pre.b3<=039x then do;
  107.                                         substr(&pre.cols{&pre.g},&pre.i,4)=byte(&pre.b0)||byte(&pre.b1)||byte(&pre.b2)||byte(&pre.b3);
  108.                                     end;
  109.                                     else do;
  110.                                         substr(&pre.cols{&pre.g},&pre.i,4)=repeat(byte(&pre.err_byte),4);
  111.                                     end;
  112.                                     &pre.i+4;
  113.                                 end;
  114.                             end;
  115.                             else do;
  116.                                 substr(&pre.cols{&pre.g},&pre.i,2)=repeat(byte(&pre.err_byte),2);
  117.                                 &pre.i+2;
  118.                             end;
  119.                         end;
  120.                     end;
  121.                 end;
  122.             end;
  123. %if &putlog.=1 %then %do;
  124.             if &pre.s^=&pre.cols{&pre.g} then do;
  125.                 &pre.err_obs='Obs['||put(_n_,1.)||']';
  126.                 &pre.err_col_name='Col['||vname(&pre.cols{&pre.g})||']';
  127.                 &pre.err_old_hex='OldHex['||trimn(put(trimn(&pre.s),$hex.))||']';
  128.                 &pre.err_new_hex='NewHex['||trimn(put(trimn(&pre.cols{&pre.g}),$hex.))||']';
  129.                 &pre.err_new_str='NewStr"'||trimn(&pre.cols{&pre.g})||'"';
  130.                 putlog 'GB18030 Fixed:' &pre.err_obs &pre.err_col_name;
  131.                 putlog &pre.err_old_hex;
  132.                 putlog &pre.err_new_hex;
  133.                 putlog &pre.err_new_str;
  134.             end;
  135. %end;
  136.         end;
  137.     end;
  138. end;
  139. %mend fix_gb18030;

  140. %macro fix_gb18030_ds(src_ds,dst_ds,cols_list,pre,fix_lost_first_half,max_length,putlog);
  141. data %if %length(&dst_ds.)=0 %then &src_ds.;%else &dst_ds.;;
  142.     set &src_ds.;
  143.     %fix_gb18030(&cols_list.,&pre.,&fix_lost_first_half.,&max_length.,&putlog.)
  144. run;
  145. %mend fix_gb18030_ds;

  146. data work.test1;
  147.     length text1 $40;
  148.     input text1$;
  149.     select(_n_);
  150.         when(1)substr(text1,37,1)=byte(32);/*最末字符为汉字且次字节丢失(替换为空格)*/
  151.         when(2)substr(text1,37,1)=byte(64);/*最末字符为汉字且次字节替换为@*/
  152.         when(3)substr(text1,9,1)=byte(32);/*首个汉字次字节替换为空格*/
  153.         when(4)text1=substr(text1,2);/*首个字节丢失*/
  154.         otherwise;
  155.     end;
  156.     datalines4;
  157. GB18030编码下汉字易出现半个汉字的问题
  158. GB18030编码下汉字易出现半个汉字的问题
  159. GB18030编码下汉字易出现半个汉字的问题
  160. 编码下汉字易出现半个汉字的问题
  161. ;;;;

  162. data work.test2;
  163.     set work.test1;
  164.     %fix_gb18030()
  165. run;

  166. data work.test3;
  167.     set work.test1;
  168.     %fix_gb18030(,,1,,1)
  169. run;
复制代码







二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

关键词:数据集 Character PrxMatch Windows length

沙发
whymath 发表于 2019-11-20 22:22:32 |只看作者 |坛友微信交流群
从来没有见到过你说的错误,可以上传一个示例数据集吗?

使用道具

藤椅
LiouPerng 发表于 2022-7-10 00:18:09 |只看作者 |坛友微信交流群
whymath 发表于 2019-11-20 22:22
从来没有见到过你说的错误,可以上传一个示例数据集吗?
不好意思,很久没上了。代码中已带有示例数据集。

使用道具

您需要登录后才可以回帖 登录 | 我要注册

本版微信群
加好友,备注cda
拉您进交流群

京ICP备16021002-2号 京B2-20170662号 京公网安备 11010802022788号 论坛法律顾问:王进律师 知识产权保护声明   免责及隐私声明

GMT+8, 2024-4-23 20:41