AI智能
改变未来

Python 基础系列–字符串与编码

字符串在编程中是使用频率最高的数据类型,像 web 网站中显示的中英文信息,使用记事本打开一个文本文件所看到的内容,软件呈现给用户的信息,包括你现在看到的文字,都属于字符串,可以说字符串无处不在。如果对字符串的编码不理解,编辑过程中非常容易出现乱码问题,相反如果懂编码,那么即使出现乱码,也可以自助解决。不同的编程语言对字符串的处理可能略有差异,但对字符串的编码原理却是相通的, 因此字符串和编码是每个准程序员必备知识,需要引起重视。

先说Python 字符串,再说编码

来自维基百科关于字符串的定义:

字符串(String),是由零个或多个字符组成的有限串行。一般记为s=a[1]a[2]…a

比如为众多程序员所周知的 \”hello, world!\”就是一个字符串,其实不管中文英文,能写出来让人们看到的信息都属于字符串。python3 中的 print() 函数用于打印字符串(在 python2 中 print 是一个命令,可以不带括号)

>>> 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中可以使用单引号\’,双绰号\”,三引号(三个单引号\’\’\’或三个双引号\”\”\”来定义一个字符串,其中三引号可方便的定义多行文本。如下所示:

>>> 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’。如:

>>> s = r\"This is a rather long string containing\\n\\
... several lines of text much as you would do in C.\"
>>> print(s)
This is a rather long string containing\\n\\
several lines of text much as you would do in C.

2. 切片

python 中字符串其实是一个只读的数组,我们可以通过下标来访问字符串中的任意一个字符,请看下面交互式环境中的操作和注释:(交互式环境中的语句可以保存在后缀为.py文件中当作 python 程序来执行,类似shell 语法)

>>> 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

#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%’ 即可显示两位的百分比。上述代码执行结果如下所示:

你好,农夫三拳,欢迎来到王者荣耀,你已获得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 表达式放入其中。这可以让你做一些漂亮的事情,如:

>>> f\"{2 * 37}\"
\'74\'
>>> f\"{\'lower\'.upper()}\"
\'LOWER\'
>>>

为了使字符串出现大括号,您必须使用双大括号,如果使用三个以上的大括号,则可以获得更多大括号:

>>> f\"{{\'hello\'}}\"
\"{\'hello\'}\"
>>> f\"{{\'hello\'}}\"*3  #字符串乘以几表示重复几次
\"{\'hello\'}{\'hello\'}{\'hello\'}\"
>>> f\"{{{{{{74}}}}}}\"  #第两个大括号输出一个大括号
\'{{{74}}}\'

打印一个整数的二进制、八进制、十六进制

>>> f\"十进制:{11},二进制:{11:b},八进制:{11:o},十六进制:{11:x}\"
\'十进制:11,二进制:1011,八进制:13,十六进制:b\'
>>> f\"十进制:{11},二进制:{11:#b},八进制:{11:#o},十六进制:{11:#x}\"
\'十进制:11,二进制:0b1011,八进制:0o13,十六进制:0xb\'

对齐操作

>>> 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)

>>> 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)遍历:
不需要下标

>>> s2
\'世界,你好!\'
>>> for s in s2:
...     print(s)
...






需要下标:

>>> 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)判断字符是否在字符串中:

>>> \'a\' in \'ab\'
True
>>> \'c\' in \'ab\'
False
>>> if \'a\' in \'ab\':
...     print(\"a is ab\")
...
a is ab

(3)判断字符串中是否以某个字符串开始或结尾:

>>> \"abcd\".startswith(\"a\")
True
>>> \"abcd\".endswith(\"cd\")
True
>>>

(4)大小写转换、查找、替换、对齐、编码、分隔、去空等等,不再一一列举,使用时 help(str) 查找帮助即可。

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 编码后的字节串如下所示:

>>> \"清如许\".encode(\'gb2312\')  # 以 GB2312 编码(encode)得到
b\'\\xc7\\xe5\\xc8\\xe7\\xd0\\xed\'
>>> list(\"清如许\".encode(\'gb2312\'))  # list将其转为列表/数组,方便十进制查看
[199, 229, 200, 231, 208, 237]
>>>
>>> b\'\\xc7\\xe5\\xc8\\xe7\\xd0\\xed\'.decode(\'gb2312\') # 字节串以 GB2312 解码(decode)得到字符串
\'清如许\'

我们可以看到 \”清如许\”在 GB2312 编码中共占用了 6 个字节,当然 GB2312 也是包含 ASCII 码的:

>>> \"abc\".encode(\'gb2312\')  #编码
b\'abc\'
>>> list(\"abc\".encode(\'gb2312\'))     #编码  ,十进制查看
[97, 98, 99]
>>> hex(97)
\'0x61\'
>>> hex(98)
\'0x62\'
>>> hex(99)
\'0x63\'
>>> b\'\\x61\\x62\\x63\'.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编码。

>>> 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) 来查询;对于字符编码问题,还是需要深入理解才行,这样在遇到任何编码的问题都可以迎刃而解。

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » Python 基础系列–字符串与编码