Caché 字符编码自动判断
Caché 字符编码自动判断
先说几个场景:
- 使用文件字符流打开一个文本文档,但是我不确定是以UTF8编码的还是GB18030,所以就无法准确设置TranslateTable,就导致了中文乱码问题。
- 有一个文件下载的csp,其中文件名参数可能是中文,如果在一个UTF8编码的界面直接调用时,后台取到的文件名就会是乱码。
- 接收到字节流后需要转成字符流读取内容,但是无法确定编码格式,就无法准确的转成字符。
以上几个场景虽然大多都可以提前做好约定解决,但是可能有历史原因或者种种情况,需要我们自己能够解决,于是就有了下面的故事。
基础
首先我方系统使用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 10xxxxxxs flag=1,f1=0,f2=0,f3=0,f4=0,f5=0,f6=0,Count=""s ind=0while(1) {
s ind=ind+1if maxLen>0,ind>maxLen qif$IsObject(bytes){
if (bytes.AtEnd) qs byte = bytes.Read(1)
}else{
if ind > $L(bytes) qs byte = $E(bytes,ind)
}
sascii=$a(byte)
ifascii=0 {
s f1=f1+1
}elseifascii<=127{ //0000 0001 - 0111 1111 [1-127]s f1=f1+1
}elseifascii<=191{ //1000 0000 - 1011 1111 [128-191] //字符首字节没有这种s flag=0q
}elseifascii<=223{ //1100 0000 - 1101 1111 [192-223] //一个字符两个字节if$$nextBytesValid(1) {
s f2=f2+1
}else{
s flag=0q
}
}elseifascii<=239{ //1110 0000 - 1110 1111 [224-239] //一个字符三个字节if$$nextBytesValid(2) {
s f3=f3+1
}else{
s flag=0q
}
}elseifascii<=247{ //1111 0000 - 1111 0111 [240-247] //一个字符四个字节if$$nextBytesValid(3) {
s f4=f4+1
}else{
s flag=0q
}
}elseifascii<=251{ //1111 1000 - 1111 1011 [248-251] //一个字符五个字节if$$nextBytesValid(4) {
s f5=f5+1
}else{
s flag=0q
}
}elseifascii<=253{ //1111 1100 - 1111 1101 [252-253] //一个字符六个字节if$$nextBytesValid(5) {
s f6=f6+1
}else{
s flag=0q
}
}else{
s flag=0q
}
}
s Count=$lb(f1,f2,f3,f4,f5,f6)
q flag
nextBytesValid(num)
s nextValidFlag=1if$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=0q nextValidFlag
}else{
fork=1:1:num {
s nextAscii=$a($e(nextBytes,k))
if nextAscii>=128,nextAscii<=191 { //1000 0000 - 1011 1111 [128-191] //非字符首字节范围
}else{
s nextValidFlag=0q
}
}
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设置成RAWif..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")解码获得字节序列再进行判断,故而实现了方法IfMatchUTF8EscapedURL与GuessUTF8EscapedURL。
/// 是否是符合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
}
代码下载
总结
一个小玩具,分享出来大家看看