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

一旦走上编程这条路,如果不把编码这个问题搞清楚,那么它会像幽灵般纠缠你整个职业生涯。

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

先说 Python 字符串,再说编码

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

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

比如为众多程序员所周知的 "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)

这里使用了三种方法来格式化字符串:

  • welcome_string 使用 F-strings,是 Python3.6 版本新引入的特性,是最简洁易读,效率也是最高的。
  • welcome_string1,welcome_string2,welcome_string3 都使用了字符串的 format 函数来进行格式化,通过不同的索引来引用 format 函数的参数。
  • 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 编码是最节省存储的,也是目前最常用的编码。

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

(完)

文章首发公众号,如果觉得这篇文章对您有帮助,请关注公众号 somenzz 获取最新信息或推荐给需要的朋友。
 somenzz 的公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值