文章
· 十一月 10, 2022 阅读大约需 6 分钟

Caché 字符编码自动判断

Caché 字符编码自动判断

 

先说几个场景:

  1. 使用文件字符流打开一个文本文档,但是我不确定是以UTF8编码的还是GB18030,所以就无法准确设置TranslateTable,就导致了中文乱码问题。
  2. 有一个文件下载的csp,其中文件名参数可能是中文,如果在一个UTF8编码的界面直接调用时,后台取到的文件名就会是乱码。
  3. 接收到字节流后需要转成字符流读取内容,但是无法确定编码格式,就无法准确的转成字符。

以上几个场景虽然大多都可以提前做好约定解决,但是可能有历史原因或者种种情况,需要我们自己能够解决,于是就有了下面的故事。

基础

首先我方系统使用GB18030编码,然后碰到的情况大多都是对方可能是UTF8编码,所以主要来解决识别字节流是不是UTF8编码的。

然后查了一个UTF8编码格式

  • 1字节 0xxxxxxx
  • 2字节 110xxxxx 10xxxxxx
  • 3字节 1110xxxx 10xxxxxx 10xxxxxx
  • 4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  • 5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
  • 6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

有了UTF8编码格式,然后逐字节进行判断,看整个字节序列是否完美符合UTF8编码,所以先实现了方法IfMatchUTF8Bytes


/// 是否是符合UTF8的字节序列
/// bytes 字节串或字节流
/// maxLen 最大验证长度 超过此长度的不再验证
/// Output Count  符合1-6字节编码标准的字符数 $lb(f1,f2,f3,f4,f5,f6)
/// 返回值 1符合 0不符合
ClassMethod IfMatchUTF8Bytes(bytes, maxLen = 30000, Output Count)
{

#;	这是标准的utf-8编码格式
#;	1字节 0xxxxxxx
#;	2字节 110xxxxx 10xxxxxx
#;	3字节 1110xxxx 10xxxxxx 10xxxxxx
#;	4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
#;	5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
#;	6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
	s flag=1,f1=0,f2=0,f3=0,f4=0,f5=0,f6=0,Count=""
	s ind=0
	while(1) {
		s ind=ind+1
		if maxLen>0,ind>maxLen q
		if $IsObject(bytes){ 
	 		if (bytes.AtEnd) q
			s byte = bytes.Read(1)
		}else{
			if ind > $L(bytes) q
			s byte = $E(bytes,ind)
		}
		
		s ascii=$a(byte)
		
		if ascii=0 { 
			s f1=f1+1
		}elseif ascii<=127{ //0000 0001 - 0111 1111  [1-127]
			s f1=f1+1
		}elseif ascii<=191{  //1000 0000 - 1011 1111  [128-191] //字符首字节没有这种
			s flag=0
			q
		}elseif ascii<=223{  //1100 0000 - 1101 1111  [192-223] //一个字符两个字节
			if $$nextBytesValid(1) {
				s f2=f2+1
			}else{
				s flag=0
				q
			}
		}elseif ascii<=239{  //1110 0000 - 1110 1111  [224-239] //一个字符三个字节
			if $$nextBytesValid(2) { 
				s f3=f3+1
			}else{
				s flag=0
				q
			}
		}elseif ascii<=247{  //1111 0000 - 1111 0111  [240-247] //一个字符四个字节
			if $$nextBytesValid(3) { 
				s f4=f4+1
			}else{
				s flag=0
				q
			}
		}elseif ascii<=251{  //1111 1000 - 1111 1011  [248-251] //一个字符五个字节
			if $$nextBytesValid(4) { 
				s f5=f5+1
			}else{
				s flag=0
				q
			}
		}elseif ascii<=253{  //1111 1100 - 1111 1101  [252-253] //一个字符六个字节
			if $$nextBytesValid(5) { 
				s f6=f6+1
			}else{
				s flag=0
				q
			}
		}else{  
			s flag=0
			q
		}
	}
	s Count=$lb(f1,f2,f3,f4,f5,f6)
	q flag
	
nextBytesValid(num)
	s nextValidFlag=1
	if $IsObject(bytes) {
		s nextBytes=bytes.Read(num)
	}else{
		s nextBytes=$e(bytes,ind+1,ind+num)
	}
	
	if $l(nextBytes)<num {  //长度不足 取不到了
		s ind=ind+$l(nextBytes)
		
		s nextValidFlag=0
		q nextValidFlag  
	}else{
		for k=1:1:num {
			s nextAscii=$a($e(nextBytes,k))
			if nextAscii>=128,nextAscii<=191 { //1000 0000 - 1011 1111 [128-191] //非字符首字节范围
			}else{
				s nextValidFlag=0
				q
			}
		}
		s ind=ind+num  //将索引后移num位
	}
	

	q nextValidFlag
}

 基于IfMatchUTF8Bytes方法,然后又实现了一个方法GuessUTF8Bytes,此方法又做了一些其它的判断:如果前三字节为UTF8BOM则直接认定为UTF8,如果校验结果中只有1、2字节的字符,而3-6字节字符没有,则认定为不是UTF8编码。

/// 猜测是否是UTF8字节序列
/// bytes 字节串或字节流
/// 返回值 1符合 0不符合
ClassMethod GuessUTF8Bytes(bytes)
{
    ///code
}

场景1

基于以上,我们就可以实现一个方法打开某文件获得字符流,自动判断编码并设置TranslateTable为相应编码了。

/// 打开某文件获得字符流(自动判断编码并设置TranslateTable为相应编码 目前只支持UTF8)
/// s fileSteam=##class(BSP.SYS.COM.Charset).OpenFileCharacterStream(fullName)
ClassMethod OpenFileCharacterStream(fullName = "", autocharset) As %FileCharacterStream
{
    s fileSteam=##class(%FileCharacterStream).%New()
    s sc=fileSteam.LinkToFile(fullName)
    if $$$ISERR(sc) {
        q ""
    }
    s oldTranslateTable=fileSteam.TranslateTable
    s fileSteam.TranslateTable="RAW"  //判断字节 需要先将TranslateTable设置成RAW
    if ..GuessUTF8Bytes(fileSteam) {
        d fileSteam.Rewind()
        s fileSteam.TranslateTable="UTF8"
    }else{
        d fileSteam.Rewind()
        s fileSteam.TranslateTable=oldTranslateTable    
    }
    q fileSteam
}

这样我们就可以随便读取本地的文本文件了,也不会乱码了,解决场景1。

 

场景2

如何判断一个请求它的参数是以UTF8编码的还是以GB18030呢,通过测试发现我们可以通过GetCgiEnv("QUERY_STRING")获取到请求的参数的,且测试发现不同浏览器设置可能不同,有的为URL编码后的,有的则只是按UTF8或GB18030编码后的字节序列。对于URL编码后的可以先使用$zconvert(url,"I","URL")解码获得字节序列再进行判断,故而实现了方法IfMatchUTF8EscapedURLGuessUTF8EscapedURL

/// 是否是符合UTF8编码并进行URL编码的字符串
/// url 字符传
/// maxLen 最大验证长度 超过此长度的不再验证
/// Output Count  符合1-6字节编码标准的字符数 $lb(f1,f2,f3,f4,f5,f6)
/// 返回值 1符合 0不符合
ClassMethod IfMatchUTF8EscapedURL(url, maxLen = 30000, Output Count)
{
    s bytes=$zconvert(url,"I","URL")
    s ret=..IfMatchUTF8Bytes(bytes,maxLen,.Count)
    q ret
}
/// 猜测是符合UTF8编码并进行URL编码的字符串
/// url 字符传
/// 返回值 1符合 0符合
ClassMethod GuessUTF8EscapedURL(url)
{
    ///code
}

基于以上就可以实现一个方法判断请求调用方是不是UTF8编码的了。

/// req %CSP.Request对象
/// 返回值 1符合 0不符合
ClassMethod GuessUTF8Request(req As %CSP.Request)
{
    ///code
}

 另外由于编码格式不一致,通过%requet.Data拿到的数据就会出现乱码,此时可以通过将自己解析QUERY_STRING来获取数据,于是有了ParseRequestData

/// 解析请求数据 (自动判断编码 ,目前只支持UTF8)
/// 目前只实现了从QUERY_STRING解析 在请求体的暂未找到原始数据
/// req %CSP.Request对象
/// data ByRef 解析出来的数据 
ClassMethod ParseRequestData(req As %CSP.Request, ByRef data)
{
    ///code
}

似乎这样就解决了场景二

 

 

 

场景3

有了上面的基础,似乎场景3自然而然水到渠成了。字节流转字符流首先想到的是读取字节,然后使用$zcvt(bytes,"I",charset)转换,但是这种需要注意不要将一个字符的多个字节分成了两段。此处是利用了%FileCharacterStream做了一次中转,使用其TranslateTable进行转换,不知是否有其它方法支持。

/// 将字节串转换为字符串(自动猜编码,目前只支持UTF8)
/// 将字节流转换为字符流(自动猜编码,目前只支持UTF8)
/// bytes 字节串或字节流
/// 返回值 字符串或字符流
/// w ##class(BSP.SYS.COM.Charset).Bytes2Chars($zcvt("测试","O","GB18030") )
ClassMethod Bytes2Chars(bytes)
{
    ///code
}

 

代码下载

Cache字符编码自动判断.zip

总结

一个小玩具,分享出来大家看看

讨论 (0)1
登录或注册以继续