字符编码问题是经常困扰python编程人员的问题,我在编写爬虫的过程中也经常遇到这个头疼的事。
从python3开始,明确区分了人类语言(文本字符串)和机器语言(二进制字节),咱们先说文本字符串
开始之前,得对"字符"进行定义:
字符:Unicode字符,从python3的str对象中获取的元素是Unicode字符
字符串:字符串就是一个字符序列(这里对于(一)中内容相呼应)
if
__name__
==
"
__main__
"
:
#
创建字符
s1 = str(
'
a
'
)
s2
=
'
b
'
s3
= u
'
c
'
print
(s1, s2, s3)
#
a b c
python3中内置有两种基本的二进制序列类型:不可变的bytes和可变bytearray
(1)bytes和bytearray的各个元素是介于0~255(8个bit)之间的整数;
(2)二进制序列的切片始终是同一类型的二进制序列
if
__name__
==
"
__main__
"
:
#
创建bytes 和 bytearray
b1 = bytes(
'
abc你好
'
, encoding=
'
utf8
'
)
#
关于encode稍后会说,不知道有没有人和我一样总是将编码与解码的方向混淆
print
(b1)
#
b'abc\xe4\xbd\xa0\xe5\xa5\xbd'
b2 = bytearray(
'
abc你好
'
, encoding=
'
utf8
'
)
print
(b2)
#
bytearray(b'abc\xe4\xbd\xa0\xe5\xa5\xbd')
#
切片(提示:序列都可以切片)
print
(b1[3:5])
#
b'\xe4\xbd'
print
(b2[3:5])
#
bytearray(b'\xe4\xbd')
#
使用列表取值的方法试试
print
(b1[3])
#
228 此时取出来的就不是字节序列了,而是一个元素
for
_
in
b1:
print
(_, end=
'
,
'
)
#
97,98,99,228,189,160,229,165,189, 这都是8bit的整数
#
bytes的不可变 vs. bytearray的可变
#
b1[3] = 160 # 报错:'bytes' object does not support item assignment
print
(id(b2), b2)
#
4373768376 bytearray(b'abc\xe4\xbd\xa0\xe5\xa5\xbd')
b2[2] = 78
print
(id(b2), b2)
#
4373768376 bytearray(b'abN\xe4\xbd\xa0\xe5\xa5\xbd')
#
将b2转换成字符串看看
print
(b2.decode(
'
utf8
'
))
#
abN你好
#
注意,这里之所以能够用utf8转成unicode,是因为N的ascii码和utf8一致
b2.extend(bytearray(
'
添加的内容
'
, encoding=
'
utf8
'
))
#
既然是可变序列,bytearray当然拥有一般的序列的方法
print
(id(b2), b2)
#
4373768376 bytearray(b'abN\xe4\xbd\xa0\xe5\xa5\xbd\xe6\xb7\xbb\xe5\x8a\xa0\xe7\x9a\x84\xe5\x86\x85\xe5\xae\xb9')
print
(b2.decode(
'
utf8
'
))
#
abN你好添加的内容
#
PS:大家可以将二进制序列当成列表,元素就是ascii编码(0~255)
struct可以从二进制序列中提取结构化信息。
struct模块提供了一些函数,可以将打包的字节序列转换成不同类型字段组成的元组;还有一些函数用于执行反向转换。
struct模块可以处理bytes、bytearray、memoryview对象。
import
struct
if
__name__
==
"
__main__
"
:
#
memoryview类用于共享内存,可以访问其他二进制序列、打包的数组和缓冲中的数据切片,该操作无需赋值字节序列
fmt =
'
<3s3sHH
'
#
设置格式,< 是小字节序,3s3s是两个3字节序列,HH是两个16位二进制整数
with open(
'
L3_图_python.jpg
'
,
'
rb
'
) as f: # 需要在github中下载后运行
img
=
memoryview(f.read())
print
(bytes(img[:10]))
#
b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x02\x00\x1c\x00\x1c\x00\x00'
print
(struct.unpack(fmt, img[:10]))
#
(b'\xff\xd8\xff', b'\xe0\x00\x10', 17994, 17993) :拆包
del
img
python自带有超过100中编解码器,用于在字符串和字节之间相互转换。
每个编码都有多个名称,例如'utf_8'、'utf8'、'utf-8'、'U8',这些都可以传递给open()、str.encode()、bytes.decode()中的
encoding参数
if
__name__
==
"
__main__
"
:
#
看看不同的编码效果
for
codec
in
[
'
gbk
'
,
'
utf8
'
,
'
utf16
'
]:
print
(codec,
"
你好
"
.encode(codec), sep=
'
\t
'
)
gbk b'\xc4\xe3\xba\xc3'
utf8 b'\xe4\xbd\xa0\xe5\xa5\xbd'
utf16 b'\xff\xfe`O}Y'
#
咱们再来解码
print
(b
'
\xc4\xe3\xba\xc3
'
.decode(
'
gbk
'
))
#
你好
print
(b
'
\xe4\xbd\xa0\xe5\xa5\xbd
'
.decode(
'
utf8
'
))
#
你好
print
(b
'
\xff\xfe`O}Y
'
.decode(
'
utf16
'
))
#
你好
#
(1)UnicodeEncodeError
#
使用errors参数
s1 =
"
hello,你长胖啦
"
.encode(
'
latin-1
'
, errors=
'
ignore
'
)
print
(s1)
#
b'hello' 使用 errors='ignore' 忽略了无法编码的字符
s2 =
"
hello,你长胖啦
"
.encode(
'
latin-1
'
, errors=
'
replace
'
)
print
(s2)
#
b'hello?????' 使用errors='replace'将无法编码的字符用问好代替
s3 =
"
hello,你长胖啦
"
.encode(
'
latin-1
'
, errors=
'
xmlcharrefreplace
'
)
print
(s3)
#
b'hello,你长胖啦' 使用errors='xmlcharrefreplace'将无法编码的内容替换成XML实体
#
(2) UnicodeDecodeError
#
乱码字符称为鬼符,以下实例演示出现鬼符的情况
s4 = b
'
Montr\xe9al
'
print
(s4.decode(
'
cp1252
'
))
#
Montréal
print
(s4.decode(
'
iso8859_7
'
))
#
Montrιal
print
(s4.decode(
'
koi8_r
'
))
#
MontrИal
#
print(s4.decode('utf8')) # 报错:UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte
print
(s4.decode(
'
utf8
'
, errors=
'
replace
'
))
#
Montr�al
for
expression
in
expressions.split():
value
=
eval(expression)
print
(expression.rjust(30),
'
->
'
, repr(value))
我电脑运行结果如下:
(' locale.getpreferredencoding()', '->', "'UTF-8'")
(' type(my_file)', '->', "<type 'file'>")
(' my_file.encoding', '->', 'None')
(' sys.stdout.isatty()', '->', 'True')
(' sys.stdout.encoding', '->', "'UTF-8'")
(' sys.stdin.isatty()', '->', 'True')
(' sys.stdin.encoding', '->', "'UTF-8'")
(' sys.stderr.isatty()', '->', 'True')
(' sys.stderr.encoding', '->', "'UTF-8'")
(' sys.getdefaultencoding()', '->', "'ascii'")
(' sys.getfilesystemencoding()', '->', "'utf-8'")
我们发现café可以用'café'和'cafe\u0301'两种方式表示,这个词对于人来说是一样的,但是这两种表示对于计算机来说确实不一样的
向这样的序列叫"标准等价物",在计算机中存储的值不相等,但应用程序应该认为相等。
要解决这个问题,需要用到unicodedata.nomalize函数,它的第一个参数可以选择这四种形式的一个:"NFC"、"NFD"和"NFKC"、"NFKD"
"NFC":使用最少的码为构成等价的字符串
"NFD":把组合的字符分割成基本字符和单独的组合字符
"NFKC"&"NFKD":这两种是较严格的规范形式,对"兼容字符有影响"
from
unicodedata
import
normalize
if
__name__
==
"
__main__
"
:
#
"NFC" & "NFD"
print
(s1.encode(
'
utf8
'
), s2.encode(
'
utf8
'
))
#
b'caf\xc3\xa9' b'cafe\xcc\x81'
s1 = normalize(
"
NFC
"
, s1)
s2
= normalize(
"
NFC
"
, s2)
print
(s1, s2)
#
café café
print
(s1 == s2)
#
True
print
(s1.encode(
'
utf8
'
), s2.encode(
'
utf8
'
))
#
b'caf\xc3\xa9' b'caf\xc3\xa9'
#
"NFKC"&"NFKD"
#
这两种方式会损失信息,所以不建议使用,除非一些特殊情况,比如搜索和索引中
s3 =
'
½
'
print
(normalize(
'
NFKC
'
, s3))
#
1⁄2 将½转换成了1⁄2
s4 =
'
™
'
print
(normalize(
'
NFKC
'
, s4))
#
TM 将™转换成了TM
另外,如果比较的时候不区分大小写,建议使用str.casefold(), 它与lower基本一致,其中大约有116个特殊的字符结果不同
s5
=
'
AKJkakshfKHDSdshfKSDShKkHkjhKJkgJhgJHkkHkjhJKhKJhK
'
print
(s5.casefold())
#
akjkakshfkhdsdshfksdshkkhkjhkjkgjhgjhkkhkjhjkhkjhk
python比较序列时,会一一比较其中的元素。对于字符来说,比较的是其码位,主要是比较的ascii码;
非ascii文本的标准排序方式是使用locale.strxfrm函数,但是使用这个函数必须事先设定区域,但有些操作系统不支持,并且改变区域设置并不十分合适
建议使用pyuca.Collator.sort_key方法进行排序
if
__name__
==
"
__main__
"
:
#
python默认的排序
fruits = [
'
caju
'
,
'
atemoia
'
,
'
cajá
'
,
'
açaí
'
,
'
acerola
'
]
print
(sorted(fruits))
#
['acerola', 'atemoia', 'açaí', 'caju', 'cajá']
#
但正确排序应该是:['açaí','acerola', 'atemoia', 'cajá', 'caju']
#
使用pyuca.Collator.sort_key
import
pyuca
print
(sorted(fruits, key = pyuca.Collator().sort_key))
#
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
#
pyuca可以将自定义排序表路径传递给Collator()构造方法,pyuca默认使用自带的allkeys.txt