杰瑞科技汇

Python2与Python3编码差异如何解决?

一切皆“字节”

首先要记住一个核心概念:计算机在底层存储和传输的所有数据,本质上都是二进制字节(bytes),编码(Encoding)和解码(Decoding)就是在这两种形式之间进行转换的桥梁。

Python2与Python3编码差异如何解决?-图1
(图片来源网络,侵删)
  • 字符串:是人类可读的文本,在 Python 2 和 Python 3 中,它的内部表示是不同的。
  • 字节:是计算机能处理的原始数据,比如一个文件、一段网络报文。

Python 2 的编码世界

Python 2 的设计存在一些历史遗留问题,导致处理编码时非常容易出错。

两种字符串类型

Python 2 中有两种字符串类型:

  • str:字节串,它直接存储原始的字节,不关心这些字节代表什么字符。'hello' 在 Python 2 中是一个 str 类型,它存储的是每个字符的 ASCII 码对应的字节。
  • unicode:Unicode 字符串,它存储的是抽象的字符,与具体的编码方式无关。u'你好' 就是一个 unicode 类型。
# Python 2
# str 类型,字节串
s = 'hello'
print type(s)  # <type 'str'>
print repr(s)  # 'hello' (在 ASCII 环境下,每个字符占一个字节)
# unicode 类型
u = u'你好'
print type(u)  # <type 'unicode'>
print repr(u)  # u'\u4f60\u597d' (存储的是字符的 Unicode 码点)

编码与解码

要在 strunicode 之间转换,就需要使用 编码解码

  • 编码 (Encode):将 unicode 字符串转换成 str 字节串。
    • unicode_string.encode(encoding)
  • 解码 (Decode):将 str 字节串转换成 unicode 字符串。
    • byte_string.decode(encoding)
# Python 2
# 假设源代码文件是 UTF-8 编写的
# -*- coding: utf-8 -*-
# 1. unicode -> str (编码)
u_str = u'你好'
# 将 unicode 字符串用 'utf-8' 编码成字节串
s_bytes = u_str.encode('utf-8')
print type(s_bytes)  # <type 'str'>
print repr(s_bytes)  # '\xe4\xbd\xa0\xe5\xa5\xbd' (这是 '你好' 的 UTF-8 编码后的字节)
# 2. str -> unicode (解码)
# 将字节串用 'utf-8' 解码成 unicode 字符串
u_str_back = s_bytes.decode('utf-8')
print type(u_str_back)  # <type 'unicode'>
print repr(u_str_back)  # u'\u4f60\u597d'

Python 2 的“隐式解码”陷阱

这是 Python 2 最坑的地方,当你混合使用 strunicode 时,Python 会尝试隐式地str 解码成 unicode

Python2与Python3编码差异如何解决?-图2
(图片来源网络,侵删)
# Python 2
# 假设 s_str 是一个 GBK 编码的 '你好'
s_str = '\xc4\xe3\xba\xc3' # '你好' 的 GBK 编码
u_str = u'hello'
# Python 尝试将 s_str 解码成 unicode,然后和 u_str 连接
# 它使用什么编码来解码?答案是系统的默认编码 (通常是 ASCII)
# 在非 ASCII 系统上,这会直接抛出 UnicodeDecodeError
# try:
#     result = u_str + s_str
# except UnicodeDecodeError as e:
#     print e # 'ascii' codec can't decode byte 0xc4 in position 0: ordinal not in range(128)
# 正确的做法是显式解码
s_str_unicode = s_str.decode('gbk')
result = u_str + s_str_unicode
print result # hello你好

这个隐式转换是 Python 2 中大部分乱码问题的根源。

文件 I/O

Python 2 打开文件时,默认是二进制模式,读出来的是 str(字节串),写进去也需要是 str

# Python 2
# 写入文件
# 需要显式地将 unicode 编码成 str 再写入
content = u'这是中文内容'
with open('test.txt', 'w') as f:
    f.write(content.encode('utf-8'))
# 读取文件
# 读出来的是 str,需要显式解码
with open('test.txt', 'r') as f:
    content_bytes = f.read()
    content_unicode = content_bytes.decode('utf-8')
    print content_unicode # 这是中文内容

Python 3 的编码世界

Python 3 对编码问题进行了彻底的改革,设计上更加清晰和一致。

两种字符串类型

Python 3 也有两种字符串类型,但含义和 Python 2 完全不同:

Python2与Python3编码差异如何解决?-图3
(图片来源网络,侵删)
  • str:Unicode 字符串,这是默认的字符串类型,用于表示文本。'hello' 在 Python 3 中是一个 str 类型,但它内部存储的是 Unicode 字符。
  • bytes:字节串,专门用来存储原始的字节数据,类似于 Python 2 的 str
# Python 3
# str 类型,Unicode 字符串
s = 'hello'
print(type(s))  # <class 'str'>
print(repr(s))  # 'hello'
# bytes 类型,字节串
b = b'hello'
print(type(b))  # <class 'bytes'>
print(repr(b))  # b'hello' (每个字符都是一个字节)

编码与解码

Python 3 的转换规则非常明确:

  • 编码 (Encode):将 str (Unicode) 转换成 bytes
    • string.encode(encoding)
  • 解码 (Decode):将 bytes 转换成 str (Unicode)。
    • bytes_object.decode(encoding)
# Python 3
# 1. str -> bytes (编码)
s_str = '你好'
# 将 unicode 字符串用 'utf-8' 编码成字节串
b_bytes = s_str.encode('utf-8')
print(type(b_bytes))  # <class 'bytes'>
print(repr(b_bytes))  # b'\xe4\xbd\xa0\xe5\xa5\xbd'
# 2. bytes -> str (解码)
# 将字节串用 'utf-8' 解码成 unicode 字符串
s_str_back = b_bytes.decode('utf-8')
print(type(s_str_back))  # <class 'str'>
print(repr(s_str_back))  # '你好'

关键区别:在 Python 3 中,strbytes完全不能直接拼接的,这从源头上杜绝了 Python 2 的隐式解码陷阱。

# Python 3
s_str = 'hello'
b_bytes = b'world'
# 下面这行代码会直接报 TypeError,非常清晰
# result = s_str + b_bytes
# TypeError: can only concatenate str (not "bytes") to str

文件 I/O

Python 3 在文件 I/O 上做了巨大改进,引入了 io 模块,并明确区分了文本模式和二进制模式。

  • 文本模式 ('r', 'w')

    • 读取:从文件中读取字节,然后自动解码str (Unicode) 返回。
    • 写入:将 str (Unicode) 自动编码成字节后写入文件。
    • 你可以通过 encoding 参数指定编码(强烈推荐!默认是系统编码,可能出问题)。
  • 二进制模式 ('rb', 'wb')

    • 读取:从文件中读取原始字节,直接返回 bytes 对象,不做任何解码。
    • 写入:将 bytes 对象直接写入文件,不做任何编码。
# Python 3
# 写入文件 (文本模式)
# 自动编码
content = '这是中文内容'
with open('test_py3.txt', 'w', encoding='utf-8') as f:
    f.write(content)
# 读取文件 (文本模式)
# 自动解码
with open('test_py3.txt', 'r', encoding='utf-8') as f:
    content_str = f.read()
    print(type(content_str)) # <class 'str'>
    print(content_str)      # 这是中文内容
# 写入文件 (二进制模式)
# 需要手动编码
content_bytes = content.encode('utf-8')
with open('test_py3.bin', 'wb') as f:
    f.write(content_bytes)
# 读取文件 (二进制模式)
# 需要手动解码
with open('test_py3.bin', 'rb') as f:
    content_bytes_read = f.read()
    print(type(content_bytes_read)) # <class 'bytes'>
    content_str_back = content_bytes_read.decode('utf-8')
    print(content_str_back)         # 这是中文内容

总结与最佳实践

特性 Python 2 Python 3
默认字符串 str (字节串) str (Unicode 字符串)
字节串 str bytes
Unicode 字符串 unicode str
核心转换 unicode.encode() -> str
str.decode() -> unicode
str.encode() -> bytes
bytes.decode() -> str
混合类型操作 危险! 会尝试隐式解码,容易出错。 安全! 直接 TypeError,强制你进行显式转换。
文件读写 默认二进制模式,需手动编解码。 文本模式:自动编解码,需指定 encoding
二进制模式 ('rb'/'wb'):直接读写 bytes

最佳实践

  1. 明确区分文本和二进制

    • 处理文本内容(如日志、配置、网页内容)时,始终使用 str (Python 3) 或 unicode (Python 2)。
    • 处理原始数据(如图片、音频、网络包、文件二进制内容)时,始终使用 bytes (Python 3) 或 str (Python 2)。
  2. 尽早解码,尽可能晚编码

    • 在程序的入口(如读取文件、网络请求),尽早将输入的字节流解码成 str (Unicode)。
    • 在程序的出口(如写入文件、发送网络请求),尽可能晚地将 str (Unicode) 编码成字节流。
  3. 统一内部编码

    • 让你的整个应用程序在内部都使用 str (Python 3) 或 unicode (Python 2) 来处理所有文本数据,只在与外界(文件、网络、数据库)交互时,才进行编解码。
  4. 指定编码,不要依赖默认值

    • 在打开文件、网络请求等所有地方,显式地指定编码,通常是 'utf-8'open(file, 'r', encoding='utf-8')
  5. 拥抱 Python 3

    Python 3 的设计从根本上解决了 Python 2 的编码混乱问题,如果你是新项目,请毫不犹豫地使用 Python 3,如果你维护 Python 2 代码,应尽快规划迁移到 Python 3。

分享:
扫描分享到社交APP
上一篇
下一篇