关于你不想知道的所有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)






