在 GB2312 等编码环境下的 zip 格式压缩包在 Mac OS X 环境下解压,或者从各种奇怪编码格式的 ftp 下载文件,以及从各种生产环境竟然不是utf-8的古代论坛下载到附件之类的事情之后,文件名很可能会变成乱码。这些乱码是由于 GB2312 编码(或者其它编码)下的文字的binary形式被错误地使用 utf-8 方式解码产生的。
刚开始我以为这种乱码的解决方法很简单,随便 Google 了一下发现了一个程序叫 convmv,安装,运行,报错,失败。由于这个程序给出的诊断信息实在太少,当时并不知道是哪里出现了问题,遂弃之。
开始自己动手,感觉用 Python 写一个简单的程序应该马上就能解决,所以我写了一段程序,核心代码如下:
#!/usr/env/python3 # -*- coding: utf-8 -*- import os def cy(s): return s.encode('iso-8859-1').decode('gb2312') for f in os.listdir(): os.rename(f, cy(f))
没想到一测试运行竟然出错了。有一些字符不能用 gb2312 解码,这是很不正常的,强行加上 erros=’ignore’ 参数之后得到了完全不正确的结果。观察结果,发现文件名的一部份能被正常还原出来,而带有重音符号、tilde 或是 diaeresis 之类符号的乱码则不能被还原。立即对 os.listdir() 函数返回的文件名产生了怀疑。把结果 print 出来 pipe 到 od,发现果然字节不对,想起很久之前刷书的时候背过这么一段话:
Certain Unicode characters can be encoded in more than one way. For example, an Á (A acute) can be encoded either precomposed, as U+00C1 (LATIN CAPITAL LETTER A WITH ACUTE), or decomposed, as U+0041 U+0301 (LATIN CAPITAL LETTER A followed by a COMBINING ACUTE ACCENT). Precomposed characters are more common in the Windows world, whereas decomposed characters are more common on Apple platforms.
要想用 iso-8859-1 编码出可以被 gb2312 解码的字符串,应该使用的是 precomposed 的 Unicode 形式,而翻了一下服务器上还幸存的一点 Apple 的开发者资料,里面有之前开发 VFS 插件的总结,才发现以前已经踩过这个坑了。OS X 存储的文件名确实是使用的 decomposed characters。因此这些乱码文件名相当于先用错误的 utf-8 编码来解码,之后又被 decompose。因此如果直接试图用编码解码的方法处理就会出错。
原理知道了解决问题也就没什么特别难的,不过是加两行用来还原的代码。
#!python3 # -*- coding: utf-8 -*- import os import unicodedata import argparse parser = argparse.ArgumentParser(description="convert incorrectly decoded filenames into utf-8 encoding") group = parser.add_mutually_exclusive_group() group.add_argument("-d", "--dry", action="store_true") group.add_argument("-r", "--run", action="store_true") args = parser.parse_args() def rec(filename): return filename.encode('iso-8859-1').decode('gb2312') def convert_to_precomposed(filename): return unicodedata.normalize('NFC', filename) filenames = os.listdir() filenames.remove(__file__) for dirpath, dirnames, filenames in os.walk("."): for name in dirnames + filenames: filename = '/'.join([dirpath, name]) try: if args.run: os.rename(filename, '/'.join([dirpath, rec(convert_to_precomposed(name))])) elif args.dry: print(filename, '/'.join([dirpath, rec(convert_to_precomposed(name))])) else: print("Argument is required explicitly") exit(0) except Exception as inst: print('error converting %s:' % filename) print(type(inst), inst.args)
还原大量文件的时候会有一小部分失败的,说明这部分的文件名原本不是 gb2312 编码,把代码里的 gb2312 依次改成 Shift-JIS,gbk 等常见的编码再试试即可。
Edit: 刚才有人问我为什么不直接在解压的时候指定编码,问这种问题说明你没有解压过爬虫爬下来的几万个附件。。来自各个国家各个年代的编码都有,最方便的办法显然还是直接用 utf-8 解压,然后再把文件名转回来。