工作中遇到了许多Python2的程序, 其中str, unicode与byte给 我带来了许多的困扰. 在努力搞懂了这些编码问题后, 我也简单的谈谈自己的理解,

这篇文章中:

  1. 不会涉及任何utf8的来历或是编码方式(我既不会, 也并未深究).
  2. 介绍Python2的程序中存在的一些问题(处理中文或是特殊字符时).
  3. 程序如何应对从Python2到Python3的升级.

代码中常见的几个字符串操作

以下几个部分的片段均是Python2中可以运行的代码(Python3中或多或少有些问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 1
ret = subprocess.check_output(cmd)
if PY3:
ret = ret.decode('utf-8')

# 2
if PY3:
value = json.dumps(local_conf[key]).encode('utf-8')
else:
value = json.dumps(local_conf[key])

# 3
line = p.stdout.readline()

# 4
with open(filepath, 'w') as fp:
fp.write(content)

# 5
raw = base64.b64decode(raw)

# 6
msg = '设置成功'

# 7
__name_mapping__ = {
Disabled: u'关闭',
Enable: u'开启',
}

假如一个程序中出现了这么多字符串的相关花样, 我几乎可以确定, 这个Python2程序绝对 不是一个人写的, 有人注意到了unicode的问题, 有人没有, 也有人为了兼容Python3 做出了努力. 但是, 我想说上面的程序是杂乱无章的, 对于接下来的维护, 或是想要把程序一点点进行升级时, 你就会发现程序的各处都要处理编码问题, encodedecode可能出现在任何地方.

encode与decode

已经说过博客中不打算考虑太多编码的底层问题, 我就简要的说说Python字符处理的两个方式:

encode decode

上面的处理形式是比较符合直观的理解, Python中, 通过encode与decode两个操作来进行 转换, 从字符串 => 字符数组或是字符数组 => 字符串.

在Python2中, 这样的转换是(这里的strbytes是一种):

1
2
3
       +----->  unicode  +-----+
decode | | encode
+-----+ str/bytes <-----+

在Python3中, 这样的转换是(这里的strbytes不同, 并且将unicode删除了):

1
2
3
       +----->    str    +-----+
decode | | encode
+-----+ bytes <-----+

接回上一小节, 程序中如果到处出现encode与decode, 说明你在程序中混用了 这两种数据结构.

也许程序本来是一致的, 但是Python3的出现打破了这个一致, 根本原因是编码的不通用.

不一致带来的问题

对于Python这种弱类型语言来说, 在写代码时几乎无法获得某个变量的类型. 所有的对象的类型只有在执行到特定语句时才会确定. 也就是说, 你会陷入到一种玄学调试的境地里: 在某条编码出错的语句里, 添加encode或是decode 程序就又能运行了. 错误是调完了, 可是接手的人怎么办呢? 只能一边口吐芬芳, 一边默默承受.

为什么不能保持一致呢?

真心推荐大家阅读一下<<代码大全>>, 其中不止一次的强调了: 在程序内部保持统一. 这里一共有两种类型的数据:

  1. 可以进行encode操作的普通字符串
  2. 可以进行decode操作的字符数组

理想情况是程序中只保留其中一种数据, 这样根本不需要encode或是decode. 但是很遗憾, 即使我们自己的程序可以, 许多我们引用的库却不一定支持支持. 它们可能产生或是消费不同种类的数据.

这种时候, 我们所应该做的, 不是放任两种字符串混杂, 而是应该保证在程序内部 只有一种数据. 接受其他类库产生的数据后, 马上进行转换, 而使用其他类库时, 将我们的数据转换成它所需要的数据类型. 记住, 唯一的原则就是, 你写的程序中 只出现其中一种数据类型.

一致性的保持

当然, 写博客之前我也做了许多尝试, 下面我提出自己的解决方案, 大家也可以根据 现有的程序自己进行摸索. 不过唯一的原则就是保持一致.

  1. 在我的程序中, 所有的字符串都保存为unicode(这是针对Python2的说法), 也就是可以进行encode操作的这一类数据.

  2. 所有的其他类库产生的输出, 全部进行过滤(使用to_unicode函数)

  3. 所有向其他类库发出的调用, 按照类库的要求进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1
ret = to_unicode(subprocess.check_output(cmd))

# 2
value = to_unicode(json.dumps(local_conf[key]))

# 3
line = to_unicode(p.stdout.readline())

# 4 输出文件比较特殊, Python2与3兼容的方式是使用bytes输出
with open(filepath, 'wb') as fp:
fp.write(to_utf8(content))

# 5 base64 需要输入bytes
raw = to_unicode(base64.b64decode(to_utf8(raw)))

# 6 这是种Python2与3兼容的形式
msg = u'设置成功'

以下是我的to_unicodeto_utf8函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
if sys.version_info > (3, 0):
unicode = str

def to_utf8(string, errors="replace"):
# type: (any) -> bytes
"""
Convert string "测试字符串" to b"测试字符串"

仅在程序与其他库交互时才使用.
"""
if not isinstance(string, unicode):
return string
# Be quiet by default
logger.debug("Encoding string with: %r" % string)
try:
return string.encode("UTF-8", errors)
except UnicodeEncodeError:
raise UnicodeEncodeError("Conversion from unicode failed: %r" % string)

def to_unicode(string, errors="replace"):
# type (any) -> str
"""
Convert b"测试字符串" to "测试字符串".

所有的输入字符串, 需要经过转换, 保证程序中使用的只有str
"""
if isinstance(string, unicode):
return string
# Be quiet by default
logger.debug("Decoding string with: %r" % string)
try:
return unicode(string, "UTF-8", errors)
except UnicodeDecodeError:
raise UnicodeDecodeError("Conversion to unicode failed: %r" % string)

你可以直接拷贝这部分代码, 把它当成基础库的一部分来使用, 确保程序中只有一种变量. 对于Python2 到 3的转换, 你几乎不需要修改一行代码, 同时, 程序中也不会 被encodedecode所淹没.

另外, 关于效率问题: 你都使用Python了, 还会在乎这么一点转换格式的效率吗, 这点效率的牺牲可以使得程序结构变得清晰的多, 我认为十分值得.

总结

这篇文章的本意并不是介绍编码问题的解决方案, 是希望大家阅读过后有自己对于程序 一致性的理解, 能够依照当前程序的具体代码来采取合适的方案, 一步步的优化迭代, 这才是我真正想表达的一些事情.

参考资料

String, Unicode and bytes in Python