文章目录
0.摘要
本文主要讲解
P
y
t
h
o
n
3
Python3
Python3中的字符串以及一些编码问题。在此我可以先告诉你一个结论:字符串编码得到字节序列;字节序列解码得到字符串。想了解更多内容可以阅读本文。
强烈建议看原文章:https://woodpecker.org.cn/diveintopython3/strings.html。
再推荐一个字符集相关的视频(科普向)。
1.在开始之前需要掌握的一些知识
当人们说起“文本”,他们通常指显示在屏幕上的字符或者其他的记号;但是计算机不能直接处理这些字符和标记;它们只认识位(
b
i
t
bit
bit)和字节(
b
y
t
e
byte
byte)。实际上,从屏幕上的每一块文本都是以某种字符编码(
c
h
a
r
a
c
t
e
r
e
n
c
o
d
i
n
g
character\ encoding
character encoding)的方式保存的。粗略地说就是,字符编码提供一种映射,使屏幕上显示的内容和内存、磁盘内存储的内容对应起来。有许多种不同的字符编码,有一些是为特定的语言,比如俄语、中文或者英语,设计、优化的,另外一些则可以用于多种语言的编码。
在实际操作中则会比上边描述的更复杂一些。许多字符在几种编码里是共用的,但是在实际的内存或者磁盘上,不同的编码方式可能会使用不同的字节序列来存储他们。所以,你可以把字符编码当做一种解码密钥。当有人给你一个字节序列 — 文件,网页,或者别的什么 — 并且告诉你它们是“文本”时,就需要知道他们使用了何种编码方式,然后才能将这些字节序列解码成字符。如果他们给的是错误的“密钥”或者根本没有给你“密钥”,那就得自己来破解这段编码,这可是一个艰难的任务。有可能你使用了错误的解码方式,然后出现一些莫名其妙的结果。
现在,我们考虑一下像
e
m
a
i
l
email
email和
w
e
b
web
web这样的全球网络的出现。大量的“纯文本”文件在全球范围内流转,它们在一台电脑上被撰写出来,通过第二台电脑进行传输,最后在另外一台电脑上显示。计算机只能识别数字,但是这些数字可能表达的是其他的东西。怎么办呢?好吧,那么系统必须被设计成在每一段“纯文本”上都搭载编码信息。记住,编码方式是将计算机可读的数字映射成人类可读的字符的解码密钥。失去解码密钥则意味着混乱不清的,莫名其妙的信息,或者更糟。
2.Unicode
U n i c o d e Unicode Unicode编码系统为表达任意语言的任意字符而设计(注意它只是一个编码系统,并不是某种编码方法)。它使用4字节的数字来表达每个字母、符号,或者表意文字。每个数字代表唯一的至少在某种语言中使用的符号。(并不是所有的数字都用上了,但是总数已经超过了65535,所以2个字节的数字是不够用的)被几种语言共用的字符通常使用相同的数字来编码,除非存在一个在理的语源学(etymological)理由使不这样做。不考虑这种情况的话,每个字符对应一个数字,每个数字对应一个字符。即不存在二义性。不再需要记录“模式”了。 U + 0041 U_{+0041} U+0041总是代表 A A A,即使这种语言没有 A A A这个字符。但是 U n i c o d e Unicode Unicode仅仅定义了每个字符的编号,并没有定义如何将这个编号进行存储的方式,所以后来出现了 u t f − 8 utf-8 utf−8, g b k gbk gbk等编码格式, 它们都是 U n i c o d e Unicode Unicode 的一种实现方式, 仍然使用了 U n i c o d e Unicode Unicode 中的唯一编号, 个人对其的简单理解就是在 U n i c o d e Unicode Unicode编码的基础之上又定义了对字符的存储方式。
2.1UTF-32
有一种 U n i c o d e Unicode Unicode编码方式每1个字符使用4个字节。它叫做 U T F − 32 UTF-32 UTF−32,因为32位 = 4字节。 U T F − 32 UTF-32 UTF−32是一种直观的编码方式;它收录每一个 U n i c o d e Unicode Unicode字符(4字节数字)然后就以那个数字代表该字符。这种方法有其优点,最重要的一点就是可以在常数时间内定位字符串里的第 N N N个字符,因为第 N N N个字符从第 4 × N t h 4×Nth 4×Nth个字节开始。另外,它也有其缺点,最明显的就是空间浪费。
2.2UTF-16
尽管有
U
n
i
c
o
d
e
Unicode
Unicode字符非常多,但是实际上大多数人不会用到超过前65535个以外的字符。因此,就有了另外一种
U
n
i
c
o
d
e
Unicode
Unicode编码方式,叫做
U
T
F
−
16
UTF-16
UTF−16(因为16位 = 2字节)。
U
T
F
−
16
UTF-16
UTF−16将0–65535范围内的字符编码成2个字节,如果真的需要表达那些很少使用的超过这65535范围的
U
n
i
c
o
d
e
Unicode
Unicode字符,则需要使用一些技巧来实现。
U
T
F
−
16
UTF-16
UTF−16编码最明显的优点是它在空间效率上比
U
T
F
−
32
UTF-32
UTF−32高两倍,因为每个字符只需要2个字节来存储(除去65535范围以外的),而不是
U
T
F
−
32
UTF-32
UTF−32中的4个字节。并且,如果我们假设某个字符串不包含任何超出65535范围的字符,那么我们依然可以在常数时间内找到其中的第N个字符,直到它不成立为止。这总是一个不错的推断…
但是对于
U
T
F
−
32
UTF-32
UTF−32和
U
T
F
−
16
UTF-16
UTF−16编码方式还有一些其他不明显的缺点。不同的计算机系统会以不同的顺序保存字节。这意味着字符
U
+
4
E
2
D
U_{+4E2D}
U+4E2D在
U
T
F
−
16
UTF-16
UTF−16编码方式下可能被保存为
4
E
2
D
4E\ 2D
4E 2D或者
2
D
4
E
2D\ 4E
2D 4E,这取决于该系统使用的是大尾端还是小尾端。(对于
U
T
F
−
32
UTF-32
UTF−32编码方式,则有更多种可能的字节排列)只要文档没有离开你的计算机,它还是安全的 — 同一台电脑上的不同程序使用相同的字节顺序。但是当我们需要在系统之间传输这个文档的时候,也许在万维网中,我们就需要一种方法来指示当前我们的字节是怎样存储的。不然的话,接收文档的计算机就无法知道这两个字节
4
E
2
D
4E\ 2D
4E 2D表达的到底是
U
+
4
E
2
D
U_{+4E2D}
U+4E2D还是
U
+
2
D
4
E
U_{+2D4E}
U+2D4E。
为了解决这个问题,多字节的
U
n
i
c
o
d
e
Unicode
Unicode编码方式定义了一个“字节顺序标记(Byte Order Mark)”,它是一个特殊的非打印字符,你可以把它包含在文档的开头来指示你所使用的字节顺序。对于
U
T
F
−
16
UTF-16
UTF−16,字节顺序标记是
U
+
F
E
F
F
U_{+FEFF}
U+FEFF。
不过,
U
T
F
−
16
UTF-16
UTF−16还不够完美,特别是要处理许多
a
s
c
i
i
ascii
ascii字符时。如果仔细想想的话,甚至一个中文网页也会包含许多的
a
s
c
i
i
ascii
ascii字符 — 所有包围在可打印中文字符周围的元素(
e
l
e
m
e
n
t
element
element)和属性(
a
t
t
r
i
b
u
t
e
attribute
attribute)。能够在常数时间内找到第
N
N
N个字符当然非常好,但是依然存在着纠缠不休的罕见字符的问题,这意味着你不能保证每个字符都是2个字节长,所以,除非你维护着另外一个索引,不然就不能真正意义上的在常数时间内定位第
N
N
N个字符。另外,朋友,世界上肯定还存在很多的
a
s
c
i
i
ascii
ascii文本…
2.3UTF-8
U
T
F
−
8
UTF-8
UTF−8是一种为
U
n
i
c
o
d
e
Unicode
Unicode设计的变长(variable-length)编码系统。即不同的字符可使用不同数量的字节编码。对于
a
s
c
i
i
ascii
ascii字符,
U
T
F
−
8
UTF-8
UTF−8仅使用1个字节来编码。事实上,
U
T
F
−
8
UTF-8
UTF−8中前128个字符(0–127)使用的是跟
a
s
c
i
i
ascii
ascii一样的编码方式。像
n
~
ñ
n~和
o
¨
ö
o¨这样的“扩展拉丁字符(Extended Latin)”则使用2个字节来编码。(这里的字节并不是像
U
T
F
−
16
UTF-16
UTF−16中那样简单的
U
n
i
c
o
d
e
Unicode
Unicode编码点(
U
n
i
c
o
d
e
c
o
d
e
p
o
i
n
t
Unicode\ code\ point
Unicode code point);它使用了一些位变换)中文字符比如“中”则占用了3个字节,很少使用的字符则占用4个字节。
缺点:因为每个字符使用不同数量的字节编码,所以寻找串中第
N
N
N个字符是一个
O
(
N
)
O(N)
O(N)复杂度的操作 — 即,串越长,则需要更多的时间来定位特定的字符。同时,还需要位变换来把字符编码成字节,把字节解码成字符。
优点:在处理经常会用到的
a
s
c
i
i
ascii
ascii字符方面非常有效。在处理扩展的拉丁字符集方面也不比
U
T
F
−
16
UTF-16
UTF−16差。对于中文字符来说,比
U
T
F
−
32
UTF-32
UTF−32要好。同时,(在这一条上你得相信我,因为我不打算给你展示它的数学原理)由位操作的天性使然,使用
U
T
F
−
8
UTF-8
UTF−8不再存在字节顺序的问题了。一份以
U
T
F
−
8
UTF-8
UTF−8编码的文档在不同的计算机之间是一样的比特流。
3.概述
在
P
y
t
h
o
n
3
Python 3
Python3,所有的字符串都是使用
U
n
i
c
o
d
e
Unicode
Unicode编码的字符序列。不再存在以
U
T
F
−
8
UTF-8
UTF−8或者
C
P
−
1252
CP-1252
CP−1252编码的情况。也就是说,“这个字符串是以
u
t
f
−
8
utf-8
utf−8编码的吗?不再是一个有效问题。”
u
t
f
−
8
utf-8
utf−8是一种将字符编码成字节序列的方式。如果需要将字符串转换成特定编码的字节序列,
P
y
t
h
o
n
3
Python 3
Python3可以为你做到。如果需要将一个字节序列转换成字符串,
P
y
t
h
o
n
3
Python 3
Python3也能为你做到。字节即字节,并非字符。字符在计算机内只是一种抽象。字符串则是一种抽象的序列。
为了创建一个字符串,将其用引号包围,单双引号均可,配套使用就行。
内置函数
l
e
n
(
)
len()
len()可返回字符串的长度,即字符的个数。这与获得列表,元组,集合或者字典的长度的函数是同一个。
P
y
t
h
o
n
Python
Python中,字符串可以想像成由字符组成的元组。
与取得列表中的元素一样,也可以通过下标记号取得字符串中的某个字符;也可以使用
+
+
+操作符来连接字符串。
4.格式化字符串
K
B
、
M
B
、
G
B
KB、MB、GB
KB、MB、GB…… 这些是字符串;函数的文档字符串(
d
o
c
s
t
r
i
n
g
docstring
docstring)也是字符串。当前的文档字符串占用了多行,所以它使用了相邻的3个引号来标记字符串的起始和终止。
注意第五个地方,它是我们即将介绍的格式化字符串。
这里使用了一个字符串字面值的方法调用。字符串也是对象,对象则有其方法。其次,整个表达式返回一个字符串。最后,
{
0
}
\{0\}
{0}和
{
1
}
\{1\}
{1} 叫做替换字段(replacement field),他们会被传递给
f
o
r
m
a
t
(
)
format()
format()方法的参数替换。
整型替换字段被当做传给
f
o
r
m
a
t
(
)
format()
format()方法的参数列表的位置索引;
{
0
}
\{0\}
{0}和
{
1
}
\{1\}
{1}分别会被第一、二个参数替换;其它的以此类推。
从
P
y
t
h
o
n
3.1
Python 3.1
Python3.1 起,在格式化标示符中使用位置索引时可以忽略数字。也就是说,无需使用格式化标示符
{
0
}
\{0\}
{0} 来指向
f
o
r
m
a
t
(
)
format()
format() 方法的第一个参数,只需简单地使用
{
}
\{\}
{} 而
P
y
t
h
o
n
Python
Python 将会填入正确的位置索引。该规则适用于任何数量的参数;第一个
{
}
\{\}
{} 代表
{
0
}
\{0\}
{0},第二个
{
}
\{\}
{} 代表
{
1
}
\{1\}
{1},以此类推。
4.1复合字段名
{
0
}
\{0\}
{0}代表传递给
f
o
r
m
a
t
(
)
format()
format()方法的第一个参数,即
s
i
_
s
u
f
f
i
x
e
s
si\_suffixes
si_suffixes。注意
s
i
_
s
u
f
f
i
x
e
s
si\_suffixes
si_suffixes是一个列表。所以
{
0
[
0
]
}
\{0[0]\}
{0[0]}指代
s
i
_
s
u
f
f
i
x
e
s
si\_suffixes
si_suffixes的第一个元素,即
K
B
KB
KB。同时,
{
0
[
1
]
}
\{0[1]\}
{0[1]}指代该列表的第二个元素,即
M
B
MB
MB。大括号以外的内容 — 包括1000,等号,还有空格等 — 则按原样输出。
这个例子说明格式说明符可以通过利用(类似)
P
y
t
h
o
n
Python
Python的语法访问到对象的元素或属性。这就叫做复合字段名(
c
o
m
p
o
u
n
d
f
i
e
l
d
n
a
m
e
s
compound field names
compoundfieldnames)。以下复合字段名都是有效的:
s
y
s
sys
sys模块保存了当前正在运行的
P
y
t
h
o
n
Python
Python实例的信息。由于已经导入了这个模块,因此可以将其作为
f
o
r
m
a
t
(
)
format()
format()方法的参数。所以替换域
{
0
}
\{0\}
{0}指代
s
y
s
sys
sys模块。
s
y
s
.
m
o
d
u
l
e
s
sys.modules
sys.modules是一个保存当前Python实例中所有已经导入模块的字典。模块的名字作为字典的键;模块自身则是键所对应的值。所以
{
0.
m
o
d
u
l
e
s
}
\{0.modules\}
{0.modules}指代保存当前己被导入模块的字典。
s
y
s
.
m
o
d
u
l
e
s
[
′
h
u
m
a
n
s
i
z
e
′
]
sys.modules['humansize']
sys.modules[′humansize′]即刚才导入的
h
u
m
a
n
s
i
z
e
humansize
humansize模块。所以替换域
0.
m
o
d
u
l
e
s
[
h
u
m
a
n
s
i
z
e
]
{0.modules[humansize]}
0.modules[humansize]指代
h
u
m
a
n
s
i
z
e
humansize
humansize模块。请注意以上两句在语法上轻微的不同。在实际的Python代码中,字典
s
y
s
.
m
o
d
u
l
e
s
sys.modules
sys.modules的键是字符串类型的;为了引用它们,我们需要在模块名周围放上引号(比如
′
h
u
m
a
n
s
i
z
e
′
'humansize'
′humansize′)。但是在使用替换域的时候,我们在省略了字典的键名周围的引号(比如
h
u
m
a
n
s
i
z
e
humansize
humansize)。在此,我们引用PEP 3101:字符串格式化高级用法,“解析键名的规则非常简单。如果名字以数字开头,则它被当作数字使用,其他情况则被认为是字符串。”
4.2格式说明符
{
1
}
\{1\}
{1}会被传递给
f
o
r
m
a
t
(
)
format()
format()方法的第二个参数替换,即
s
u
f
f
i
x
suffix
suffix。但是
{
0
:
.
1
f
}
\{0:.1f\}
{0:.1f}是什么意思呢?它其实包含了两方面的内容:
{
0
}
\{0\}
{0}你已经能理解,
:
.
1
f
:.1f
:.1f则不一定了。第二部分(包括冒号及其后边的部分)是格式说明符(
f
o
r
m
a
t
s
p
e
c
i
f
i
e
r
format specifier
formatspecifier),它进一步定义了被替换的变量应该如何被格式化。
格式说明符允许你使用各种实用的方法来修饰被替换的文本,就像
C
C
C语言中的
p
r
i
n
t
f
(
)
printf()
printf()函数一样。我们可以添加使用零填充(
z
e
r
o
−
p
a
d
d
i
n
g
zero-padding
zero−padding),衬距(
s
p
a
c
e
−
p
a
d
d
i
n
g
space-padding
space−padding),对齐字符串(
a
l
i
g
n
s
t
r
i
n
g
s
align\ strings
align strings),控制10进制数输出精度,甚至将数字转换成16进制数输出。
在替换域中,冒号标示格式说明符的开始。
“
.
1
”
“.1”
“.1”的意思是四舍五入到保留一位小数点。
“
f
”
“f”
“f”的意思是定点数(与指数标记法或者其他10进制数表示方法相对应)。
想了解格式说明符的复杂细节,请参阅Python官方文档关于格式化规范的迷你语言。
5.其他常用字符串方法
s
p
l
i
t
l
i
n
e
s
(
)
splitlines()
splitlines()方法以多行字符串作为输入,返回一个由字符串组成的列表,列表的元素即原来的单行字符串。请注意,每行行末的回车符没有被包括进去。
l
o
w
e
r
(
)
lower()
lower()方法把整个字符串转换成小写的。(类似地,
u
p
p
e
r
(
)
upper()
upper()方法执行大写化转换操作)
c
o
u
n
t
(
)
count()
count()方法对串中的指定的子串进行计数。是的,在那一句中确实出现了6个字母
f
f
f。
s p l i t ( ) split() split()通过指定分隔符对字符串进行切片,如果第二个参数 n u m num num有指定值,则分割为 n u m + 1 num+1 num+1 个子字符串。
字符串可以像列表一样进行切片。
6.String vs. Bytes
使用
b
y
t
e
byte
byte字面值语法
b
b
b来定义
b
y
t
e
s
bytes
bytes对象。
b
y
t
e
byte
byte字面值里的每个字节可以是
a
s
c
i
i
ascii
ascii字符或者是从\x00到\xff编码了的16进制数。
b
y
t
e
s
bytes
bytes对象的类型是
b
y
t
e
s
bytes
bytes。
跟列表和字符串一样,我们可以通过内置函数
l
e
n
(
)
len()
len()来获得
b
y
t
e
s
bytes
bytes对象的长度,也可以使用
+
+
+运算符连接两个
b
y
t
e
s
bytes
bytes对象。
一如列表和字符串,可以使用下标记号来获取
b
y
t
e
s
bytes
bytes对象中的单个字节。对字符串做这种操作获得的元素仍为字符串,而对
b
y
t
e
s
bytes
bytes对象做这种操作的返回值则为整数。确切地说,是
0
–
255
0–255
0–255之间的整数。
b
y
t
e
s
bytes
bytes对象是不可变的;我们不可以给单个字节赋上新值。如果需要改变某个字节,可以组合使用字符串的切片和连接操作(效果跟字符串是一样的),或者我们也可以将
b
y
t
e
s
bytes
bytes对象转换为
b
y
t
e
a
r
r
a
y
bytearray
bytearray对象。
使用内置函数
b
y
t
e
a
r
r
a
y
(
)
bytearray()
bytearray()来完成从
b
y
t
e
s
bytes
bytes对象到可变的
b
y
t
e
a
r
r
a
y
bytearray
bytearray对象的转换。所有对
b
y
t
e
s
bytes
bytes对象的操作也可以用在
b
y
t
e
a
r
r
a
y
bytearray
bytearray对象上,不过我们可以使用下标标记给
b
y
t
e
a
r
r
a
y
bytearray
bytearray对象的某个字节赋值,这个值必须是
0
−
255
0-255
0−255之间的一个整数。
不能连接
b
y
t
e
s
bytes
bytes对象和字符串对象,他们是两种不同的数据类型。也不允许针对字符串中
b
y
t
e
s
bytes
bytes对象的出现次数进行计数,因为串里面根本没有
b
y
t
e
s
bytes
bytes。
字符串是字符序列。也许你是想要先把这些字节序列通过某种编码方式进行解码获得字符串,然后对该字符串进行计数?可以,但是需要显式地指明它。
P
y
t
h
o
n
3
Python 3
Python3不会隐含地将
b
y
t
e
s
bytes
bytes转换成字符串,反之亦然。
至此你应该清楚
b
y
t
e
s
bytes
bytes和字符串对象的关系了。各种语言(英文、中文等)的字符串—编码—得到
b
y
t
e
s
bytes
bytes对象;
b
y
t
e
s
bytes
bytes对象—解码—得到对应的字符串。
b
y
t
e
s
bytes
bytes对象有一个
d
e
c
o
d
e
(
)
decode()
decode()方法,它使用某种字符编码(比如
u
t
f
−
8
utf-8
utf−8)作为参数,然后依照这种编码方式将
b
y
t
e
s
bytes
bytes对象转换为字符串;对应地,字符串有一个
e
n
c
o
d
e
(
)
encode()
encode()方法,它也使用某种字符编码作为参数,然后依照它将串转换为
b
y
t
e
s
bytes
bytes对象。在上一个例子中,解码的过程相对直观一些 — 使用
a
s
c
i
i
ascii
ascii编码将一个字节序列转换为字符串。同样的过程对其他的编码方式依然有效 — 传统的(非
U
n
i
c
o
d
e
Unicode
Unicode)编码方式也可以,只要它们能够编码串中的所有字符。
a
_
s
t
r
i
n
g
a\_string
a_string是一个字符串,它有9个字符。
b
y
by
by是一个
b
y
t
e
s
bytes
bytes对象,它有13个字节。它是通过
a
_
s
t
r
i
n
g
a\_string
a_string使用
u
t
f
−
8
utf-8
utf−8编码而得到的一串字节序列。
b
y
by
by还是一个
b
y
t
e
s
bytes
bytes对象,它有11个字节。它是通过
a
_
s
t
r
i
n
g
a\_string
a_string使用
G
B
18030
GB18030
GB18030编码而得到的一串字节序列。
此时的
b
y
by
by仍旧是一个
b
y
t
e
s
bytes
bytes对象,由11个字节组成。它又是一种完全不同的字节序列,我们通过对
a
_
s
t
r
i
n
g
a\_string
a_string使用
B
i
g
5
Big5
Big5编码得到。
r
o
u
n
d
t
r
i
p
roundtrip
roundtrip是一个字符串,共有9个字符。它是通过对
b
y
by
by使用
B
i
g
5
Big5
Big5解码算法得到的一个字符序列。并且,从执行结果可以看出,
r
o
u
n
d
t
r
i
p
roundtrip
roundtrip与
a
_
s
t
r
i
n
g
a\_string
a_string是完全一样的。
7.补充内容:Python源码的编码方式
8.蛋疼的python2
py2中的字符串编码是真滴蛋疼。。主要感觉是
s
t
r
str
str包含的内容太多了,你不知道它到底是以何种方式编码的字节流。
举个例子(windos下pycharm终端):
通过
U
n
i
c
o
d
e
Unicode
Unicode字符搜索网站可以知道在
U
n
i
c
o
d
e
Unicode
Unicode编码系统中,
4
F
60
4F60
4F60代表中文的“你”,
597
D
597D
597D代表中午的“好”。这和变量
b
b
b可以很好的对应上,于是乎我们可以对变量
b
b
b进行以下操作将其转换为字节流,对端再通过相同的编码方式进行解码即可还原出
b
b
b:
如果编码方式和解码方式不一致,或者编码系统不匹配,就有可能遇到各种错误:
那么问题来了,
a
a
a到底是以什么格式编码的呢?可能是windows平台的原因,它是以
g
b
k
gbk
gbk编码的:
所以用
u
t
f
−
8
utf-8
utf−8等方式解码就可能发生错误(蛋疼吧)。如果想把它用
u
t
f
−
8
utf-8
utf−8编码成字节流,就之能分两步操作: