Python爬虫开发:从入门到实战(微课版)
上QQ阅读APP看书,第一时间看更新

2.3 Python的数据结构和控制结构

2.3.1 整数、浮点数和变量

1.整数与浮点数

Python里面的整数和数学里面的整数定义是一样的,Python里面的浮点数可以看作是数学里面的小数。在Python中使用print函数打印一个整数或者浮点数,可以看到这个整数或者浮点数被原样打印了出来:

    >>> print(1234)
    1234
    >>> print(3.14159)
    3.14159

整数的加、减、乘可以直接在print中进行,也可以通过括号来改变运算的优先级:

    >>> print(1-10)
    -9
    >>> print(3+2-5 * 0)
    5
    >>> print((3+2-5) * 0)
    0

在PyCharm中的运行效果如图2-20所示。

图2-20 使用Python进行加、减、乘运算

上面的例子说到了整数的加、减、乘,那整数的除法呢?浮点数的加、减、乘、除呢?如果在Python中打印“0.1+0.2”的结果,会得到什么呢?例如下列代码:

    >>> print(0.1+0.2)
    0.30000000000000004

结果并不是0.3,而是一个很长的浮点数。这不是Python的问题,Java、C语言、C++等各种语言都有这个问题。这是由于计算机里面浮点数的储存机制导致的。有兴趣的读者可以了解一下浮点数从十进制转化为二进制的原理和结果。

由于这个原因,不应该直接使用Python来进行精确的计算,但是进行日常的精度不高的四则运算是没有问题的,如图2-21所示。在图2-21中,第7行使用#号开头的内容表示注释,Python在运行的时候会自动忽略#号后面的内容。

图2-21 使用Python进行整数的除法和浮点数的加、减、乘、除运算

2.变量

所谓变量,可以理解为一个存放其他数据的盒子。使用变量可以减少重复输入。例如在Python中计算一个长方体的底面积和体积,代码如图2-22所示。

图2-22 在Python中计算长方体的底面积和体积

在图2-22的代码中,变量在等号的左边,变量里面将要存放的值在等号的右边。等号是赋值的意思。将等号右边的值赋给左边的变量,这样变量里面的值就等于右边了。而如果等号的右边也是一个变量,那么就把等号右边的变量里面的值赋给等号左边的变量。

虽然在Python 3中可以使用中文作为变量名,但还是建议读者将变量名设置为英文。

    >>> length = 10
    >>> width = 5
    >>> height = 2
    >>> area = length * width
    >>> volume = area * height
    >>> print(area)
    50
    >>> print(volume)
    100

2.3.2 字符串、列表、元组

1.字符串(String)

在Python中,除了整数和浮点数外,还有字符串。任何被单引号或者双引号括起来的内容都可以认为是字符串。字符串也可以赋值给变量。

    string_1 = ’我是一个字符串’ #字符串可以是中文或者任何其他语言
    string_2 = 'I am a string'
    string_3 = '' #空字符串
    string_4 = ' ' #空格
    string_5 = 'a' #字符串可以只有一个字母
    string_6 = '123' #字符串型的数字
    string_7=’我是字符串I am a string 12345'
    string_8 = "我是用双引号括起来的字符串,我和单引号括起来的字符串没有区别"

从上面的8行代码中可以看到,字符串的内容可以是中文,可以是英文,可以是数字,可以是空格,可以是中文、英文、数字和空格的组合。

需要注意的是,字符串形式的数字和普通的数字是不一样的,它们不相等。例如如下代码:

    string_6 = '123'
    int_variable = 123

2.列表(List)

列表是Python里面的容器之一,由方括号和方括号括起来的数据构成。里面的数据可以是整数、浮点数、字符串,也可以是另一个列表或者其他的数据结构。列表里面的每一项叫作列表的一个元素,每个元素之间使用英文逗号隔开:

    list_1 = [1, 2, 3, 4, 5] #列表里面有5个元素,全部是数字
    list_2 = ['abc', 'x', '', 'kkk'] #列表里面有4个元素,全部是字符串
    list_3 = [] #空列表
    list_4 = [123, 'xyz', 3.14, [1, 2, 'yy']] #由多种元素组合起来的列表

3.元组(Tuple)

元组是Python里面的容器之一,由小括号和小括号括起来的数据构成。它的外型和列表非常像,只不过列表使用的是方括号,元组使用的是小括号。“元组”中的“元”和“二元一次方程”中的“元”是同一个意思,“组”就是组合的意思。

    tuple_1 = (1, 2, 3, 4, 5) #元组里面有5个元素,全部为数字
    tuple_2 = ('abc', 'x', '', 'kkk') #元组里面有4个元素,全部是字符串
    tuple_3 = () #空元组
    tuple_4 = (123, 'xyz', [1, 't', 'z'], ('o', 'pp')) #由多种元素组合起来的元组

元组和列表的区别:列表生成以后还可以往里面继续添加数据,也可以从里面删除数据;但是元组一旦生成就不能修改。如果它里面只有整数、浮点数、字符串、另一个元组,就既不能添加数据,也不能删除数据,还不能修改里面数据的值。但是如果元组里面包含了一个列表,那么这个元组里面的列表依旧可以变化。

2.3.3 数据的读取

之所以要把字符串、列表和元组放在一起介绍,是因为可以使用完全一样的方式从这3个数据结构中读取数据,如图2-23所示。

图2-23 字符串、列表和元组的读取方法完全相同

图2-23中给出了13个例子。在这13个例子中,字符串、列表和元组的操作完全相同。

1.指定下标

在大多数编程语言里面,下标都是从0开始的,Python也不例外。第0个元素就是指最左边的元素。

    example_string = ’我是字符串’

在字符串中,第0个字符是“我”字,第1个字符是“是”字,以此类推。

    example_list = [’我’, ’是’, ’列’, ’表’]

在列表中,第0个元素是“我”字,第1个元素是“是”字,以此类推。

    example_tuple = (’我’, ’是’, ’元’, ’组’)

在元组中,第0个元素是“我”字,第1个元素是“是”字,以此类推。

在这3数据结构中,想取任何一个元素,都可以直接使用:

    变量名[下标]

例如:

    >>> print(example_string[0])
    我
    >>> print(example_list[1])
    是
    >>> print(example_tuple[2])
    元

-1表示最后一个元素,-2表示倒数第2个元素,-3表示倒数第3个元素,以此类推,所以:

    >>> print(example_string[-1])
    串
    >>> print(example_list[-2])
    列
    >>> print(example_tuple[-3])
    是

2.切片操作

字符串切片以后的结果还是字符串,列表切片以后的结果还是列表,元组切片以后的结果还是元组。切片的格式为:

    变量名[开始位置下标:结束位置下标:步长]

其中“开始位置下标”“结束位置下标”“步长”可以部分省略,但是不能全部省略。这3个参数对应的都是数字。切片的结果包括“开始位置下标”所对应的元素,但是不包括“结束位置下标”所对应的元素。

省略“开始位置下标”,表示从下标为0的元素开始计算。省略“结束位置下标”,表示直到最后一个元素且包含最后一个元素。例如:

    >>> print(example_string[1:3]) #读取下标为1和下标为2的两个字符
    是字
    >>> print(example_list[:3]) #读取下标为0、1、2的3个元素
    我是列
    >>> print(example_tuple[2:]) #读取下标为2的元素和它后面的所有元素
    元组

省略“开始位置下标”和“结束位置下标”,“步长”取-1,表示倒序输出,例如:

    >>> print(example_string[::-1])
    串符字是我

3.拼接与修改

字符串与字符串之间可以相加,相加表示两个字符串拼接起来。例如:

    >>> string_1 = ’你好’
    >>> string_2 = ’世界’
    >>> string_3 = string_1+string_2
    >>> print(string_3)
    你好世界

元组与元组之间也可以相加,相加表示两个元组拼接起来。例如:

    >>> tuple_1 = ('abc', 'xyz')
    >>> tuple_2 = (’哈哈哈哈’, ’嘿嘿嘿嘿’]
    >>> tuple_3 = tuple_1+tuple_2
    >>> print(tuple_3)
    ('abc', 'xyz', ’哈哈哈哈’, ’嘿嘿嘿嘿’)

列表与列表之间也可以相加,相加表示两个列表拼接起来。例如:

    >>> list_1 = ['abc', 'xyz']
    >>> list_2 = [’哈哈哈哈’, ’嘿嘿嘿嘿’]
    >>> list_3 = list_1+list_2
    >>> print(list_3)
    ['abc', 'xyz', ’哈哈哈哈’, ’嘿嘿嘿嘿’]

特别的,可以通过列表的下标来修改列表里面的值,格式为:

    变量名[下标]= 新的值

例如:

    >>> existed_list = [1, 2, 3]
    >>> existed_list[1] = ’新的值’
    >>> print(existed_list)
    [1, ’新的值’, 3]

列表还可以单独在末尾添加元素,例如:

    >>> list_4 = ['Python', ’爬虫’]
    >>> print(list_4)
    ['Python', ’爬虫’]
    >>> list_4.append(’一’)
    >>> print(list_4)
    ['Python', ’爬虫’, ’一’]
    >>> list_4.append(’酷’)
    >>> print(list_4)
    ['Python', ’爬虫’, ’一’, ’酷’]

这个特性非常有用,在爬虫开发中会大量使用,一定要掌握。

元组和字符串不能添加新的内容,不能修改元组里面的非可变容器元素,也不能修改字符串里面的某一个字符。

字符串、列表和元组还有一些其他特性,它们之间的互相转化将在爬虫开发的过程中逐渐介绍。

2.3.4 字典与集合

1.字典

字典就是使用大括号括起来的键(Key)值(Value)对(Key-Value对)。每个键值对之间使用英文逗号分隔,每个Key与Value之间使用英文冒号分隔。例如:

    dict_1 = {'superman': ’超人是一个可以在天上飞的两足兽’, ’天才’: ’天才跑在时代的前面,把时代拖得气喘吁吁。', 'xx': 0, 42:
    '42是一切的答案’}

Key可以使用中文、英文或者数字,但是不能重复。Value可以是任意字符串、数字、列表、元组或者另一个字典,Value可以重复。

可以通过Key来从字典中读取对应的Value,有3种主要的格式:

    变量名[key]
    变量名.get(key)
    变量名.get(key, ’在找不到key的情况下使用这个值’)

例如:

    >>> example_dict = {'superman': ’超人是一个可以在天上飞的两足兽’, ’天才’: ’天才跑在时代的前面,把时代拖得气喘吁吁。',
    'xx': 0, 42: '42是一切的答案’}
    >>> print(example_dict[’天才’])
    天才跑在时代的前面,把时代拖得气喘吁吁。
    >>> print(example_dict.get(42))
    42是一切的答案
    >>> print(example_dict.get(’不存在的key'))
    None
    >>> print(example_dict.get(’不存在的key', ’找不到’))
    找不到

使用方括号的方式来读取字典的Value时,一定要保证字典里面有这个Key和它对应的Value,否则程序会报错。使用get来读取,如果get只有一个参数,那么在找不到Key的情况下会得到“None”;如果get有两个参数,那么在找不到Key的情况下,会返回第2个参数。

如果要修改一个已经存在的字典的Key对应的Value,或者要往里面增加新的Key-Value对,可以使用以下格式:

    变量名[key] = ’新的值’

如果Key不存在,就会创建新的Key-Value对;如果Key已经存在,就会修改它的原来的Value。例如:

    >>> existed_dict = {'a': 123, 'b': 456}
    >>> print(existed_dict)
    {'b': 456, 'a': 123}
    >>> existed_dict['b'] = ’我修改了b'
    >>> print(existed_dict)
    {'b': ’我修改了b', 'a': 123}
    >>> existed_dict['new'] = ’我来也’
    >>> print(existed_dict)
    {'b': ’我修改了b', 'a': 123, 'new': ’我来也’}

需要特别注意的是,字典的Key的顺序是乱的,所以不能认为先添加到字典里面的数据就排在前面。

2.集合

集合是使用大括号括起来的各种数据,可以看作没有Value的字典。集合里面的元素不能重复。集合也是无序的。

    example_set = {1, 2, 3, 'a', 'b', 'c'}

集合最大的应用之一就是去重。例如,把一个带有重复元素的列表先转换为集合,再转换回列表,那么重复元素就只会保留一个。把列表转换为集合需要使用set()函数,把集合转换为列表使用list()函数:

    >>> duplicated_list = [3, 1, 3, 2, 4, 6, 6, 7, 's', 's', 'a']
    >>> unique_list = list(set(duplicated_list))
    >>> print(unique_list)
    [1, 2, 3, 4, 's', 6, 7, 'a']

由于集合与字典一样,里面的值没有顺序,因此使用集合来去重是有代价的,代价就是原来列表的顺序也会被改变。

2.3.5 条件语句

1.if语句

if这个关键字正如它的英文一样,是“如果”的意思,即如果什么情况发生,就怎么样。if的用法如下:

    if可以判断真假的表达式或者是能被判断是否为空的数据结构:
      在表达式的条件为真时运行的代码

所有需要在if里面运行的代码都需要添加至少一个空格作为缩进,一般约定俗成用4个空格,从而方便人眼阅读。一旦退出缩进,新的代码就不再属于这个if。

例如:

    a = 1
    b = 2
    if a+b == 3:
      print(’答案正确’)
    print(’以后的代码与上面的if无关’)

只有在“a+b”的值等于3的时候,才会打印出“答案正确”这4个字。注意这里的表达式可以是进行普通运算的表达式,也可以是后面将要讲到的函数。但是无论a+b的值是多少,后面那一句“以后的代码与上面的if无关”都会被打印出来。

if后面的表达式可以有一个,也可以有多个。如果有多个,就使用and或者or连接。

(1)and表示“并且”,只有在使用and连接的所有表达式同时为真时,if下面的内容才会运行。

(2)or表示“或者”,只要使用or连接的所有表达式中至少有一个为真时,if后面的内容就会运行。

如下代码为and和or的使用方法:

    if 1+1 == 2 and 3+3 == 6:
      print(’答案正确’)
    if 1+1 == 5 or 3+3 == 6:
      print(’答案正确’)

2.短路效应

(1)在使用and连接的多个表达式中,只要有一个表达式不为真,那么后面的表达式就不会执行。(2)在使用or连接的多个表达式中,只要有一个表达式为真,那么后面的表达式就不会执行。

这个短路效应有什么作用呢?来看看下面的代码:

    name_list = []
    if name_list and name_list[100] == ’张三’:
      print('OK')

从一个空列表里面读下标为100的元素,显然会导致Python报错,但是像上面这样写却不会有任何问题。这是因为如果name_list为空,那么这个判断直接就失败了。根据短路效应,取第100个元素的操作根本就不会执行,也就不会报错。只有这个列表里面至少有一个元素的时候,才会进入第2个表达式“name_list[100] == ’张三’”的判断。

同理,or的短路效应的表达式如下:

    if 1+1 == 2 or 2+'a' == 3 or 9+9 == 0:
      print(’成功’)

if后面使用or连接了3个表达式,其中第2个表达式将数字和字符串相加。这个操作在Python里面显然是不合法的,一旦运行,就会导致报错。但是上面的代码运行起来却没有任何问题。这是由于第1个表达式1+1 = = 2显然是成立的,那么后面的两个表达式根本就不会被执行。既然不会被执行,当然就不会报错。

3.多重条件判断

对于多重条件的判断,需要使用“if...elif...else...”。其中,“elif”可以有0个,也可以有多个,但是else只能有0个或者1个。例如下面的代码:

    answer = 2
    if answer == 2:
      print(’回答正确’)
    else:
      print(’回答错误’)

“if...else...”主要用于非此即彼的条件判断。如果正确就执行第3行代码,如果错误就执行第5行代码。第3行和第5行只会执行其中之一,绝对不可能同时执行。

再看下面的代码:

    name = ’回锅肉’
    if name == ’回锅肉’:
      print('15元’)
    elif name == ’水煮肉片’:
      print('20元’):
    elif name == ’米饭’:
      print('1元’)
    elif name == ’鸡汤’:
      print('1角’)
    else:
      print(’菜单里面没有这道菜’)

上面这段代码实现了多重条件判断,在name为不同值的时候有不同的结果。如果if和elif里面的所有条件都不符合,就会执行else里面的情况。

请读者思考:

下面两段代码的运行结果有何不同?分别会打印出几个“OK”?

代码片段1:

    a = 1
    b = 2
    if a == 1:
      print('OK')
    elif b == 2:
      print('OK')

代码片段2:

    a = 1
    b = 2
    if a == 1:
      print('OK')
    if b == 2:
      print('OK')

4.使用字典实现多重条件控制

如果有多个if,写起来会很烦琐,例如下面这一段代码:

    if state == 'start':
      code = 1
    elif state == 'running':
      code = 2
    elif state == 'offline':
      code = 3
    elif state == 'unknown':
      code = 4
    else:
      code = 5

使用“if...elif...else...”会让代码显得冗长。如果使用字典改写,代码就会变得非常简洁:

    state_dict = {'start': 1, 'running': 2, 'offline': 3, 'unknown': 4}
    code = state_dict.get(state, 5)

2.3.6 for循环与while循环

所谓循环,就是让一段代码反复运行多次。例如把“爬虫”这个词打印5次,读者可能会先写一行代码,然后复制、粘贴:

    print(’扒虫’)
    print(’扒虫’)
    print(’扒虫’)
    print(’扒虫’)
    print(’扒虫’)

但是粘贴完后才发现把“爬虫”写成了“扒虫”,于是又要一行代码一行代码地去修改。这样的写法,不仅增加了大量重复的代码,还会使维护和重构变得很麻烦。为了解决这个问题,就有了循环。在上面的例子中,想把“爬虫”打印5次,只需要两行代码:

    for i in range(5):
      print(’爬虫’)

1.for循环

for循环的常见写法为:

    for x in y:
      循环体

先来看看Python独有的for循环写法。

从“可迭代”的对象中获得每一个元素,代码和运行结果如图2-24所示。

图2-24 读取列表中的每一个元素并打印

图2-24所示的是for循环从列表中取出每一个元素。将列表换成元组或者集合再运行代码,可以发现效果一样。

for循环也可以直接从字符串里面获得每一个字符,如图2-25所示。

图2-25 for循环读取字符串里面的每一个字符

这里的每一个汉字、每一个字母、每一个标点符号都会被for循环分开读取。循环第1次得到的是“大”,第2次得到的是“家”,第3次得到的是“好”,以此类推。

在做爬虫的时候会遇到需要把列表展开的情况,常犯的一个错误就是把字符串错当成了列表展开。这个时候就会得到不正常的结果。

for循环也可以把一个字典展开,得到里面的每一个Key,如图2-26所示。

图2-26 for循环获取字典每一个Key

这个循环一共进行了3轮,每一轮可以得到字典的一个Key。

再来看看几乎所有编程语言中都有的写法,如图2-27所示。

图2-27 最常见的按次数循环

通过指定range里面的数字,可以控制循环的执行次数。需要特别注意的是,i是从0开始的。

2.while循环

while循环主要用在不知道循环需要执行多少次的情况。这种情况下,要么让程序永远运行,要么在某个特定的条件下才结束,如图2-28所示。

图2-28 while循环运行10次

图2-28中代码的意义为,如果i的值小于10,那么就进入循环,打印一句话,然后让i增加1。使用while循环最常遇到的问题就是循环停不下来。如果忘记让i增加1,那么i就会永远小于10,循环也就永远停不下来了。读者可以把第4行代码注释以后运行,看看会出现什么样的效果。

当然,在某些特殊的情况下,确实需要循环永远运行,这个时候需要这样写:

    import time
    while True:
      你要执行的代码
      time.sleep(1)

如果要让循环永久运行,那么增加一个延迟时间是非常有必要的。time.sleep()的参数为一个数字,单位为秒。如果不增加这个延迟时间,就会导致循环超高速运行。在爬虫的开发过程中,如果超高速运行,很有可能导致爬虫被网站封锁。

3.跳过本次循环与退出循环

在循环的运行中,可能会遇到在某些时候不需要继续执行的情况,此时需要使用continue关键字来跳过本次循环。请看图2-29所示的代码运行结果。

图2-29 使用continue跳过一次循环

当名字为“王五”的时候,跳过后面的代码。continue只会影响到本次循环,后面的循环不受影响。

当遇到某些情况时,需要结束整个循环,这个时候需要使用break关键字。请看图2-30所示的代码。

图2-30 使用break提前结束整个循环

while循环和for循环在使用continue和break的时候完全相同,请各位读者自行测试。

特别注意:在循环里面还有循环(循环嵌套)的时候,continue和break都只对自己所在的这一层循环有效,不会影响外面的循环。