关于你不想知道的所有Python3 unicode特性(2)
UNIX上的unicode只在你强制所有东西用它的时候会很疯狂。但那不是unicode在UNIX上工作的方式。UNIX没有区别unicode和字节的API。它们是相同的,使其更容易处理。
C Locale
C Locale在这里出现的次数非常多。C Locale是避免POSIX的规格被强行应用到任何地方的一种手段。POSIX服从操作系统需要支持设置LC_CTYPE,来让一切使用ASCII编码。
这个locale是在不同的情况下挑选的。你主要发现这个locale为所有从cron启动的程序,你的初始化程序和子进程提供一个空的环境。C Locale在环境里复原了一个健全的ASCII地带,否则你无法信任任何东西。
但是ASCII这个词指出它是7bit编码。这不是问题,因为操作系统是能处理字节的!任何基于8bit的内容能正常处理,但你与操作系统遵循约定,那么字符处理会限制在前7bit。任何你的工具生成的信息它会用ASCII编码并且使用英语。
注意POSIX规范没有说你的应用程序应当死于火焰。
Python3死于火焰
Python3在unicode上选择了与UNIX不同的立场。Python3说:任何东西是Unicode(默认情况下,除非是在某些情况下,除非我们发送重复编码的数据,可即使如此,有时候它仍然是Unicode,虽然是错误的Unicode)。文件名是Unicode,终端是Unicode,stdin和stdout是Unicode,有如此多的Unicode。因为UNIX不是Unicode,Python3现在的立场是它是对的UNIX是错的,人们也应该修改POSIX的定义来添加Unicode。那么这样的话,文件名就是Unicode了,终端也是Unicode了,这样也就不会看到一些由于字节导致的错误了。
不是仅仅我这样说。这些是Python关于Unicode的脑残想法导致的bug:
- ASCII是很槽糕的文件名编码
- 用surrogateescape作为默认error handler
- Python3在C locale下抛出Unicode错误
- LC CTYPE=C,pydoc给终端留下一个不能使用的状态
如果你Google一下,你就能发现如此多的吐槽。看看有多少人安装pip模块失败,原因是changelog里的一些字符,或者是因为home文件夹的原因又,或者是因为SSH session是用ASCII的,或者是因为他们是使用Putty连接的。
Python3 cat
现在开始为Python3修复cat。我们如何做?首先,我们需要处理字节,因为有些东西可能会显示一些不符合shell编码的东西。所以无论如何,文件内容需要是字节。但我们也需要打开基本输出来让它支持字节,而它默认是不支持的。我们也需要分别处理一些情况比如Unicode API失败,因为编码是C。那么这就是,Python3特性的cat。
import sys import shutil def _is_binary_reader(stream, default=False): try: return isinstance(stream.read(0), bytes) except Exception: return default def _is_binary_writer(stream, default=False): try: stream.write(b'') except Exception: try: stream.write('') return False except Exception: pass return default return True def get_binary_stdin(): # sys.stdin might or might not be binary in some extra cases. By # default it's obviously non binary which is the core of the # problem but the docs recomend changing it to binary for such # cases so we need to deal with it. Also someone might put # StringIO there for testing. is_binary = _is_binary_reader(sys.stdin, False) if is_binary: return sys.stdin buf = getattr(sys.stdin, 'buffer', None) if buf is not None and _is_binary_reader(buf, True): return buf raise RuntimeError('Did not manage to get binary stdin') def get_binary_stdout(): if _is_binary_writer(sys.stdout, False): return sys.stdout buf = getattr(sys.stdout, 'buffer', None) if buf is not None and _is_binary_writer(buf, True): return buf raise RuntimeError('Did not manage to get binary stdout') def filename_to_ui(value): # The bytes branch is unecessary for *this* script but otherwise # necessary as python 3 still supports addressing files by bytes # through separate APIs. if isinstance(value, bytes): value = value.decode(sys.getfilesystemencoding(), 'replace') else: value = value.encode('utf-8', 'surrogateescape') \ .decode('utf-8', 'replace') return value binary_stdout = get_binary_stdout() for filename in sys.argv[1:]: if filename != '-': try: f = open(filename, 'rb') except IOError as err: print('cat.py: %s: %s' % ( filename_to_ui(filename), err ), file=sys.stderr) continue else: f = get_binary_stdin() with f: shutil.copyfileobj(f, binary_stdout)