一旦走上编程这条路,如果不把编码这个问题搞清楚,那么它会像幽灵般纠缠你整个职业生涯。
字符串在编程中是使用频率最高的数据类型,像 web 网站中显示的中英文信息,使用记事本打开一个文本文件所看到的内容,软件呈现给用户的信息,包括你现在看到的文字,都属于字符串,可以说字符串无处不在。如果对字符串的编码不理解,编辑过程中非常容易出现乱码问题,相反如果懂编码,那么即使出现乱码,也可以自助解决。不同的编程语言对字符串的处理可能略有差异,但对字符串的编码原理却是相通的, 因此字符串和编码是每个准程序员必备知识,需要引起重视。
先说Python 字符串,再说编码
来自维基百科关于字符串的定义:
字符串(String),是由零个或多个字符组成的有限串行。一般记为s=a[1]a[2]…a[n]。
比如为众多程序员所周知的 "hello, world!"就是一个字符串,其实不管中文英文,能写出来让人们看到的信息都属于字符串。python3 中的 print() 函数用于打印字符串(在 python2 中 print 是一个命令,可以不带括号)
代码语言:javascript复制>>> s1="hello,world!"
>>> s2="世界,你好!"
>>> type(s1)
<class 'str'>
>>> type(s2)
<class 'str'>
>>> print(s1)
hello,world!
>>> print(s2)
世界,你好!
>>>
上文中定义了两个字符串变量 s1,s2,注意 python 是动态语言,不需要事先声明变量的类型,变量的类型由其实际的值决定,运行时可动态改变,是不是非常灵活?!查看变量的类型使用 type() 函数,这个在 debug时请经常使用。
一、Python 中的字符串
python 对字符串的处理非常灵活,对字符串常用到的操作有:
1.定义字符串
python中可以使用单引号',双绰号",三引号(三个单引号'''或三个双引号"""来定义一个字符串,其中三引号可方便的定义多行文本。如下所示:
代码语言:javascript复制>>> s='你好,欢迎来到清如許的公众号。' #定义一个字符串s ,下同
>>> print(s) #打印字符串s,下同
你好,欢迎来到清如許的公众号。
>>> s="你好,欢迎来到清如許的公众号。"
>>> print(s)
你好,欢迎来到清如許的公众号。
>>> s='''你好,欢迎来到清如許的公众号。'''
>>> print(s)
你好,欢迎来到清如許的公众号。
>>> s="""你好,欢迎来到清如許的公众号。"""
>>> print(s)
你好,欢迎来到清如許的公众号。
>>> s="""你好, #定义多行文本,打印时按定义时的格式输出
... 欢迎来到清如許的公众号。"""
>>> print(s)
你好,
欢迎来到清如許的公众号。
>>> s='你好,欢迎来到"清如許"的公众号。' #如果字符串中出现单引号、双引号,那么定义时不能使用相同的引号
>>> print(s)
你好,欢迎来到"清如許"的公众号。
>>> s="""你好,欢迎来到"清如許"的公众号。""" #如果字符串中出现单引号、双引号,那么定义时不能使用相同的引号
>>> print(s)
你好,欢迎来到"清如許"的公众号。
>>> s="你好,欢迎来到"清如許"的公众号。" #如果字符串中出现单引号、双引号,如果使用相同的引号定义,那么要使用转义。
>>> print(s)
你好,欢迎来到"清如許"的公众号。
>>> s='欢迎 #如果在定义时一行写不下,可以使用连接下一行,它们仍然是一行字符串
... 来到清如許的公众号'
>>> print(s)
欢迎来到清如許的公众号
>>> s='你好,n欢迎来到清如許的公众号。' #如果使用单引号或双引号打印多行文本,使用n做为换行符
>>> print(s)
你好,
欢迎来到清如許的公众号。
>>>
这里需要注意的是如果字符串中含有单引号或双引号时,有两种方法处理:转义或使用与字符串中引号不同的引号来定义。 如果需要避免转义,我们可以使用原始字符串,即在字符串的前面加上’r’。如:
代码语言:javascript复制>>> s = r"This is a rather long string containingn
... several lines of text much as you would do in C."
>>> print(s)
This is a rather long string containingn
several lines of text much as you would do in C.
2. 切片
python 中字符串其实是一个只读的数组,我们可以通过下标来访问字符串中的任意一个字符,请看下面交互式环境中的操作和注释:(交互式环境中的语句可以保存在后缀为.py文件中当作 python 程序来执行,类似shell 语法)
代码语言:javascript复制>>> s1="hello,world!"
>>> s2="世界,你好!"
>>> s1[0] #python数据的下标从0开始,0表示字符串中第一个字符
'h'
>>> s1[1]
'e'
>>> s1[2]
'l'
>>> s1[-1] # -1表示字符串中倒数第一个字符,是不是很容易记忆?!
'!'
>>> s1[-2] # -2表示字符串中倒数第一个字符,是不是很容易记忆?!
'd'
>>> s2[0] #中文字符串,也同样适用
'世'
>>> s2[1]
'界'
>>> s2[-1]
'!'
3.格式化字符串
格式化字符串的目的为了更方便打印字符串,先看一个例子: 文件名 lx_str_format.py
代码语言:javascript复制#encoding=utf-8
yourname = "农夫三拳"
game = "王者荣耀"
num = 100
rate = 0.81236 # 注意 rate 最终的打印显示
welcome_string = f"你好,{yourname},欢迎来到{game},你已获得{num}次 MVP,平均胜率 {rate:.2%}"
welcome_string1 = "你好,{},欢迎来到{},你已获得{}次 MVP,平均胜率 {:.2%}".format(
yourname, game, num, rate
)
welcome_string2 = "你好,{0},欢迎来到{1},你已获得{2}次 MVP,平均胜率 {3:.2%}".format(
yourname, game, num, rate
)
welcome_string3 = "你好,{3},欢迎来到{0},你已获得{1}次 MVP,平均胜率 {2:.2%}".format(
game, num, rate, yourname
)
welcome_string4 = "你好,%s,欢迎来到%s,你已获得%d次 MVP,平均胜率 %.2f%%" % (yourname, game, num, rate*100)
print(welcome_string)
print(welcome_string1)
print(welcome_string2)
print(welcome_string3)
print(welcome_string4)
这里使用了三种方法来格式化字符串:
(1)welcome_string 使用 F-strings,是 Python3.6 版本新引入的特性,是最简洁易读,效率也是最高的。 (2)welcome_string1,welcome_string2,welcome_string3 都使用了字符串的 format 函数来进行格式化,通过不同的索引来引用 format 函数的参数。 (3)welcome_string4 使用 % 来格式化字符串,类似C语言中的 printf 函数,不再详述。 三种方法都对 rate 做了 %的转换,并保留两位小数,可以看到在{}使用 ‘:.2%’ 即可显示两位的百分比。上述代码执行结果如下所示:
代码语言:javascript复制你好,农夫三拳,欢迎来到王者荣耀,你已获得100次 MVP,平均胜率 81.24%
你好,农夫三拳,欢迎来到王者荣耀,你已获得100次 MVP,平均胜率 81.24%
你好,农夫三拳,欢迎来到王者荣耀,你已获得100次 MVP,平均胜率 81.24%
你好,农夫三拳,欢迎来到王者荣耀,你已获得100次 MVP,平均胜率 81.24%
你好,农夫三拳,欢迎来到王者荣耀,你已获得100次 MVP,平均胜率 81.24%
其中:F-strings 快速、易学、实用,能有效减少代码量,在实际使用中能使用 F-strings,能看懂其他两种方法即可。
一些细节: 由于 F-strings 是在运行时进行渲染的,因此可以将任何有效的 Python 表达式放入其中。这可以让你做一些漂亮的事情,如:
代码语言:javascript复制>>> f"{2 * 37}"
'74'
>>> f"{'lower'.upper()}"
'LOWER'
>>>
为了使字符串出现大括号,您必须使用双大括号,如果使用三个以上的大括号,则可以获得更多大括号:
代码语言:javascript复制>>> f"{{'hello'}}"
"{'hello'}"
>>> f"{{'hello'}}"*3 #字符串乘以几表示重复几次
"{'hello'}{'hello'}{'hello'}"
>>> f"{{{{{{74}}}}}}" #第两个大括号输出一个大括号
'{{{74}}}'
打印一个整数的二进制、八进制、十六进制
代码语言:javascript复制>>> f"十进制:{11},二进制:{11:b},八进制:{11:o},十六进制:{11:x}"
'十进制:11,二进制:1011,八进制:13,十六进制:b'
>>> f"十进制:{11},二进制:{11:#b},八进制:{11:#o},十六进制:{11:#x}"
'十进制:11,二进制:0b1011,八进制:0o13,十六进制:0xb'
对齐操作
代码语言:javascript复制>>> s
'a'
>>> f"{s.center(10)}" # 共10位,字符串s居中显示,默认以空格填充
' a '
>>> f"{s.center(10,'*')}" # 共10位,字符串s居中显示,指定以'*'填充
'****a*****'
>>> f"{s.ljust(10,'*')}" # 共10位,字符串s靠左对齐,指定以'*'填充
'a*********'
>>> f"{s.rjust(10,'*')}" # 共10位,字符串s靠右对齐,指定以'*'填充
'*********a'
>>> num=10
>>> f"{num:5d}" # 整数对齐,默认以空格填充,右对齐
' 10'
>>> f"{num:f}" # 以浮点数据显示
'10.000000'
>>> f"{num:.3f}" # 以浮点数据显示3位小数
'10.000'
>>> num=1234567890
>>> f"{num:,}"
'1,234,567,890' #智能显示大数字。
4.其他对象转字符串
在实际应用中,将数据(整数,浮点数据)转为字符串的需求是非常频繁的,python3 中有两种方法将其他对象转为字符串:repr(object),str(object)
代码语言:javascript复制>>> repr(49)
'49'
>>> str(49)
'49'
>>> repr(49.99)
'49.99'
>>> str(49.99)
'49.99'
>>> repr(-49.99)
'-49.99'
>>> str(-49.99)
'-49.99'
>>> repr("hello,world")
"'hello,world'"
>>> str("hello,world")
'hello,world'
大多数情况下,这二者没有区别,函数 str() 用于将值转化为适于人阅读的形式,而 repr() 转化为供解释器读取的形式,如果一个对象没有适于人阅读的解释形式的话,str() 会返回与 repr() 等同的值。很多类型,诸如数值或链表、字典这样的结构,针对各函数都有着统一的解读方式。字符串和浮点数,有着独特的解读方式。因此,作为初学者还是使用 str() 函数吧。
5. 其他常见操作
(1)遍历: 不需要下标
代码语言:javascript复制>>> s2
'世界,你好!'
>>> for s in s2:
... print(s)
...
世
界
,
你
好
!
需要下标:
代码语言:javascript复制>>> for i,s in enumerate(s2): #这是一种比较高效的方法,尽量不要使用 len(s0)。
... print(f"s2[{i}] = {s}")
...
s2[0] = 世
s2[1] = 界
s2[2] = ,
s2[3] = 你
s2[4] = 好
s2[5] = !
(2)判断字符是否在字符串中:
代码语言:javascript复制>>> 'a' in 'ab'
True
>>> 'c' in 'ab'
False
>>> if 'a' in 'ab':
... print("a is ab")
...
a is ab
(3)判断字符串中是否以某个字符串开始或结尾:
代码语言:javascript复制>>> "abcd".startswith("a")
True
>>> "abcd".endswith("cd")
True
>>>
(4)大小写转换、查找、替换、对齐、编码、分隔、去空等等,不再一一列举,使用时 help(str) 查找帮助即可。
代码语言:javascript复制help(str)
二、字符串编码
我们都知道计算机底层只能处理数字,也就是 0 和 1,因此任何文件存储在磁盘上都是 0 和 1 的二进制流。我们看到的字符串都是这些二进制流经过一定的规则转化而来的。比如小写字母 'a',根据美国信息交换标准代码,即按 ASCII 码编码时,对应的十进制为整数 97,十六进制为 61 ,二进制为 1100001。保存在磁盘时,它就变成了二进制流 1100001,当从磁盘中读取文件时,1100001 按 ASCII 码解码,会转为 'a' 呈现在我们眼前。过程简写如下:
字符串------->编码------->二进制流 二进制流------->解码------->字符串
计算机在设计时就使用一个字节表示 8 位二进制位,因此我们称这里的二进制流称为字节串,即:
写文件:字符串------->编码------->字节串(在磁盘) 读文件:字节串------->解码------->字符串 (在内存)
注意:字符串是存储在内存中的,二进制流/字节是存储在硬盘或网络数据流中。
由于 ASCII 编码只占用一个字节,即 8 个二进制位,共有 2 的 8 次方个,也就是 256 种可能,完全可以覆盖英文大小写字符及特殊符号。而我们中文汉字远超过256个,使用 ASCII 编码的一个字节来处理中文显然是不够用的,于是中国制定了 GB2312 编码,使用两个字节,可以支持共 2 的 16 次方共 65536 种汉字,可以覆盖常用的中文汉字60370个(当代的《汉语大字典》(2010年版) 收字60,370个)及 ASCII 码。
比如 "清如许" 这个字符串以 GB2312 编码后的字节串如下所示:
代码语言:javascript复制>>> "清如许".encode('gb2312') # 以 GB2312 编码(encode)得到
b'xc7xe5xc8xe7xd0xed'
>>> list("清如许".encode('gb2312')) # list将其转为列表/数组,方便十进制查看
[199, 229, 200, 231, 208, 237]
>>>
>>> b'xc7xe5xc8xe7xd0xed'.decode('gb2312') # 字节串以 GB2312 解码(decode)得到字符串
'清如许'
我们可以看到 "清如许"在 GB2312 编码中共占用了 6 个字节,当然 GB2312 也是包含 ASCII 码的:
代码语言:javascript复制>>> "abc".encode('gb2312') #编码
b'abc'
>>> list("abc".encode('gb2312')) #编码 ,十进制查看
[97, 98, 99]
>>> hex(97)
'0x61'
>>> hex(98)
'0x62'
>>> hex(99)
'0x63'
>>> b'x61x62x63'.decode('gb2312') #解码
'abc'
这仅仅是适用中文简体的一个编码,全世界有上百种语言,每个语言都设计自己独特的编码,这样计算机在跨语言进行信息传输时还是无法沟通(出现乱码)的,于是 Unicode 编码应运而生,Unicode 使用 2-4 个字节编码,已经收录136690个字符,并还在一直不断扩张中。 Unicode 起到了 2 个作用:
- 直接支持全球所有语言,每个国家都可以不用再使用自己之前的旧编码了,用 Unicode 就可以了。(就跟英语是全球统一语言一样)。
- Unicode 包含了跟全球所有国家编码的映射关系。
所有的系统、编程语言都默认支持 Unicode 。 Unicode 编码虽然统一了不同语言的编码不一致的问题,但是新的问题又来了,如果一段纯英文文本,用 Unicode 编码存储会比用 ASCII 编码多占用一倍空间!存储和网络传输时一般数据都会非常多,那么增加一倍空间是无法容忍的,为了解决上述问题,UTF 编码应运而生,UTF 编码将一个 Unicode 字符编码成 1~6 个字节,常用的英文字母被编码成 1 个字节,汉字通常是 3 个字节,只有很生僻的字符才会被编码成 4~6 个字节。注意,从 Unicode 到 UTF 并不是直接的对应,而是通过一些算法和规则来转换的。
- UTF-8: 使用1、2、3、4个字节表示所有字符;优先使用1个字符、无法满足则使增加一个字节,最多 4 个字节。英文占1个字节、欧洲语系占2个、东亚占3个,其它及特殊字符占4个。
- UTF-16: 使用2、4个字节表示所有字符;优先使用2个字节,否则使用4个字节表示。
- UTF-32: 使用4个字节表示所有字符。
可以看出 UTF-8 编码是最节省存储的,也是目前最常用的编码,很多网页的源码上会有类似<meta charset="UTF-8" />的信息,表示该网页正是用的UTF-8编码。
代码语言:javascript复制>>> list("清如许".encode('utf8'))
[230, 184, 133, 229, 166, 130, 232, 174, 184]
>>> list("abc".encode('utf8'))
[97, 98, 99]
>>>
从上面的输出可以看出,中文在 utf8 编码中占用 3 个节点,英文还是占用 1 个字节,因此如果是中文文本以 utf8 编码保存占用的磁盘空间是 gb2312 编码保存的 1.5 倍。
编码的问题理解了,我们再来看下 Python3 代码的执行过程。
首先 Python3 解释器找到源代码文件,按源代码文件声明的编码方式解码内存,再转成 unicode 字符串。 把 unicode 字符串按照语法规则进行解释并执行,其中所有的变量字符都会以 unicode 编码声明。 读写文件过程如下图所示:
python源代码的编码解码过程
下面在 windows 上做个测试 编写 bm_test.py 保存为 utf8 编码,如下图所示:
bm.png
只要文件头部声明的编码和文件保存的编码一致,输出不会有乱码,推荐大家在编码过程都这样操作。
执行结果
但是如果 bm_test.py 不声明 # -- coding: utf-8 --,在默认编码为 gbk 的 windows 上执行仍会正常输出,这是因为到了内存里 python3 解释器把 utf-8 转成了 unicode , 但是这只是 python3, 并不是所有的编程语言在内存里默认编码都是 unicode ,比如 python2 的默认编码是 ascii ,python2 解释器仅以文件头声明的编码去解释你的代码,上述 bm_test.py 在 python2 中会以 utf-8 解码得到 utf-8 字符串,不会自动转为 unicode 字符串,这意味着在默认编码为 gbk 的 windows 上执行结果是乱码。
因为只有2种情况 ,你的 windows上显示才不会乱 (1)字符串以 GBK 字符串显示 (2)字符串是 unicode 编码 那么在 python2 中,需要你手工转换,在 windows 修改 bm_test.py 如下所示:
python2 中的 bm_test.py 执行结果
python 2 的执行结果
可以看出 Python3 容忍你的偷懒,而 Python2 却不行,还需要你手工转换,Python3 在编码方面比 Python2 是有明显进步的,建议初学者从 Python3 开始学习。 Python2 将在 2020 年停止更新。
总结:Python3 对字符串的处理是非常灵活的,有许多操作都可以一行代码完成,换成其他语言可能需要多写很多代码,如果了解关于字符串的详细信息,请使用 help(str) 来查询;对于字符编码问题,还是需要深入理解才行,这样在遇到任何编码的问题都可以迎刃而解。
如有读者针对文中内容有疑问,欢迎微信后台留言,会尽快回复。
(完)