Chinaunix

标题: 单字节字符编码识别问题,windows-1251字符集识别问题 [打印本页]

作者: mci2004    时间: 2012-10-26 21:46
标题: 单字节字符编码识别问题,windows-1251字符集识别问题
好吧,不得不提个问题了,关于windows-1251字符集检测和转码utf-8的问题。

其实我对字符集,unicode,utf-8啥的一知半解。
  

    最近在做android,遇到一个mp3文件中俄文字符显示乱码的问题。其实呢,对于这种蛋疼的问题完全可以无视的,但是bug是外国客户报出来的,优先级P0。我想肯定是那个制作这个mp3的人,在编辑文件信息(歌曲名字,专辑...)的时候保存成了window-1251,结果到android上媒体文件扫描的时候,不认识这个字符集然后就乱码了。我又想起序哥当年教育我,utf-8保存文件是程序员的基本常识。
    我看了下android上媒体扫描这部分的代码,对于双字节编码的字符android上会做一个检测找到其对应的字符集,例如gbk,big5, shift-jis这些字符集。这个检测依据就是到一个CharSetRange的表中出二分查找这个字符,看看在不在对应的表中。
   Ok,我的问题是,对于windows-1251这种单字节编码的字符,该怎么做检测呢?比如两个俄文字符例如 A B,在windows-1251上被编码成0xCFF0,但是按照android原来的检测方案0xCFF0这两个字节会被当成一个字符去那几个CharSetRange里面查表,查找失败就什么都不做直接传到上层,然后显示就是乱码,那些乱码的字符和用utf-8打开这个mp3文件看到的一样。
   唉,我凌乱了,对于这种单字节编码的字符,在这个字符串传过来的时候我有办法做个检测吗?有办法知道是哪种字符集吗?我想通过查找字符范围的方法并不能确定是哪一个单字节字符集吧?因为,单字节诶我怎么知道0xCF这个字符不会出现在其他的什么XXX字符集中啊?
   看看这个链接是windows-1251 to Unicode table。其实,这种字符的转换的问题,只要按照一个native code to unicode表转成对应的unicode码字传到上层就ok了,是这样吗?
     
    字太多,大神看的烦,我可以把代码贴上来。谢谢各位了。


@starwing83 序哥,我就不浪费你的电话费了,浪费一点点流量吧!
作者: mci2004    时间: 2012-10-26 21:53
还是贴个代码吧....

  1. static uint32_t possibleEncodings(const char* s)
  2. {
  3.     uint32_t result = kEncodingAll;
  4.     // if s contains a native encoding, then it was mistakenly encoded in utf8 as if it were latin-1
  5.     // so we need to reverse the latin-1 -> utf8 conversion to get the native chars back
  6.     uint8_t ch1, ch2;
  7.     uint8_t* chp = (uint8_t *)s;

  8.     while ((ch1 = *chp++)) {
  9.         if (ch1 & 0x80) {
  10.             ch2 = *chp++;
  11.             ch1 = ((ch1 << 6) & 0xC0) | (ch2 & 0x3F);
  12.             // ch1 is now the first byte of the potential native char

  13.             ch2 = *chp++;
  14.             if (ch2 & 0x80)
  15.                 ch2 = ((ch2 << 6) & 0xC0) | (*chp++ & 0x3F);
  16.             // ch2 is now the second byte of the potential native char
  17.             int ch = (int)ch1 << 8 | (int)ch2;
  18.             result &= findPossibleEncodings(ch);
  19.         }
  20.         // else ASCII character, which could be anything
  21.     }

  22.     return result;
  23. }
复制代码
  1. extern uint32_t findPossibleEncodings(int ch)
  2. {
  3.     // ASCII matches everything
  4.     if (ch < 256) return kEncodingAll;

  5.     int result = kEncodingNone;

  6.     if (charMatchesEncoding(ch, kShiftJISRanges, ARRAY_SIZE(kShiftJISRanges)))
  7.         result |= kEncodingShiftJIS;
  8.     if (charMatchesEncoding(ch, kGBKRanges, ARRAY_SIZE(kGBKRanges)))
  9.         result |= kEncodingGBK;
  10.     if (charMatchesEncoding(ch, kBig5Ranges, ARRAY_SIZE(kBig5Ranges)))
  11.         result |= kEncodingBig5;
  12.     if (charMatchesEncoding(ch, kEUCKRRanges, ARRAY_SIZE(kEUCKRRanges)))
  13.         result |= kEncodingEUCKR;

  14.     return result;
  15. }
复制代码

作者: starwing83    时间: 2012-10-26 21:55
这样,你能不能获得系统的默认编码?如果发现编码不是UTF8(这个应该很简单),就认为是当前编码(肯定是俄文),然后转换成utf8,怎么样?
作者: mci2004    时间: 2012-10-26 22:01
starwing83 发表于 2012-10-26 21:55
这样,你能不能获得系统的默认编码?如果发现编码不是UTF8(这个应该很简单),就认为是当前编码(肯定是俄 ...


肯定不行啊?你忽悠我吧,我跟我那个同事的方案不是一样恶心吗?用你的话说,很脏....
作者: starwing83    时间: 2012-10-26 22:05
回复 4# mci2004


    C层面应该有直接转utf8的方法,即使没有我记得Android的C层面也有ICU这样的库,你找找怎么用,获取默认locale应该也是有方法的,一种是LANG环境变量,一种是LC_XXX环境变量等等,你找找这方面的资料嘛。
作者: OwnWaterloo    时间: 2012-10-26 22:40
character(或者code point) 是否能按某种方式编码为 bytes? 这是可以检测出的。
具体术语其实很混乱。。。 举个例子就是:

'©' <- 能看到这个字符么? copy right sign。 它的code point是0xa9。
可以确定它能按utf8编码为0xc2, 0xa9, 也能确定它不能按cp936编码。 至于怎么确定。。。 查文档。。。



而从一堆bytes推测它们是由哪些character以什么编码得到。。。 这只能。。。
举个例子。。。
"\xce\xbb", 它可能是'λ'(小写希腊字母lambda, code point=0x3bb)按utf-8编码得到, 也可能是'位'(code point=0x4f4d)按cp936编码得到。
如果把"\xce\xbb"传给你的人不告诉你他使用的编码方式。。。  是没法确定他到底是想表达'λ'还是'位'。。。

当然,bytes给得够多的话,还是有很高概率猜对(只有一种解码方式完全不产生错误)的。。。

具体。。。   让序哥去翻vim的源代码呗。。。

作者: mci2004    时间: 2012-10-27 00:00
回复 6# OwnWaterloo


    Waterloo大神都来了,十分感谢,“猜”这个我大概明白一点,说白了就是计算一个可信度的问题。这个蛋疼的问题,和序哥讨论过了,只能用比较恶心的方法来规避了。其实,我倒觉得不改最和谐。

作者: mci2004    时间: 2012-10-28 02:02
本帖最后由 mci2004 于 2012-10-28 02:02 编辑

@starwing83 序哥,今天晚上又仔细看了下代码,发现代码中似乎不可能出现我昨天说的情况----在手机看到的乱码就是latin1编码。

1,
首先,可以确定你说的是对的,在vim上打个一个mp3文件看到的乱码确实是latin1,为此我特地去查了ISO-8859-1,也就说明了一个文件在vim上如果它不认识直接就当作latin1来转了。而且latin1是单字节的。

2,
关于locale的问题,我也查看了代码,locale信息的确定实际上通过jdk里面的Local类来确定的。这个Locale的确定又和android的系统属性有关。还记得我们昨天看到的那个函数吗?
  1. // if the locale encoding matches, then assume we have a native encoding.
  2.         if (encoding & mLocaleEncoding)
复制代码
事实上这个函数是做一个double check后面一个mLocalEncoding是通过android系统在java层获得的。前面一个encoding是通过findPossibleEncoding()---查巨表的函数获得的。如果这两个相等就非常确定可以是四种字符集中的某一个了,就可以交给下面的conertValues()了。

3,
回到最开始的问题,我为什么可以确定latin1编码不可能出现呢?因为,我看到了下面的代码
还记得endFile()这个函数吧?在没有比配成功字符集后,会直接交给上层去处理,也就是下面的逻辑
  1. // if the locale encoding matches, then assume we have a native encoding.
  2.         if (encoding & mLocaleEncoding)
  3.             convertValues(mLocaleEncoding);

  4.         // finally, push all name/value pairs to the client
  5.         for (int i = 0; i < mNames->size(); i++) {
  6.             status_t status = handleStringTag(mNames->getEntry(i), mValues->getEntry(i));
  7.             if (status) {
  8.                 break;
  9.             }
复制代码
convertValues没有机会执行,那么直接handleSringTag,这个时候那个传过来的字符(乱码那个)被当作一个字符串传进了handleString里(没做任何处理)。

//序哥下面是最后一个函数handleStringTag,主要关注value参数,它是乱码
  1. virtual status_t handleStringTag(const char* name, const char* value)
  2.     {
  3.         ALOGV("handleStringTag: name(%s) and value(%s)", name, value);
  4.         jstring nameStr, valueStr;
  5.         if ((nameStr = mEnv->NewStringUTF(name)) == NULL) {
  6.             mEnv->ExceptionClear();
  7.             return NO_MEMORY;
  8.         }

  9.         // Check if the value is valid UTF-8 string and replace
  10.         // any un-printable characters with '?' when it's not.
  11.         char *cleaned = NULL;
  12.         //判断是不是utf-8原理很简单,看单个字节是否在0x80-->0xBF之间,序哥这里有什么要补充的吗?
  13.         if (utf8_length(value) == -1){
  14.             cleaned = strdup(value);
  15.             char *chp = cleaned;
  16.             char ch;
  17.             while ((ch = *chp)) {
  18.                 if (ch & 0x80) {
  19. //看这里,如果不是utf-8且字符在0x80之后,就认为是unprintable,然后设置成‘?’。
  20.                     *chp = '?';
  21.                 }
  22.                 chp++;
  23.             }
  24.             value = cleaned;
  25.         }
  26.         valueStr = mEnv->NewStringUTF(value);
  27.         free(cleaned);
  28.         if (valueStr == NULL) {
  29.             mEnv->DeleteLocalRef(nameStr);
  30.             mEnv->ExceptionClear();
  31.             return NO_MEMORY;
  32.         }

  33.         mEnv->CallVoidMethod(
  34.             mClient, mHandleStringTagMethodID, nameStr, valueStr);

  35.         mEnv->DeleteLocalRef(nameStr);
  36.         mEnv->DeleteLocalRef(valueStr);
  37.         return checkAndClearExceptionFromCallback(mEnv, "handleStringTag");
  38.     }
复制代码
所以我觉得应该会被显示成'?'才对啊。难道序哥,这个字符传到java层会被转换城latin1吗?不可能吧,java的文档我查了,没有这个说法啊?
但是我肯定我在手机上看到的那个乱码是latin1.
作者: starwing83    时间: 2012-10-28 03:57
回复 8# mci2004


    那么,你要干一件事情:

在C++的handleStringTag之前,把数据打印出来:直接按16进制打印裸的二进制到Log,然后把数据拿出来看。我怀疑在这一步以前,数据就已经被当作latin-1给转成utf-8了。这意味着,读出来的时候就被转换了。那么,你就顺着数据来的方向,隔一段距离打印一次,看看到底是在哪儿被改掉(变成合法的utf-8)的,最终应该能被追踪到StageFright里面去。然后就容易了。
作者: madaossan    时间: 2012-10-28 07:59
叔叔一年多前做android系统层, 也处理过andorid media库扫描的问题. LZ贴的代码我依稀看过.

不过对LZ问题的解决, 我帮不上忙. 纯灌水...

顺带吐槽下, Android媒体库扫描真TMD烂. 认为扫描路径只有/system/xxx/xxx和一张SD卡的Google程序员更是脑子被驴踢了...


作者: madaossan    时间: 2012-10-28 08:26
另外在上层, 不是framework, 而是packages目录里面有写媒体库扫描的服务(MediaProvider), 那里面在扫描之前有locale设定. 代码文件是MediaScannerService.java.

LZ你贴的代码是MediaScannerService通过JNI调过来的中间层. 中间层我记得里面还有获取locale的一个操作(即, 你的mLocalEncoding是由MediaScannerService设定, 你这里的mLocalEncoding应该是根据MediaScannerService的设定而来). 所以可以试试是否可以通过改上层locale来搞定. 不然就是LZ你贴的代码需要改了.
MediaScannerService.java中设定locale的代码如下(还是2.2 froyo版本的. 这段代码我直接搜出来的, 手上没环境和源码, 因为我很久没碰Android了):
  1. private MediaScanner createMediaScanner() {

  2.     MediaScanner scanner = new MediaScanner(this);

  3.     Locale locale = getResources().getConfiguration().locale;

  4.     if (locale != null) {
  5.         String language = locale.getLanguage();
  6.         String country = locale.getCountry();
  7.         String localeString = null;

  8.         if (language != null) {
  9.             if (country != null) {
  10.                 scanner.setLocale(language + "_" + country);
  11.             } else {
  12.                 scanner.setLocale(language);
  13.             }
  14.         }
  15.     }
  16.     return scanner;
  17. }
复制代码

作者: mci2004    时间: 2012-10-28 09:10
回复 11# madaossan


        对,谢谢你的回复,我这边是android4.1,我这边看代码用的vim只用cscope索引了framework层的代码,所以在查找函数调用的时候忽略了app层,如你所说确实是app那边MediaProvider---》MediaScannerConnection----》Bind一个MediaScannerService,然后就走到了我所说的setLocale逻辑。
       事实上我贴的代码是MediaScannerCinet部分的应该是算是MediaScannerServier的worker吧,逻辑上的顺利我懒得看了。
   
       话说android这么大个开源项目,有些在其他人看来的‘BUG’再正常不过了。但是,做android手机那些客户提的逆天的bug真的挺让人蛋疼吧。瞎折腾,反馈这个bug的外国客户恨不得让我们的手机支持所有现存native字符集。以后,再也不做android了,遇到这样的客户真心伤不起。
作者: mci2004    时间: 2012-10-29 13:10
本帖最后由 mci2004 于 2013-05-25 23:13 编辑
starwing83 发表于 2012-10-28 03:57
回复 8# mci2004

序哥,
我不得不说,又被你说对了,ID3.cpp这个文件做了这件事。

吐槽下,android是要闹哪样啊?latin1就laitin1嘛,非写成ISO/IEC 8859,尼玛8859版本分了好几个不知道啊。然后android c++层,分别用String8和String16来代表utf-8和utf-16。搞的好像Java一样。


Ps:序哥,目前‘安好’。一片“欣欣向荣”,人们“安居乐业”的景象。




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2