Mac OS X 下文件名乱码出现的原因和解决方法

在 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 解压,然后再把文件名转回来。

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.