字符串操作

4.1 字符串类型

Python中,用单引号、双引号、三个连续单引号括起来的都是字符串字面值,可以把它们赋给一个变量。如:

s1='Henry'     # 中间可以使用双引号作为字符 (想想为什么)
s2="Jack"     # 中间可以使用单引号作为字符
s3='''Jane'''  # 中间可以使用单引号或双引号作为字符,
             # 但连续单引号数量不能大于等于3个
             # (想想为什么,再试一下字符串末尾能放单引号吗)

转义字符

如果想要在单引号括起的字符串中使用单引号,或在双引号括起的字符串中使用双引号,或在连续3个单引号括起的字符串中使用连续3个单引号,需要将它们转义(在这儿是不将它们用作字符串的分界符):

s1 = 'Henry's father is Jack'   # 错误, Henry后面的'表示字符串结束,
                             # 它后面的字符没有意义
                             # 这行代码会产生一个语法错误: SyntaxError: invalid syntax
                             # 翻译过来就是               语法错误:     无效的语法
s1 = 'Henry\'s father is Jack'  # 正确,\'用在字符串里表示'字符本身,
                             # 使用\对'进行了转义,使它不再起字符串分界符的作用

类似的,有:

s1="Henry is reading the book \"The Old Man and The Sea\""
# '无需转义,而"需要转义,想想这是为什么
# 这个字符串打印出来是什么效果? 试试下面这行代码:
print(s1)

1. 字符串的类型

我们讲过,Python中的变量和值都有类型,那么字符串字面值和对应的变量是什么类型呢? 我们可以用Python中常用的一个函数来得到它们的类型,这就是type()函数:

$python3       # 运行python3
Python 3.6.1 (default, Apr  5 2017, 23:07:58)
[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.38)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> s="Henry"       # >>>是python提示符,在它后面,
                  # 我们可以输入python指令,回车即可执行
>>> type("Henry")   # type是类型的意思,它包含基本数据类型如整型、浮点型
                  # 也包括"类和对象"这组概念中的"类"类型,即class类型
                  # class是"类"的意思,而非"班级"的意思
<class 'str'>     # "Henry"的类型是'str'这个类,即字符串类,这里'str'是字符串类的名字
                  # class 说明'str'是一个类的名字,而非字符串'str'本身
>>> type(s)       # 同上
<class 'str'>  

再看看type()的两个例子:

>>> i=8
>>> f=0.8
>>> type(i)
<class 'int'>    # i是整型,'int'是整型类型的名字
>>> type(f)
<class 'float'>  # f是浮点型,'float'是浮点类型的名字

练习

  1. 想一下,字符串s1="Henry is\treading the book "The Old Man and The Sea"\nHe like it."打印出来是什么效果,在Python命令行中将它打印出来看看。(提示,用print()函数)
  2. 随便构造两个Python变量(或称"对象"),用type()函数看看它们的类型是什么? type(3), type(3.14),type(3.14000000000000000000000000000000000000000000000001)分别是什么类型? Python 3.6 中有double类型吗? 怎么试验?

4.2 字符串的取值和操作

上文我们说过,在计算机程序设计语言中,当我们定义了一个类型时,就定义了这个类型可以取的值和可以在这些值上可以进行的操作。

1. 字符串的取值和编码

字符串可以取的值有很多,最常用和最简单的是ASCII字符集中的字符。ASCII字符集中共有128个字符,其二进制编码对应于十进制数0~127,其中有95个可打印字符,33个控制字符。第0~32号及第127号是控制字符;第33~126号是可打印字符,其中第48~57号为0~9十个阿拉伯数字;65~90号为26个大写英文字母,97~122号为26个小写英文字母,其余的是一些标点符号、运算符号等。

下面这个例子使用Python代码打印出了ASCII字符集中的可打印字符和其数量:

# 例3-1: 打印ASCII字符集中的可打印字符
count=0
for i in range(128):
    if str.isprintable(chr(i)):  # chr(i)把数字转换成对应的字符
        # str类型的isprintable方法(函数)判断该字符是否可打印字符
        print(chr(i))
        count+=1
print("total: "+str(count))

ASCII字符集形成时,计算机主要由英文世界的人们使用,因此ASCII字符集主要包含了英文字符和一些控制字符,只需1个字节(8位二进制数)即可表示所有字符。后来,随着计算机及其应用程序在世界上的推广,由于世界上的语言多种多样,为了在计算机世界中表达这些语言中的字符,保留ASCII字符集的同时,对字符集进行了扩展,形成了Unicode字符集,如果想要简单的理解,可以把Unicode字符集看成用4个字节(32位二进制数)表示的字符集,每个数可以表示一个字符,共有4294967296个字符,足够表示现在世界上的所有语言中的字符了。不过,由于4个字节表示一个字符,对于类似ASCII字符这样只需少量字节即可表示的字符来说,浪费了大量内存空间,所以实际使用中,这样的字符还只用1个字节表示,需要两个字节表示的字符就用两个字节表示,以此类推,从而可以节省大量存储空间。国际字符的这种编码方式,称为Unicode编码。另外,由于历史的原因,除了ASCII编码外,还有针对欧洲字符集、亚洲字符集等字符集的编码方式。对于我国的简体中文字符,有GB2312-80字符集、GBK字符集、GB18030-2000字符集、GB18030-2005字符集等编码方式,它们收录的汉字数量有所不同,针对繁体中文有Big-5(大五码)字符集。Windows系统下,一般使用GBK字符集表示中文,Linux系列操作系统下一般使用Unicode字符集表示中文,因此,常常会有将Windows系统下建立的中文本文文件拷贝到Linux操作系统下,查看时发生乱码的情况,这时需要对Linux添加GBK字符集即可正常显示。

由于上述原因,在Python中使用国际字符集时,一般需要指定它所用的编码,否则就不能正常处理字符串。比如,在Python脚本文件中,常常在首部加上如下一行代码:

#coding: utf-8

这行代码表示Python脚本使用的是utf-8编码,这是Unicode编码的一种。这样做了之后,就可在字符串和注释中使用中文字符。

2. 字符串的存储、网络传输和编码

计算机世界常常有一些竞争、对立的东西,它们各有自己的拥护者,一旦在网络上提及这些内容,常常会引发"圣战": 比如Windows和Unix操作系统、Vim和Emacs编辑器、Python和Perl程序设计语言…,多的不可胜数。遇到这种情况时,无需纠结,只要选择一个你喜欢的阵营,然后坚持使用,肯定会有所收获。

对于多字节字符(数据)在存储器中的存储方式,也有类似的两个阵营,它们是"大头字节序(big endian)"和"小头字节序(little endian)"。("大头"和"小头"这个说法,出自《格列佛游记》,其中两个国家因为吃鸡蛋应该先从大头吃还是先从小头吃爆发了一场战争)

大头字节序和小头字节序和硬件架构有关。一些机器如Motorola的PowerPC使用大头字节序,个人电脑如x86系列计算机采用小头字节序。arduino系列开发板的芯片是Atmel系列芯片,使用小头字节序。

结合以前的知识,我们知道计算机的存储器可以对每个字节都可分配一个地址。对于多字节数据,这些字节在存储器上是连续存放的,内存地址有高位地址(地址数字大)和低位地址(地址数字小)。小头字节序指低字节数据(小头)存放在内存低地址处,高字节数据存放在内存高地址处;大头字节序是高字节数据(大头)存放在低地址处,低字节数据存放在高地址处。

figure3-1.png

图3-1 Intel处理器的内存布局

对于多字节数据0x12345678,这个顺序是: 高字节数据<———>低字节数据 :

小头字节序(Little endian):高地址<------->低:存储数据0x12 0x34 0x56 0x78

大头字节序(Big endian) :高地址<------->低:存储数据0x78 0x56 0x34 0x12

由于大头字节序和小头字节序的存在,多字节数据如国际字符,在不同类型的计算设备上的存储方式是不同的,即相同编码的国际字符,存储在不同类型的计算设备上,编码的存储顺序可能不同,它可能按大头字节序存储,也可能按小头字节序存储。

如上所述,大头字节序和小头字节序都是计算机内部存储多字节数据时所用的布局方式,只是不同类型的计算机硬件采用的方案不同,如果只要开发单机程序,我们无需担心其具体存储方式是哪种,但是如果需要开发在网络上进行通信的程序,就规定通信双方采用哪种字节序进行通信,就好比我们交流时应该使用同种语言一样。现今网络通用的字节序称为网络字节序,它就是大头字节序。

在网络通信之前,我们需要将主机的存储字节序转换成网络字节序,这使用字符串的encode()方法实现,字符串的encode()函数默认使用utf-8编码将字符串转换成网络字节序。比如:

# encode.py
#coding: utf-8
s="王"  #  utf-8编码的国际字符串
s_byte=s.encode();
print(s_byte)

输出为:

$ python3 encode.py
b'\xe7\x8e\x8b'      # 网络传输时,传输的内容是这个编码

当计算设备接收到网络上传输过来的这个编码时,使用decode()将其转换成主机字节序(大头或小头字节序):

# decode.py
#coding: utf-8
s_byte=b'\xe7\x8e\x8b'  # 接收到的网络数据
s=s_byte.decode()       # 默认使用utf-8编码方式进行解码,
                        # 只有s_byte是使用utf-8方式编码时才能得到正确的结果
print(s)

上述代码的输出结果是:

$ python3 decode.py
王

练习

  1. utf-8编码的字符串"故宫博物院"的网络编码是什么?怎么把网络编码转换成原来的字符串。

3. 字符串的性质

字符串中的每个字符都可使用下标进行访问

可以使用整数作为下标对字符串中的字符进行访问,但是下标不能超出某个范围,这个范围是由字符串的长度决定的。例如:

# 依次打印字符串中的每个字符和对应的下标
s = "Henry is my son!"
for i in range(len(s)): # i为0~len(s)-1中的整数
    print("s[%d] = %s" % (i, s[i]))
    # 字符串"s[%d] = %s"称为格式字符串,它用于表示输出
    # 的格式,其中的%d和%s是两个占位符,
    # %开始,加上特定数字和字符,表示用指定的格式输出某种类型的值
    # 遇到%d时,按整型打印后面对应位置上的变量或值,这里是i
    # 遇到%s时,按字符串打印后面对应位置上的变量或值,这里是s[i]
    # %f可以输出浮点数
    # 这个字符串中的其他字符按原样打印,其中支持转义字符
    # 这个字符串后面的%表示格式字符串的结束和用于输出的变量组的开始
    # (i, s[i])是用于输出的变量组,其中变量或值的数目通常与
    # 格式字符串中的占位符数量相同
# 想想看这个代码输出了什么,再测试一下它的结果与你的想法是否符合

试着取一个不在上述索引位置范围中的字符,看看有什么结果:

>>> s='Henry is my son!'
>>> s[len(s)]
Traceback (most recent call last):     # 运行出错
  File "<stdin>", line 1, in <module>  # line 1 标出了出错的位置为第1行
IndexError: string index out of range  # 指出了错误的类型
# 下标错误: 字符串索引超出范围

在C语言中,引用了数组中一个不存在的位置的元素时,编译器可能会报警,但一般会正常编译通过,程序可以运行,但运行结果会不正常,原因是下标引用的并非正常数据。

再看一个有趣的内容,给下标取一个负值怎么样?

# [-i for i in range(1, len(s)+1)] 称为列表解析,是对
# range(1, len(s)+1)中的每个整数取负值,然后组成一个新的列表
# 而下面这行代码是逐个取用上述新列表中的负整数值
for i in [-i for i in range(1, len(s)+1)]:
    print("s[%d] = %s" % (i, s[i]))
# 想想看这两行代码的运行结果是什么,
# 然后测试一下是否符合你的想法
# 猜猜看负索引值的范围是什么

负下标是C语言中没有的内容,它的出现,方便了我们反向索引字符串中的字符。结合负下标和字符串的切片技术,我们可以用很少的代码实现一个程序,判断一个字符串是否回文体,很长的字符串也没有问题。

回文体是正读和反读完全一样的字符串,英文和中文里面都有。例如"上海自来水来自海上", “Able was I ere I saw Elba”(我在到俄尔巴岛之前是有能力的——拿破仑在战败后所发的叹语, 忽略大小写)

字符串是不可变量

可以把字符串理解成只能读而不能写的数据。例如,我们可以这样引用字符串中的一个字符:

>>> s="Henry studies hard!"
>>> s[0]
'H'         # s[0]引用了第1个字符

但是,假若我们想要使用下标操作修改字符串s的内容,比如:

>>> s[1]='a'; s[2]='b'
Traceback (most recent call last):     #报告错误了
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
# 类型错误: 'str' 对象不支持对元素赋值

错误的原因在错误提示里已经给出了,即字符串对象不支持对元素进行赋值。这是由于字符串属于不可修改的对象造成的。也就是说,字符串对象一旦生成,就不能对它进行修修补补,改变它的样子。那么,要实现我们想要的效果,该怎么办呢?办法就是重新构造一个想要的字符串对象,赋给同一个变量 s 就行了。

C语言里的字符数组是可以修改的,它的字符串本质上是字符数组。

>>> s="Jack studies hard!"
>>> s
'Jack studies hard!'  # 没有问题了

但是,问题来了,上面两个s在内存中占用的是同一个存储空间吗? 答案是它们并不占用同样的存储空间。让我们来看一下它们的内存地址:

>>> s='Henry studies hard!'
>>> id(s)     # id函数返回内存中某个对象的起始地址
4337854984    # 内存地址用长整型表示,可以简单地把它理解成一个很大的整数
              # 我们讲过,内存中的每个字节都可有一个独特的地址
              # s在这里引用了内存中起始地址为4337854984的存储空间中的数据,这里是
              # 'Henry studies hard!'
>>> s='Jack studies hard!'
>>> id(s)
4337855056    # 看,内存起始地址不同了。
              # 现在s引用的是内存地址4337855056开始的存储空间的数据,这里是
              # 'Jack studies hard!'

要是我们能够修改字符串对象的值,那么上述代码中,后一个s指向的起始地址不会改变,而只需把该地址处的 Henry 改成 Jack 即可。

不能修改字符串,我们该如何操作和使用字符串呢?Python中使用字符串的正确打开方式是利用字符串的切片、连接操作和字符串类型提供的各种操作函数(它们本质上也是通过切片和连接操作实现的)。

练习

  1. s="0123456789",取出s中的奇数数字,把它们组合成一个新的字符串返回,返回值应当是r="13579"

4.操作字符串

字符串的连接操作使用操作符+号即可,没有太多可以介绍的。下面我们重点讲讲切片操作。

字符串的切片

所谓字符串的切片,可以理解成按照规则从字符串中提取一些字符,形成新的字符串作为返回结果。字符串切片的用法如下:

s[start:end_plus_one:step] # start 开始下标,省略时默认为0
                           # end_plus_one 结束下标的下一个位置,省略时默认为len(s)
                           # step 步长, 可省略,省略时默认为1
#从开始下标起,到结束下标的下一个位置止,不包括该位置,
#每隔step个位置取一个字符,构成一个新字符串返回

举些例子:

>>> s='Henry is my son.'
>>> s[0:5]
'Henry'        # 取出了下标为0、1、2、3、4处的字符,
             # 可简写为 s[:5] :前面的0省略了,表示从第1个字符取到第5个字符
             # 但:不能省略
>>> s[12:len(s)] # 同理,可简写为s[12:],表示从第13个字符取到最后一个字符'son.'           
                 # :不能省略
# 如果要按原来的样子重新构建一个字符串怎么办?
>>> s[0:len(s)]  # 这样做即可,它和原来的字符串一模一样,但内存地址不同了
                 # 它们是两个独立的对象,
                 # 你可以用id()函数查看一下
>>> s[:]   # 和上面一行代码是同样的结果,但省略了起点、终点和步长
# 上面全是不带步长的,我们举一个带步长的例子看看
>>> s[::2]  # 起点是字符串开头,终点是字符串结尾,
            # 步长是2,每2个字符取出一个字符,结果如下
'Hnyi ysn'   # 你想到了吗

还可以使用负下标

>>> s='Henry is my son.'
>>> s[-4:]  # 从倒数第4个字符开始到最后一个字符
'son.'
>>> s[-1:-5:-1] # 从最后一个字符开始,到倒数第4个字符,逆序
'.nos'
>>> s[-1:-len(s)-1:-2] # 步长为-2,逆序每2个字符取一个字符
                       # 你明白为什么用-len(s)-1而不是-len(s)+1或-len(s)吗
'.o ms y'

还可以将正负下标结合使用:

>>> s='Henry is my son.'
>>> s[-1:5:-1]  # 注意,不包括下标为5的字符哦
'.nos ym '
# 所以,不能用下面这行代码取字符串的逆序
>>> s[-1:0:-1]
'.nos ym si yrne'  # 少了一个'H'
# 而要用这行代码,想想为什么
>>> s[-1:-len(s)-1:-1]  
>>> '.nos ym si yrneH'

现在,给出一个有趣的例子,它能判断一个字符串是否为回文:

#coding:utf-8
s=input("请输入一个字符串:")
if s==s[-1:-len(s)-1:-1]:       # 比较正序和逆序字符串,是这个程序的核心功能
                                # 这个功能在C语言中需很多代码实现
    print("是回文体")
else:
    print("不是回文体")
练习
  1. 用切片的方法将s="0123456789"中的奇数数字组成一个新字符串返回,结果为r="13579"
  2. 不看教程中的代码,亲自把判断一个字符串是否为回文体的代码编写出来。要求: 最好能反复提示输入新的字符串,判断它是否回文,输出结果,直到输入no停止程序。
字符串的操作函数

如前所述,我们可以把一个字符串字面值赋给一个变量,常用的字符串操作还有用 str( ) 函数使用其他类型的数据构造一个字符串,用 len( ) 函数取得字符串的长度,以及使用操作符 + 号进行字符串的连接,使用 print( ) 函数打印字符串等。

使用简单的字符串处理函数,结合一些其他 Python 功能,就可以编写一个有活力的程序,如下面的命令行时钟。

例3-1,制作一个命令行时钟:

import time   # 导入time模块

def cur_time():
    hour=str(time.localtime().tm_hour)  # str()将数字转换成字符串
    minute = str(time.localtime().tm_min)
    sec = str(time.localtime().tm_sjnxyfhwfec)

    if len(hour)==1:  # len()返回字符串的长度
        hour="0"+hour  # 字符串的加法(连接)
    if len(minute)==1:
        minute="0"+minute
    if len(sec)==1:
        sec="0"+sec

    return hour+":"+minute+":"+sec  # 字符串的加法


if __name__ == "__main__":
    time_old=cur_time()
    print(time_old, end='', flush=True)  # print()打印字符串,flush参数的作用是立即刷新
    len_t = len(time_old)
    while True:
        time_new=cur_time()
        if(time_new!=time_old):
            print(len_t*"\b", end='',flush=True)  # 使用转义字符 \b表示退格
            print(time_new, end='', flush=True)  
            time_old=time_new

实际上,Python中操作字符串的函数,可以分成两类,一类是全局函数,一类是字符串类型中定义的函数。对于全局函数,使用时直接对它进行调用即可,如,求字符串s的长度使用length=len(s)。对于字符串类型中定义的函数,我们需要使用字符串(字面值或变量)加上一个.,再加上函数的名称和调用参数列表即可,如"Jack".lower()可以把字符串"Jack"变成小写字符(你运行一下看看)。

这里普及一点面向对象程序设计的知识。

上文讲过,Python可以使用面向对象式编程,真实的情况是,Python语言在设计时充满了面向对象的思维,实际上,在Python中,"一切皆对象",也就是说,所有东西都是某个类的实例。比如:

i = 1
s = "abcde"
def foo(): pass
class C(object): pass
instance = C()
l = [1,2]
t = (1,2) 

它们在python解释器中执行的时候,都会在堆中新建一个对象,然后把新建的对象绑定到变量名上。

i = 1                    -----新建一个PyIntObject对象,然后绑定到i上
s = "abcde"              -----新建一个PyStringObject对象,绑定到s上
def foo(): pass          -----新建一个PyFunctionObject对象, 绑定到foo上
class C(object): pass    -----新建一个类对象,绑定到C上
instance = C()           -----新建一个实例对象,绑定到instance上
l = [1,2]                -----新建一个PyListObject对象,绑定到l上
t = (1,2)                -----新建一个PyTupleObject对象,绑定到t上

PyIntObject、PyStringObject、PyFunctionObject等都是 Python 中内置的类类型。从中,我们可以看到整型数、字符串、函数、类、类的对象、列表、元组等都是某个类实例化的结果,也就是说,它们都是对象。由于在定义类时,可以定义在类上的操作(常称作"方法"),这些操作就是可以在此类对象上可以执行的函数。如,上文中的s.lower()方法,它可以在所有字符串类型的对象上使用,其中s是一个字符串对象。

为了加深理解,我们现在来定义一个类,且实例化该类的一些对象,以获得感性认识。

class Point(obejct):               # 类定义,class关键字用于定义类, Point是类名
                                   # object 是父类
    def __init__(self, x=0, y=0):  # 构造函数,它是一个特殊的成员函数用于生成一个对象
                                   # self是所有成员函数的第一个参数,调用时可不给出,
                                   # 而是默认使用
                                   # self表示类实例(对象)本身
        self.x = x                 # 对象中有一个名为x的变量,用self.x给出,用函数参数
                                   # x给它赋值
        self.y = y                 # 同上,只是用y代替x
    def printSelf(self):           # 普通成员函数,你能看出它的作用来吗
        print("Point(%d, %d")" % (self.x, self.y) )
    def setPoint(self, x, y):      # 普通成员函数,你能看出它的作用来吗
        self.x = x
        self.y = y

# 主程序
if __name__ == "__main__":
    point1 = Point()           # 调用的是Point.__init__(0,0),它是类的构造函数
    point2 = Point(5)          # 参数是(5, 0)
    point3 = Point(y=6)        # 参数是(0, 6)
    point4 = Point(3, 4)       # 参数是(3, 4)
                               # point1、point2、point3、point4是对象
    point1.printSelf()         # 调用point1对象的成员函数打印它自己的信息
    point2.printSelf()
    point3.printSelf()
    point4.printSelf()
          
    point4.setPoint(6,7)
    point4.printSelf()

输出为:

Point(0, 0)  # point1.printSelf()
Point(5, 0)  # point2.printSelf()
Point(0, 6)  # point3.printSelf()
Point(3, 4)  # point4.printSelf()

Point(6, 7)  # point4.printSelf()

从上面可以看出,类的定义中既包括数据,又包括函数。类定义中的数据指出了以类为模具构造出来的对象可以包含什么类型的数据,以及含有数据的数量,如class Point中的成员变量self.x 和self.y。类定义中的函数指出了在该类对象上可以执行何种操作,如class Point中的构造函数__init__()和普通成员函数printSelf()和setPoint()。使用对象中的成员时需要使用.操作符进行,形式为对象.成员变量对象.成员函数,向成员函数传递参数的形式和调用普通函数传递参数的形式相同,都是将参数放置在函数调用操作符()中。

现在,我们知道了可以使用字符串的成员函数对字符串进行操作,那么字符串类型定义了多少成员函数呢,而且各个成员函数的用法是怎样的呢。我们可以使用Python中的两个重要函数得到它们的名称和用法。第一个函数是dir(),dir()函数的参数是一个类型的名字或对象的名字,它的作用是列出定义在该类型或该对象所属类型上定义的名字。如:

>>> dir(str)   # python3 命令行
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

我们看到dir(str)返回了一个列表,其中的每一项都是一个字符串,它们是str类型(字符串类型)上定义的成员名字,这里面以__开头和结尾的名字是特殊成员名字,我们暂时不用管它,现在对我们有意义的是从"capitalize"到'zfill'的这些名字,它们都是我们可以用在字符串对象上的普通成员函数,每个这样的成员函数对字符串执行一个操作。例如:

>>> s='Jack'
>>> s.capitalize()    # 字符串首字母变成大写,其他字母变成小写字母
'Jack'
>>> s.count('hx')     # 子字符串'hx'在字符串s中出现的次数
1

现在,我们已经知道可以通过dir() 获得在一个变量或某种类型的对象上可以执行的操作(成员函数),那么,某个操作到底执行什么运算呢? 下面轮到Python中一个闪闪发亮的实用函数help()函数出场了,用它可以得到某个函数的帮助信息,例如,对于上面的s.capitalize()函数,我们可以调用

>>> help(s.capitalize)  # 注意不需要加上函数调用操作符()和参数列表哦,
                        # help()的参数只是一个名字而已
                        # s.capitalize说明capitalize是字符串对象中定义的一个名字
capitalize(...)         # 函数原型
    S.capitalize() -> str   # 调用形式和返回值,这里是使用
                            # 字符串对象+ . 操作符 + 函数名(参数列表) 进行调用
                            # 返回值是一个字符串对象
    Return a capitalized version of S, i.e. make the first character  
    # 该函数的说明: 返回S的首字母大写版本,即
    have upper case and the rest lower case.                          
    # 第一个字母大写,其余字母均为小写
# 再看一个
>>> help(str.count)   # 使用help(类型.函数名)的形式也能得到帮助
count(...)            # 函数原型
    S.count(sub[, start[, end]]) -> int  # 调用形式和返回值, [ ]里的是可选参数,
                                      # 注意辨别[ ]的嵌套表示什么,你能猜出来吗
    
    Return the number of non-overlapping occurrences of substring sub in 
    # 该函数的说明: 返回字符串
    # S[star:end] --> 这是一个切片,还记得吗
    string S[start:end].  Optional arguments start and end are           
    # 中子字符串sub的非重叠出现的次数
    # 像s="fooooooooo" 中的"oo"子字符串就是重叠的
    # 非重叠指的就是不出现像上述情况的子字符串
    interpreted as in slice notation.                                    
    # 可选参数start和end解释为切片记法

到此为止,在Python中获取帮助信息的4个常用函数都已介绍完毕了,它们分别是id(), type(), dir() 和help()。合理使用它们,你就可以查看一个对象的地址、获取一个对象的类型、列出某个对象或类型上可以执行的操作,以及查看某个对象、函数或类的帮助信息。有了它们的帮助,我们可以通过做一些小实验来自由地学习Python中的基础知识,很方便不是吗? 自己实际编写一些Python代码,使用这些功能探索Python的能力,颇为酣畅淋漓,你会喜欢上这种感觉的。

下面列出了一些很快就能掌握的字符串类型支持的成员函数,请你用help()查看一下它们的帮助文件,弄懂它们的含义和用法。

'encode', 'endswith', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'islower', 'isnumeric', 'isprintable', 'isspace',  'isupper', 'lower', 'replace', 'rfind', 'rindex', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'upper'

基本上,上面这些成员函数就是字符串类型中常用的操作了,你弄懂多少了呢 ?

find()、split()、strip()、upper()、lower()成员函数

现在我们先了解字符串成员函数str.find():

>>> help(str.find)
find(...)
    S.find(sub[, start[, end]]) -> int
    
    Return the lowest index in S where substring sub is found,  # 返回字符串S中找到的子
                                                                # 字符串sub的最小下标
    such that sub is contained within S[start:end].  Optional   # 这个sub包含在
                                                                # S[start:end]中。
    arguments start and end are interpreted as in slice notation.  # 可选参数start和
                                                                   # end被解释为切片
    
    Return -1 on failure.                                          # 失败时返回-1

下面是使用str.find()函数的一个例子:

>>> s="Henry is my son, I love Henry, his mother love Henry too."
>>> s.find("Henry")       # sub='Henry', start默认为0, end默认为len(s)
0

假如要找到s中的每一个'Henry'出现的位置怎么办?

# 这样做就找到了所有的"Henry”出现的下标位置了
start=0
end = len(s)
while (pos=s.find("Henry", start, end)) != -1:
    print(pos)
    start+=len('Henry')

又假如我们要找到代表你的名字的所有字符串怎么办,str.find()函数是区分大小写的呀? 很简单,查找之前,把它们都转换成大写或小写就可以了:

start=0
end=len(s)
while (pos=s.lower().find('Henry', start, end))!= -1:
    print(pos)
    start+=len('Henry')

然后,让我们看一下str.split()函数,它的函数原型是:

S.split(sep=None, maxsplit=-1) -> list of strings
    # 返回S中的单词列表,使用sep作为分隔字符串。
    # 假如给出了maxsplit,最多分割maxsplit个单元。
    # 假如没有指定sep或sep指定为None,使用任意空白字符
    # 作为分隔符,并将空字符串从结果中移除

关于这个函数,有几点需要注意:

  1. 分隔符默认为空白字符,空白字符(whitespace)指的是在屏幕上不会显示出来的字符,包括空格(" ")、水平制表符("\t")、垂直制表符("\v")、换页符("\f")、回车和换行符("\n"),括号中给出了空白字符的转义字符形式(空格用" "表示即可,无需转义)。默认情况下,sep=" \t\v\f\n",当str.split()函数运行时,默认情况下遇到上述字符,就会将字符串左右分开。
  2. 可以定制sep,使用sep指定的字符分隔字符串,但只能把sep字符串作为一个整体使用。
  3. 默认情况下str.split()将按默认分隔符分开所有可分隔子字符串,maxsplit指定最多将字符串分成多少个子字符串 ( 从源字符串的左侧到右侧进行分割,达到maxsplit 时结束分割 )。
  4. 结果中的空字符串将被移除。
  5. 该函数返回一个列表,包含所有分隔后的子字符串。

下面给出一个例子:

>>> s='This is\tsample text! \n It includes some whitespace characters.'
>>> s.split()
['This', 'is', 'sample', 'text!', 'It', 'includes', 'some', 'whitespace', 'characters.']
# 我们看到,字符串s被去掉了空格符,并且分成了单词形式
# 但其中的标点符号尚未被去除,下面讲str.strip()函数时,可将其去掉

>>> sep="\n"  #指定sep为换行符
>>> s.split(sep)
['This is\tsample text! ', ' It includes some whitespace characters.']
# 上面的代码按照换行符将字符串"\n"分成了两个子字符串
# sep 可以是多个字符,但只能把它们用作一个整体
>>> sep="sample"
>>> s.split(sep)
['This is\t', ' text! \n It includes some whitespace characters.']
# "sample"被用作分隔符,在子字符串中不再出现
>>> s.split(maxsplit=3)
['This', 'is', 'sample', 'text! \n It includes some whitespace characters.']
# 按照空白字符分成了3个子字符串,最后面的子字符串还可以分割,但已
# 达到最大分割数,不再进一步分割了

接下来,让我们看看str.strip()函数

S.strip([chars]) -> str
# 返回一个新字符串,默认情况下,
# 它是由字符串S去掉前后的空白字符得到的
# (chars没有给出,或chars=None)
# 假如给出了chars,且它非None
# 将字符串S移除chars中给出的字符,
# 然后返回得到的新字符串

下面是一个例子:

>>> s="\nThis is a\ttest string.\t"
>>> s.strip()
'This is a\ttest string.' #去掉了前后的空白字符

我们可以定制[chars],使它可以去除我们想要删掉的字符,如上文str.split()得到的单词列表的例子:

>>> s='This is\tsample text! \n It includes some whitespace characters.'
>>> s=s.split()
>>> s
['This', 'is', 'sample', 'text!', 'It', 'includes', 'some', 'whitespace', 'characters.']
# 单词中的标点符号尚未去掉,我们通过构造
# str.strip()函数的chars参数,
# 将标点符号去掉
>>> puncs=".!?\"\'"  # 可以去掉头尾的. ! ? " '等标点符号
>>> s=[word.strip(puncs) for word in s]
>>> s
['This', 'is', 'sample', 'text', 'It', 'includes', 'some', 'whitespace', 'characters']
# 可以看到!和.被去掉了

最后,让我们看看str.upper()和str.lower()函数。它们分别表示将字符串转换为大写字母和小写字母。当我们需要不区分字母的大小写形式而对字符串进行处理时,非常有用。如,类似前面所举的例子:

>>> s="Jack loves studying.\n Henry loves studying, too.\n Henry loves Jack. Jack loves Henry too."
>>> s.lower().count("Henry")
# 可以找到s中Henry不区分大小写出现的次数
3

现在,字符串的操作函数的主要内容介绍完了,下面让我们看两个实例:

例子:字符画动态图

下面,我列出一个Python程序作为例子,它实现了一个字符画的动态图,里面使用了一些没有讲过的知识,你能结合现有的知识弄懂它的原理吗? 请为这段代码填上注释:

import os   # 导入os模块

images=[]  # 构造一个空列表,命名为images
for i in range(1, 197): # 构造了001.txt~196.txt等文件名
    if len(str(i))==1: name="00"+str(i)
    if len(str(i))==2: name="0"+str(i)
    if len(str(i))==3: name=str(i)
    with open(name+".txt", mode='r') as f: # 以只读方式打开一个文本文件
        image=f.readlines()  # 将文件中所有的文本行读入image,这是一幅静态字符图像
        images.append(image) # 将image图像放入images列表,后者包含多个图像
    f.close()  # 关闭打开的文件
os.system("clear") # 清屏
while True:  # 一直运行
    for image in images:  # 对于image中的每个图像
        os.system("clear") # 先清屏
        for line in image:  # 对于每幅图像中的文本行
            print(line, end="") # 打印该行,不打印多加上的换行符
        print("") # 打印一个换行符
        os.system("sleep 0.3")  # 打印一幅图像后,停顿0.3秒

可以使用下面的方法在Python命令行中逐行运行Python代码,随时查看各种信息

$ python  # 在Linux终端窗口的命令行中键入python,可以用交互式运行Python代码
          # 所谓交互式,就是你运行一行Python代码,它会向你返回这行代码的执行结果
          # 然后你继续键入下一行代码,它又返回该结果,如此往复
Python 3.6.1 (default, Mar 27 2017, 00:27:06) 
[GCC 6.3.1 20170306] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>                      # 这是Python提示符,可以在它后面输入一行Python代码
                         # 你可以逐行输入上一个例子的Python代码,运行之
                         # 且随时查看Python代码中对象和名字的含义
                         # 如:
>>> import os
>>> type(os)
<class 'module'>         # os是一个模块(module)类(class)
>>> dir(os)              # 列出os中定义的名字
...
...
...
>>> help(os)   # 列出os的帮助文件, ☺现在看不懂没关系
>>> help(os.system)  # 我们现在只需要知道这个函数的功能就可以了
system(command)      # 函数原型 command是参数,表示一个命令
    Execute the command in a subshell.  # 在一个子shell中运行command命令
                                        # 所以os.system("clear") 是运行clear命令
                                        # os.system("sleep 0.3") 是运行sleep 0.3命令
# 其他代码也可以用这种方式解读                                        

例子: 列出英文文本词频

下面给出了统计英文文本中不同单词出现频率的一个例子,同样用到了一些没有讲过的知识。

s='''
As tens of millions marvel at a 16-year-old's prowess with verse, questions are raised about where country is getting it wrong in teaching culture

Soon after the program Rendezvous With Chinese Poetry returned to television screens two months ago, it was pulling in the kinds of audiences that you would normally only associate with a top-notch reality TV show.

Indeed, after its 10-episode run during the Spring Festival period, the China Central Television show's producers were able to boast that it had attracted an aggregate of more than 1.2 billion viewers.

Those figures are all the more astonishing given the program's very modest aims: to encourage the public "to appreciate classic Chinese poetry, look to their cultural roots and enjoy the beauty of life".

It had aired for the first time a year earlier, and no sooner had it returned than it seemed almost everyone in the country was talking about the program, which invites poetry lovers to vie with each other to see who knows the most about classic Chinese poetry.

By the end of the series, messages about it on Sina Weibo, a Chinese version of Twitter, had been read more than 90 million times, and videos from the program had received nearly 6 million clicks online, CCTV said.

Helping drive the program's popularity was Wu Yishu, 16, a high school student from Shanghai who made her way to the finals, and in doing so captivated millions with knowledge of classic Chinese poetry that she combined with calmness, elegance and a razor-sharp mind.

Eventually Wu would be the outright winner, beating dozens of rivals of various age groups, including her ultimate opponent, a poetry magazine editor.

"The program has enhanced our appreciation of the beauty of classic poetry and traditional culture," says Beijing's Wen Chen, 30, adding that many of his entourage were as enamored of the program as he was.
'''
puncs="\"\',.?!\n"
words=s.split()
words=[sub.strip(puncs).lower() for sub in words]
wordset=set()  # 构造一个集合
for word in words:
    wordset.add(word)  # 把列表中的单词放入集合,重复的单词不会被放入集合
for word in wordset:
    print("%s : %d\n" % (word, words.count(word)))

练习

  1. 在Python命令行下,逐行输入教程中词频分析的代码,理解每行代码的作用,必要时使用id()、type()、dir()、help()等函数进行分析。

4.3 正则表达式

1. 入门

上文中,我们使用str.find()对字符串中的子字符串进行了查找。在一个字符串中查找子字符串的应用场景非常广泛,但str.find()最多只能做到忽略大小写进行精确的字符串匹配,它不能达到模糊匹配字符串的效果,而后者才是大量使用的应用场景。如,如何方便地找出下面这个字符串中的所有电话号码 ?

s='''欢迎来到梦想大学附中,你可以拨打 2468-2712410 联系上网事宜,你可以拨打 2468-2711112 联系附中校长。你可以拨打 2468-2710110联系安保部门。你可以拨打 2468-2712828 联系宿舍管理员。'''

str.find()显然不能胜任了。

下面,我们介绍正则表达式,它可以模糊匹配字符串。你可以把它理解为通过给出一组子字符串共有的格式构造一个正则表达式,利用这个正则表达式在给定的字符串中查找符合这个格式的子字符串。比如,上文列出的字符串中的电话号码可以用如下方式找出:

>>> import re              # 导入正则表达式库,re = 'regular expression'的首字母缩写,类似
                           # C语言中的 #include <...>语句
>>> pattern='\d{4}-\d{7}'  # 将一个格式字符串赋给pattern,
                           # 这个字符串给出了正则表达式遵守的格式
                           # 可以把\d理解成占位符,表示一个10进制数位
                           # {4}跟在\d后面,表示有4个\d
                           # - 是一个常规字符,它的位置说明它出现在4位
                           # 十进制数和7位十进制数之间
>>> regex = re.compile(pattern)  # 将pattern编译成一个正则表达式,赋给regex,可以将其理解为
                                 # 所有符合pattern格式的字符串集合
>>> type(regex)
<class '_sre.SRE_Pattern'>  # 看一下regex的类型
>>> regex                   # 看一下regex的值
re.compile('\\d{4}-\\d{7}')
>>> #coding: utf-8          # 使用utf-8编码以支持中文
>>> s='''欢迎来到梦想大学附中,你可以拨打 2468-2712410 联系上网事宜,你可以拨打 2468-2711112 联系附中校长。你可以拨打 2468-2710110联系安保部门。你可以拨打 2468-2712828 联系宿舍管理员。'''
>>> result=regex.findall(s) # 在字符串s中查找所有符合正则表达式regex的子字符串
>>> result
['2468-2712410', '2468-2711112', '2468-2710110', '2468-2712828']  # 返回了一个包含所有电话号码的列表

上面我们讲到,正则表达式字符串pattern给出了待查找字符串的格式,但实际上符合这个pattern格式的字符串比我们这里出现的电话号码本身的格式要多,所有满足dddd-ddddddd形式的字符串都会被找出来。例如,如果s中出现了"3769-2514783",也会把它找出来,而我们需查找的字符串格式可能形如"2468-271dddd"。也就是说,正则表达式的表达范围"宽于"实际所需找出的字符串。这种情况常会出现,但通常不会产生问题,因为它会扩大我们的查找范围,正确结果必定出现在查找结果中,而干扰项在源字符串中常常不出现,从而使我们恰好得到完全正确的结果。但当源字符串中有干扰项出现的情况下,我们不能放宽正则表达式,而必须正确编写它的格式字符串,使它符合要求。比如,在这里我们可以使用

pattern="2468-271\d{4}"  # 字符串中的"2468-271"必须出现在子字符串的开头

作为格式字符串,这样,即使s中出现"3769-2514783"这样的字符串,也不会出现在结果中了。

与之相反的是,有时我们正则表达式的表达范围"窄于"实际所需查找的字符串,也不能达到我们想要的结果。

2. 如何编写格式字符串

我们将分别从原子、元字符、模式修正符、贪婪模式与懒惰模式等方面介绍如何编写格式字符串。

原子

原子是正则表达式中最基本的组成单位,每个正则表达式中至少要包含一个原子,常见的原子有以下几类:

  • 普通字符
  • 不可打印字符
  • 通用字符
  • 原子表。
普通字符作为原子

普通字符指数字、大小写字母、下划线等字符,如: Python__main__123go等。普通字符出现在格式字符串中,表示相应位置上必须用这些字符进行匹配,如:

>>> import re
>>> pattern="Python"
>>> regex=re.compile(pattern)
>>> s="Python is an outstanding programming language. I like Python."
>>> result=regex.findall(s)
>>> result
['Python', 'Python']  # 找到了两个结果
不可打印字符作为原子

上文讲过不可打印字符及对其转义的表示方法。如换行符用"\n"表示,回车符用"\r"表示,响铃符用"\a"表示,水平制表符用"\t"表示,这些转义字符也可以作为原子出现在正则表达式的格式字符串中,如:

>>> import re
>>> pattern="\n"
>>> regex=re.compile(pattern)
>>> s='This is a multiline text.\nThis is the second line.\nThis is the third line.\nThis is the last line\n'
>>> result=regex.findall(s)
>>> result
['\n', '\n', '\n', '\n']
通用字符作为原子

通用字符类似Linux命令行中常用的通配符,一个通用字符可以表示一类字符,它和下面将要介绍的元字符是正则表达式可以模糊匹配字符串的核心所在。下表列出了常用的通用字符:

符号 含义
\w 匹配任意一个字母、数字或下划线
\W 匹配字母、数字、下划线之外的任意一个字符
\d 匹配任意一个十进制数码
\D 匹配除十进制数码之外的任意一个字符
\s 匹配任意一个空白字符
\S 匹配除空白字符之外的任意一个其他字符

举个例子:

>>> import re
>>> pattern = 'Python\d.x'
>>> regex=re.compile(pattern)
>>> s="Among all the Python programs, some use Python2.x, some use Python3.x, the others use the rarely seen Python1.x."
>>> result=regex.findall(s)
>>> result
['Python2.x', 'Python3.x', 'Python1.x'] #前面的Python并不会被匹配
原子表

有时我们需要在一组字符中进行多选一的匹配,这可以使用原子表方便地实现。原子表的语法如下:

[chars]  # chars是一组字符,任选其中一个字符进行匹配

如:

>>> import re
>>> pattern = '[pP]ython\d.[xX]' # 从p和P/x和X中任选1个进行匹配
>>> regex=re.compile(pattern)
>>> s="Among all the Python programs, some use python2.x, some use Python3.X, the others use the rarely seen python1.X."
>>> result=regex.findall(s)
>>> result
['python2.x', 'Python3.X', 'python1.X'] #前面的Python并不会被匹配

元字符

所谓的元字符,就是正则表达式中具有一些特殊含义的字符,比如重复N次前面的字符等。

下表列出了一些常见的元字符:

符号 含义
. 匹配除换行符以外的其他任意字符
^ 匹配字符串的开始位置
$ 匹配字符串的结束位置
* 匹配0次、1次或多次前面的原子
? 匹配0次或1次前面的原子
+ 匹配1次或多次前面的原子
{n} 前面的原子恰好出现n次
{n, } 前面的原子至少出现n次
{m, n} 前面的原子至少出现m次,至多出现n次
| 模式选择符
() 模式单元符
任意匹配元字符

首先讲解任意匹配元字符“.”,我们可以使用“.”匹配一个除换行符以外的任意字符。比如,我们可以使用正则表达式".python..."匹配一个"python"字符前面有1位,后面有3位格式的字符,这前面的1位和后面的3位可以是除了换行符以外的任意字符。

>>> import re
>>> pattern=".Python..."
>>> regex=re.compile(pattern)
>>> s="aPythonApp is a Python application"
>>> result=regex.findall(s)
>>> result
['aPythonApp', ' Python ap']
边界限制元字符

接下来,讲解边界限制符,可以使用“^”匹配字符串的开始,使用“$”匹配字符串的结束。

>>> import re
>>> pattern="^.Python..."
>>> regex=re.compile(pattern)
>>> s="aPythonApp is a Python application"
>>> result=regex.findall(s)
>>> result
['aPythonApp']  # 只有开头的子串被匹配了,
                # $的用法类似,只不过放在格式字符串的最后
限定符

接下来讲解限定符的使用,限定符也是元字符中的一种,常见的限定符包括*、?、+、{n}、{n,}、{m,n}。

# 正则表达式默认情况下使用贪婪算法,即匹配使正则表达式成立的最长字符串
>>> import re
>>> s="aPythonApp is a Python application"
>>> pattern1=".Python.*"         # Python前面有1个除换行符外的任意字符,
                                 # 后面有任意多个除换行符外的任意字符
                                 # 贪婪模式使它匹配的源字符串的尾端
                                 # 因为整个字符串都被匹配了,所以
                                 # " Python"及其后面的字符串不会被重复匹配
>>> regex1=re.compile(pattern1)
>>> result1=regex1.findall(s)
>>> result1
['aPythonApp is a Python application']
>>> pattern2=".Python.?"         # 贪婪模式下,?默认按照1个字符匹配,而不是0个字符,
                                 # 但源字符串尾端可能按0个字符匹配
>>> regex2=re.compile(pattern2)
>>> result2=regex2.findall(s)
>>> result2
['aPythonA', ' Python ']
>>> pattern3=".Python.+"         # 解释类似上面.*的那个,只是+至少匹配一个字符
>>> regex3=re.compile(pattern3)
>>> result3=regex3.findall(s)
>>> result3
['aPythonApp is a Python application']
>>> pattern4=".Python.{3}"      # .{3}匹配除换行符外的任意3个字符
>>> regex4=re.compile(pattern4)
>>> result4=regex4.findall(s)
>>> result4
['aPythonApp', ' Python ap']
>>> pattern5=".Python.{3,}"      # .{3, }至少匹配3个除换行符以外的任意字符
>>> regex5=re.compile(pattern5)
>>> result5=regex5.findall(s)
>>> result5
['aPythonApp is a Python application']  # .{2,5} 匹配2~5个除换行符以外的任意字符
>>> pattern6=".Python.{2,5}"
>>> regex6=re.compile(pattern6)
>>> result6=regex6.findall(s)
>>> result6
['aPythonApp i', ' Python appl']

有了通用字符和元字符,我们可以很方便地制作一段用于验证网站用户名和登录密码的Python代码:

#coding: utf-8
import re

user=input("请输入用户名 ")
pswd=input("请输入密码 ")

patUser='^\w{8,14}$'
patPswd='^\w{8,26}$'

matchUser=re.match(patUser, user)
if matchUser:
    print("你的用户名符合要求")
else:
    print("用户名由8-14位字母、数字或下划线组成")
matchPswd=re.match(patPswd, pswd)
if matchPswd:
    print("你的密码符合要求")
else:
    print("密码由8-26位字母、数字或下划线组成")

实际应用中,当用户输入不正确的用户名或密码时,重新跳转到输入用户名或密码的步骤,当输入次数超过限制之后,禁止用户登录就可以了。

模式选择符

接下来,讲解模式选择符"|",使用模式选择符,可以设置多个模式,匹配时,可以从中选择任意一个模式匹配。比如正则表达式"python|php"中,字符串"python""php"均满足匹配条件。

模式单元符

可以使用模式单元符"( )"将一些原子组合成一个大原子使用,小括号括起来的部分会被当做一个整体去使用。例如:

pattern='^(hello ){3,10}world!$' # 匹配开头有3-10个"hello ",然后跟一个"world"结尾的字符串

贪婪模式和懒惰模式

正则表达式分贪婪模式和懒惰模式,它们是由格式字符串说明的。

贪婪模式

所谓贪婪模式就是匹配字符串时,按照格式字符串所能匹配的最大长度进行匹配。例如:

>>> import re
>>> pattern=".*Python"     # 贪婪模式
>>> s="This is a Python example. It shows that I love Python best."
>>> regex=re.compile(pattern)
>>> result=regex.findall(s)    
>>> result
['This is a Python example. It shows that I love Python']   # 一直匹配到第二个Python,
                                                            # 而不会在第一个Python处停下来
懒惰模式

懒惰模式与此相反,当正则表达式处于懒惰模式时,当匹配到首个符合要求的子字符串时,就停止继续匹配,还是上面的例子,不过,我们的格式字符串使用了懒惰模式:

>>> import re
>>> pattern=".*?Python"     # 懒惰模式,比贪婪模式多了一个?,表示匹配0次或1次前面的字符
>>> s="This is a Python example. It shows that I love Python best."
>>> regex=re.compile(pattern)
>>> result=regex.findall(s)    
>>> result
['This is a Python',' example. It shows that I love Python']   # 一直匹配到第一个Python,就停下来了

在网络爬虫中常常使用正则表达式的懒惰模式进行匹配,比如查找一个网页中的所有图片链接,然后用下载函数把它们下载下来。

我们上网时经常访问Web站点,通过浏览器,我们可以看到丰富的文字、图片、音频、视频。设计优良的Web站点,常常给我们眼前一亮的惊艳感觉。但是,你知道吗,我们访问的这些网页,其实是通过浏览器发出访问请求后(比如: 跳转到一个页面),Web服务器返回给我们的一个文本文件,通常它以.html或.htm结尾,称作html文件。html文件本质上和其他文本文件没有什么不同,只是它以称为html语言的Web编程语言进行编写,通过称作html标签的工具对我们想要呈现的内容进行了编排,再通过符合标准的网络浏览器对它们进行可视化,就得到了我们通常所见到的网页效果。

由于html文件或称网页文件是一个文本文件,因此,我们可以很容易编制计算机程序对它进行自动处理。常见的一个应用是网络爬虫。网络爬虫可以从一个网站的起始页面开始,提取其中的链接,进行逐个访问,分析每个页面的内容,从而爬取整个网站中我们感兴趣的内容。实际上,我们常用的搜索引擎,都自主研发了专业的网络爬虫,每时每刻都在爬取信息,放到搜索引擎的服务器上供我们搜索使用(想像一下,搜索引擎的网络爬虫需要满足什么样的严苛要求? )。搜索引擎使用的是通用型爬虫,它什么样的信息都爬。还有专用的网络爬虫,它们以爬取某一方面的信息为主。比如,有爬取股票信息的爬虫、爬取购物网站信息的爬虫、爬取图片的爬虫、爬取笑话和段子的爬虫、爬取视频和音乐的爬虫、爬取QQ和微信用户信息的爬虫、爬取知乎、豆瓣、去哪儿等网站信息的爬虫等。

下面这个http链接是在搜狗图片网站搜索"海滩"得到的html页面,页面文件中包含了海滩图片的原始链接,我们下面把这些链接提取出来:

在浏览器地址栏中键入http://pic.sogou.com/pics?query=海滩,可以看到它列出了一堆图片,按下Ctrl+u组合键,可以查看这个网页的源代码。在其中,我们看到图片的原始链接形如: "pic_url":"http://himg2.huanqiu.com/attachment2010/2012/0727/20120727090239785.jpg"

s=... # 网页文件的源代码
patImgURL='"pic_url":"http://(.+?\.jpg)"'  # 懒惰模式,否则会找到网页中最后一个.jpg处停止
regex=re.compile(patImgURL)
imgURLs=regex.findall(s)
for url in imgURLs:
    print(url)
'''

该程序的结果如下所示:

$ python links.py
himg2.huanqiu.com/attachment2010/2012/0727/20120727090239785.jpg
pica.nipic.com/2007-11-14/2007111495634824_2.jpg
picm.bbzhi.com/qitabizhi/pinpaidiannaomorenzhuomianhaitanpian/pinpaidiannaomorenzhuomianhaitanpian_488885_m.jpg
himg2.huanqiu.com/attachment2010/2012/0727/20120727090234617.jpg
pic3.bbzhi.com/fengjingbizhi/xiarihaitanlantianbaiyunhai/show_fengjingta_306541_10.jpg
img2.fengniao.com/product/32/400/cesL3TfrcYDZM.jpg
pic22.nipic.com/20120720/10540108_221106395000_2.jpg
...

通过这个例子,我们结束了正则表达式的,现在你对它的感觉好一点了吗?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容