从零开始学Scrapy网络爬虫
(视频教学版)
点击查看第二章
点击查看第三章
张涛 编著
第1章 Python基础
Scrapy网络爬虫框架是用Python编写的,因此掌握Python编程基础是更好地学习Scrapy的前提条件。即使你从未接触过Python,通过本章的学习,也能很熟练地进行Scrapy网络爬虫开发,因为Python的设计哲学是优雅、明确、简单,用最少的代码完成更多的工作。
1.1 Python简介
在开发者社群流行一句话“人生苦短,我用Python”。看似一句戏言,其实十分恰当地说明了Python独特的魅力及其在开发者心目中的地位。
1.1.1 Python简史
要说近几年最受关注的编程语言,非Python莫属。根据2019年3月Tiobe发布的编程语言排行榜显示,Python以惊人的速度上升到了第三位。这门“古老”的语言,之所以能够焕发新生,得益于人工智能的崛起。因为Python是人工智能的首选编程语言,这已是业界的共识,也是必然的选择。
Python是一门解释型的高级编程语言,创始人为荷兰人Guido van Rossum(吉多·范罗苏姆)。1989年圣诞节期间,在阿姆斯特丹,Guido为了打发圣诞节无聊的时间,决心开发一个简单易用的新语言,它介于C和Shell之间,同时吸收了ABC语言的优点。之所以起Python这个名字,是因为他喜欢看英国电视秀节目《蒙提·派森的飞行马戏团(Monty Python’s Flying Circus)》。
Python主要有以下几个特点。
- 初学者的语言:结构简单、语法优雅、易于阅读和维护。
- 跨平台:支持主流的操作系统,如Windows、Mac OS和Linux。
- 内置电池:极其丰富和强大的第三方库,让编程工作看起来更像是在“搭积木”。
- 胶水语言:就像使用胶水一样把用其他编程语言(尤其是C/C++)编写的模块黏合过来,让整个程序同时兼备其他语言的优点,起到了黏合剂的作用。
1.1.2 搭建Python环境
一提到环境的搭建,相信很多人都有过痛苦的经历,除了需要安装一堆软件,还要忍受一系列复杂的步骤及天书般的配置命令,稍有不慎,就会功亏一篑。本节将为大家介绍使用Anaconda“傻瓜式”地搭建Python编程环境的方法。
1.Anaconda介绍
Anaconda是最受欢迎的数据科学Python发行版,它集成了Python环境,包含了一千多个Python/R数据科学包,并能有效地管理包、依赖项和环境,更重要的是它包含了Scrapy框架的各种依赖包,因此以后安装Scrapy框架时,基本不会出现任何问题。
2.安装Anaconda
(1)下载Anaconda。
官方网站下载网址为https://www.anaconda.com/download,如图1-1所示。
图1-1 Anaconda下载页面
网速慢的读者可在清华大学开源软件镜像站下载,网址为https://mirrors.tuna.tsinghua. edu.cn/anaconda/archive/,如图1-2所示。
(2)Anaconda是跨平台的,有Windows、Linux和Mac OS版本,请根据自己的操作系统及系统类型(32/64位),下载最新版本的Anaconda。
图1-2 清华大学开源软件Anaconda下载页面
(3)安装过程比较简单,直接双击安装包,按照提示安装即可。在安装过程中,有两处需要注意:
一是勾选Add Anaconda to my PATH environment variable复选框,将Anaconda注册到环境变量中,如图1-3所示。
图1-3 设置环境变量
二是忽略下载VSCode,即单击Skip按钮,如图1-4所示。VSCode(Visual Studio Code),是微软推出的一款轻量级代码编辑器,这里用不到。
3.验证安装是否成功
如何验证Anaconda是否已经安装成功了呢?很简单,打开控制台,输入命令:python。如果显示如图1-5所示的Python版本的信息,说明Anaconda已经成功安装,这时即可进入Python的解释器界面。
图1-4 忽略安装VSCode
图1-5 验证安装是否成功
4.编写第一行Python代码
在解释器界面就可以进行Python编程了,如输入print("hello Python!"),回车,就会打印出字符串"hello Python!",如图1-6所示。自己动手试一试吧。
图1-6 第一行Python代码
1.1.3 安装PyCharm集成开发环境
如果仅仅是基本的Python程序开发,安装Anaconda就足够了。但是对于Scrapy网络爬虫开发,就显得力不从心了,我们需要功能更强大的集成开发环境,来帮助我们整合资源,减少错误,提高效率。
PyCharm是一种Python编程的集成开发环境(IDE),带有一整套可以帮助用户在使用Python语言开发时提高效率的工具,比如调试、语法高亮、项目管理、代码跳转、智能提示、自动完成、单元测试和版本控制等。当然PyCharm对于专业的Python Web开发,也提供了Django框架(用于开发Python Web的框架)的支持。
1.下载PyCharm
PyCharm官方网站下载网址为https://www.jetbrains.com/pycharm/download。
2.选择版本
PyCharm分Professional(专业版)和Community(社区版)。专业版拥有全部功能,但是收费;社区版是个较轻量级的IDE,免费开源。对于开发者来说,使用社区版完全够用了。
3.安装PyCharm
PyCharm的安装也是“傻瓜式”的,只要按照提示执行“下一步”即可。不过,在选择操作系统类型(32/64位)时,需要根据操作系统的实际情况选择对应的系统类型,如图1-7所示。
图1-7 选择自己的操作系统类型
4.编写第一个Python代码hello Python!
下面在PyCharm中编写Python程序。首先新建一个名为hello的项目(Project),一个项目中可以包含多个Python源文件,然后在hello项目中新建一个名为hello.py的源文件,在源文件中输入print("hello Python!"),最后在源文件中右击,在弹出的快捷菜单中选择run ‘hello’选项,即可执行程序。结果显示在信息显示区,如图1-8所示。
图1-8 PyCharm编程
1.2 Python基本语法
Python语法简单、优雅,如同阅读英语一般,即使是非科班出身的用户,也能很快理解Python语句的含义。
1.2.1 基本数据类型和运算
1.变量
有计算机基础的读者都知道,计算机在工作过程中所产生的数据都是在内存中存储和读取的。内存类似于工厂的仓库,数据作为零件存储在仓库(内存)中不同的地方。仓库那么大,怎样才能快速找到这些零件(数据)呢?我们可以给每个零件贴上“标签”,根据标签名称,就可以找到它们了。这些标签,就是传说中的“变量”。使用“变量”可以快速定位数据、操作数据。比如,存储同学cathy的个人信息:
name = "cathy" #变量名name,存储姓名
age = 10 #变量名age,存储年龄
height = 138.5 #变量名height,存储身高
is_student = True #变量名is_student,存储是否是学生的标记
score1 = None #变量名score1,存储成绩
【重点说明】
- 变量名包含英文、数字及下划线,但不能以数字开头。
- =用来给变量赋值,如变量name的值为cathy。
- 变量在使用前必须赋值。
-
代表行内注释。
2.数据类型
Python的基本数据类型包括整数、浮点数、布尔型和字符串,可以使用type()函数查看一个变量的类型。比如查看同学cathy的各个变量的类型:
type(name) #字符串:
type(age) #整数:
type(height) #浮点数:
type(is_student) #布尔型:
type(score1) #NoneType:
【重点说明】
- 注释中的尖括号是执行type()函数后输出的结果。
- 结果中的class意味着Python中一切皆对象,后面会讲到。
- score1的类型是NoneType,不是0,也不是空。很多情况下,API执行失败会返回None。
- 变量不需要声明类型,Python会自动识别。
1.2.2 运算符和表达式
Python中数值的基本运算和其他语言差不多,运算符及其使用说明如表1-1所示。
表1-1 运算符及其使用说明
1.2.3 条件判断语句
条件语句是指根据条件表达式的不同,使程序跳转至不同的代码块。Python的条件语句有:if、if-else和if-elif-else。下面来看几个判断成绩的例子。
(1)判断成绩是否合格:
score = 95 #成绩
#二选一
if score >= 60: #如果成绩60分及以上,则输出“合格”
print("合格")
else: #否则,输出“不合格”
print("不合格")
运行以上代码后,输出“合格”。
(2)判断成绩是优秀、良好、及格还是不及格:
#多选一
if score >= 90: #如果成绩大于等于90
print("优秀")
print("再接再厉")
elif score <90 and score >=70: #如果成绩在70~90之间
print("良好")
elif score < 70 and score >= 60: #如果成绩在60~70之间
print("及格")
else: #其他情况
print("不及格")
运行以上代码后,输出“优秀”和“再接再厉”。
【重点说明】
- 关键字if、elif和else后面的冒号(:)不能缺,这是语法规则。
- 每个判断条件下的代码块都必须缩进,这是Python的一大特点,即通过强行缩进来表明成块的代码。这样做的好处是代码十分清晰工整,坏处是稍不注意,代码块就会对不齐,运行就会出错。
- Python中用于比较大小的关系运算符,跟其他语言类似,如表1-2所示。
表1-2 关系运算符
- Python中用于连接多个条件的逻辑运算符,如表1-3所示。
表1-3 逻辑运算符
下面来看一个判断闰年的例子。要判断是否是闰年,只要看年份是否满足条件:能被4整除,并且不能被100整除,或者能被4整除,并且又能被400整除。
实现代码如下:
year = input("请输入年份:") #通过命令行输入年份
year = int(year) #转换为整型
if (year%4==0 and year%100!=0) or (year%4==0 and year%400==0):
print("%d年是闰年"%year) #闰年
else:
print("%d年不是闰年"%year) #非闰年
【重点说明】
- 第一行通过input(?)函数实现从命令行中动态输入年份。
- if后面是判断闰年的条件表达式,由于and的优先级高于or,也可以省略圆括号。条件表达式还可以简写为:
if year%4==0 and (year%100!=0 or year%400==0):
- 通过print输出字符串文字。这是一个经过格式化的字符串,双引号中是将要格式化的字符串,其中的%d是格式化符号,表示整数。双引号后面跟%year,表示将变量year的值转换为整数后插入到%d的位置上。
1.2.4 循环语句
生活中有许多重复的劳动,如cathy做错事被罚抄课文5遍等。代码的世界也是如此,对于重复的功能,如果通过简单的复制、粘贴,代码就会变得沉重冗余,难以理解。Python中使用while和for循环来实现代码的重复利用,通常用于遍历集合或累加计算。
1.while循环
while循环的语法结构为:
while <条件>:
循环体
在给定的判定条件为True时执行循环体,否则,退出循环。循环的流程图如图1-9所示。
图1-9 循环流程图
以下代码实现了打印5遍字符串的功能:
#1.使用while执行5次循环
n = 1 #记录次数
while n<=5: #n<=5为循环条件
print("cathy正在努力抄第%d遍课文"%n) #每次循环输出的文字
n += 1 #自增1
【重点说明】
- while语句后要有半角冒号(:)。
- 循环体要有缩进。
- 每次循环n都会自增1,否则就会死循环。
2.for循环
for循环的语法结构为:
for <目标对象> in <对象集合>:
循环体
当执行for循环时,会逐个将对象集合中的元素赋给目标对象,然后为每个元素执行循环体。以下代码使用for循环实现了计数和遍历集合的功能:
#1.使用for执行5次循环
for n in range(1,6): #range()函数生成整数集合(1,2,3,4,5)
print("cathy正在努力抄第%d遍课文"%n) #每次循环输出文字
#2.遍历字符串所有字符
name = "cathy"
for n in name:
print(n) #每次循环分别输出c、a、t、h、y
#3.遍历列表中的所有项目
student = ["cathy",10,25] #记录姓名、年龄、体重
for item in student:
print(item) #每次循环分别输出cathy 10 25
3.break和continue
在循环过程中,有时需要终止循环或者跳过当前循环。Python使用break和continue来分别表示终止循环和跳过当前循环。
来看一个break的例子:实现在1~100之间,找到第一个能被3整除且能被8整除的整数。实现代码如下:
a = 1 #初始为1
while a<=100: #循环100次
if a%3==0 and a%8==0:
print("第一个能被3整除且能被8整除的整数是:%d"%a)
break #终止循环
a+=1 #每次循环自增1
再来看一个continue的例子:实现在1~100之间,找到所有不能被3和8整除的数。实现代码如下:
for i in range(1,101):
if i%3==0 and i%8==0:
continue
print("%d "%i)
【重点说明】
- range(1,101)函数生成了一个包含1~100的整数集合,注意,不包括101。
- if语句判断的是能被3和8整除的数,使用continue跳过for循环剩下的代码,继续执行下一次循环。
4.while和for使用场景
一般情况下,while和for循环可以互相代替,但也有一些使用原则:
- 如果循环变量的变化,伴随着一些条件判断等因素,推荐使用while循环。
- 如果仅仅是遍历集合中所有的数据,没有一些条件判断因素,推荐使用for循环。
1.2.5 字符串
1.引号
字符串是Python中最常见的数据类型,它包含在一对双引号(" ")或单引号(' ')中。单引号和双引号没有任何区别。
name = "cathy" #双引号字符串
like = 'english' #单引号字符串
当单引号中含有单引号(或者叫撇号)时,程序运行就会出错,解释器会“犯迷糊”,如下面的代码所示。它会将'i'看成一个字符串,后面的m就不知道如何处理了。
age = 'i'm ten ' #单引号中包含单引号
错误信息:SyntaxError: invalid syntax
针对上述问题,有以下两种修改方式。
方式一:将字符串改为双引号括起来。
age = "i'm ten " #使用双引号
方式二:使用反斜杠()将字符串中的单引号进行转义。
age = 'i'm ten ' #加转移字符:\
2.访问字符串
Python访问字符串,可以使用方括号([ ])下标法来截取字符串,代码如下:
hello = "hello,Python!"
hello[0] #获取第1个值:h
hello[1:4] #获得第2~5个(不包括)范围的值:ell
hello[-1] #获取最后一个值:!
【重点说明】
- 字符串的下标是由左往右,从0开始标记的。
- 截取任意范围内容,其格式为:起始下标:终止下标,这叫做切片。需要注意的是,终止下标是不包含在截取范围内的,如hello[1:4]得到ell。
- 下标为负数时,从右往左标记,如-1就是获取最后一个值,-2获取倒数第二个值,以此类推。
3.字符串方法
字符串自带很多处理方法,通过简单的调用,就可以实现对自身的处理。以下为字符串最常用的几种处理方法,读者可以打印出来看一下效果。
cathyStr =" Hello,cathy! " #两边有空格的字符串
cathyStr.strip(" ") #去除字符串两边的空格
cathyLst = cathyStr.split(",") #以逗号作为分隔符,切分字符串,保存为列表
cathyStr.replace("!",".") #将字符串中所有感叹号替换为句号
cathyStr.lower() #将字符串中所有字母都转换为小写字母
cathyStr.upper() #将字符串中所有字母都转换为大写字母
4.格式化输出
字符串的格式化输出有3种方法:
第1种是我们一直在print( )函数中使用的%格式法。例如,要输出字符串“我的名字叫XX,今年X岁了。”,其中名字和年龄都是动态输入的。实现代码如下:
name = input("请输入姓名:")
age = int(input("请输入年龄:"))
message = "我的名字叫%s,今年%d岁了。"%(name,age)
print(message)
【重点说明】
- 在message字符串中,%s和%d是格式化符号,%s代表字符串,%d代表整数。它们与后面的name和age一一对应,功能是将name设为字符串,将age设为整数,再插入到%s和%d对应的位置上。
程序运行后,根据提示输入cathy和10,输出的结果如下:
>请输入姓名:cathy
>请输入年龄:10
我的名字叫cathy,今年10岁了。
这种方法有个特点,就是格式化符号和后面的变量要一一对应,位置一旦搞错,就会出现错乱。这时候可以考虑使用第2种格式化输出方法。先看一下代码:
name = input("请输入姓名:")
age = int(input("请输入年龄:"))
message = "我的名字叫%(i_name)s,今年%(i_age)d岁了。"%{"i_name":name,"i_age":age}
print(message)
【重点说明】
- %s和%d的中间添加了i_name和i_age这两个参数。在后面的字典({}括起来的部分)中可以找到参数对应的值,这些值会替换参数形成完整的字符串。
程序运行后,根据提示输入tom和15,输出结果如下:
>请输入姓名:tom
>请输入年龄:15
我的名字叫tom,今年15岁了。
第3种格式化输出的方法是使用字符串的format()函数,用法与第2种方法类似。还是先来看代码:
name = input("请输入姓名:")
age = int(input("请输入年龄:"))
message = "我的名字叫{i_name},今年{i_age}岁了。".format(i_name=name,i_age=age)
print(message)
【重点说明】
- 字符串中的{}中定义了参数,这些参数可以在format()函数中找到对应的值,这些值会替换参数形成完整的字符串。
程序运行后,根据提示输入lili和8,输出的结果如下:
>请输入姓名:lili
>请输入年龄:8
我的名字叫lili,今年8岁了。
1.3 Python内置数据结构
1.2.5节使用了变量存储同学cathy的个人信息,但是如果她的个人信息很多,就需要定义更多的变量来存储,这就会产生以下问题:
- 变量定义多,容易混淆。
- 数据各自独立,没有关联性。
- 代码量大。
- 可读性不强。
使用Python容器就可以解决上述问题。容器可以用来盛放一组相关联的数据,并对数据进行统一的功能操作。容器主要分为列表(list)、字典(dict)和元组(tuple),这些结构和其他语言中的类似结构本质上是相同的,但Python容器更简单、更强大。
1.3.1 列表
列表是一组元素的集合,可以实现元素的添加、删除、修改和查找等操作。现将同学cathy的个人信息统一放到列表中,代码如下:
cathy = ["cathy",10,138.5,True,None] #cathy的个人信息
score = [90,100,98,95] #各科成绩
name = list("cathy") #利用list()函数初始化一个列表
print(name) #输出结果:['c', 'a', 't', 'h', 'y']
【重点说明】
- 列表内的元素用方括号([ ])包裹。
- 列表内不同元素之间使用逗号(,)分隔。
- 列表内可以包含任何数据类型,也可以包含另一个列表。
- 可以使用list()函数生成一个列表。
可以使用列表自带的方法实现列表的访问、增加、删除和倒序等操作。仔细阅读以下代码及注释。
cathy = ["cathy",10,138.5,True,None] #cathy的个人信息
a = cathy[0] #下标法获取第1个元素(姓名):cathy
b = cathy[1:3] #使用切片获取下标1到下标3之前的子序列:[10, 138.5]
c = cathy[1:-2] #切片下标也可以倒着数,-1对应最后一个元素:[10, 138.5]
d = cathy[:3] #获取从开始到下标3之前的子序列:['cathy', 10, 138.5]
e = cathy[2:] #获取下标2开始到结尾的子序列:[138.5, True, None]
cathy[2] = 140.2 #将第3个元素修改为140.2
10 in cathy #判断10是否在列表中,True
cathy.append(28) #将体重添加到列表末尾
print(cathy) #['cathy', 10, 140.2, True, None, 28]
cathy.insert(2,"中国") #将国籍插入到第2个元素之后
print(cathy) #['cathy', 10, '中国', 140.2, True, None, 28]
cathy.pop() #默认删除最后一个元素
print(cathy) #['cathy', 10, '中国', 140.2, True, None]
cathy.remove(10) #删除第1个符合条件的元素
print(cathy) #['cathy', '中国', 140.2, True, None]
cathy.reverse() #倒序
print(cathy) #[None, True, 140.2, '中国', 'cathy']
现在要使用列表存储另一个同学terry的信息,已知除了姓名以外,其他的信息跟cathy一样。通过以下操作就可以得到同学terry的列表。
#cathy的个人信息
cathy_list = ["cathy",10,138.5,True,None]
terry_list = cathy_list #将cathy_list赋给变量terry_list
terry_list[0] = "terry" #修改terry的姓名
print(terry_list) #打印terry信息:['terry', 10, 138.5, True, None]
print(cathy_list) #打印cathy信息:['terry', 10, 138.5, True, None]
和大家的预期不同的是,cathy_list中的姓名也变成terry了,但是我们并未修改cathy_list的姓名,这是什么原因呢?原来在执行terry_list=cathy_list时,程序并不会将cathy_ list的值复制一遍,然后赋给terry_list,而是简单地为cathy_list的值即["cathy",10,138.5, True,None]建立了一个引用,相当于cathy_list和terry_list都是指向同一个值的指针,所以当terry_list中的值改变后,cathy_list的值也会跟着变。可以通过id()函数来获取变量的地址。实现代码如下:
print(id(cathy_list)) #获取cathy_list的地址:2011809417032
print(id(terry_list)) #获取terry_list的地址:2011809417032
结果显示,cathy_list和terry_list这两个变量均指向同一个地址。如何解决这个问题呢?可以使用copy()函数将值复制一份,再赋给terry_list,实现代码如下:
#terry_list = cathy_list #删除该条语句
terry_list = cathy_list.copy() #将值复制一份赋给变量terry_list
1.3.2 字典
将同学cathy各科的成绩保存于列表score中,实现代码如下:
score = [90,100,98,95] #成绩
如果想要获取cathy的语文成绩,如何做到呢?除非事先将每门课的位置都做了记录,否则无论如何是获取不到语文成绩的。当需要对数据做明确的标注,以供别人理解和处理时,使用列表就不太方便了,这时字典就派上用场了。
字典是一种非常常见的“键-值”(key-value)映射结构,它为每一个元素分配了一个唯一的key,你无须关心位置,通过key就可以获取对应的值。下面来看一下使用字典保存的成绩:
score1 = {"math":90,"chinese":100,"english":98,"PE":95} #成绩字典
print(score1["chinese"])
【重点说明】
- 字典内的元素用大括号({})包裹。
- 使用key:value的形式存储一个元素,如"math":90,字符串math是分数90的key。
- 字典内不同键值对之间采用逗号(,)分隔。
- 字典是无序的,字典中的元素是通过key来访问的,如score1["chinese"]得到语文成绩。
也可以使用dict()函数初始化字典,实现代码如下:
score2 = dict(math=90,chinese=100,english=98,PE=95)
print(score1["chinese"]) #根据key获取语文成绩:100
if "PE" in score1: #判断字典中是否包含"PE"的key
print(score1["PE"]) #得到体育成绩:95
#获取所有的key并保存于列表中,输出结果:['math', 'chinese', 'english', 'PE']
print(score1.keys())
#获取所有的value并保存于列表中,输出结果:[90, 100, 98, 95]
print(score1.values())
#获取key和value对转化为列表
#输出结果:[('math', 90), ('chinese', 100), ('english', 98), ('PE', 95)]
print(score1.items())
1.3.3 元组
元组和列表最大的区别就是不可变的特性,即元组的值一旦确定了,就无法进行任何改动,包括修改、新增和删除。
sex1 = ("male","female") #使用括号生成并初始化元组
sex2 = tuple(["male","female"]) #从列表初始化
sex3 = ("male",) #只有一个元素时,后面也要加逗号
sex4 = "male","female" #默认是元组类型("male","female")
【重点说明】
- 元组中元素的访问方法和列表一样,都可以使用下标和切片。
- 圆括号(( ))表示元组,方括号([ ])代表列表,大括号({ })代表字典。
- 初始化只包含一个元素的元组时,也必须在元素后加上逗号,如sex3。
- 直接用逗号分隔多个元素的赋值默认是元组,如变量sex4。
- 元组内的数据一旦被初始化,就不能更改。
1.3.4 遍历对象集合
for循环用于遍历一个对象集合,依次访问集合中的每个项目。前面提到的列表、字典和元组,均可通过for循环遍历。下面来看几个例子。
1.遍历列表
cathy = ["cathy",10,138.5,True,None]
#依次输出:"cathy",10,138.5,True,None
for a in cathy:
print(a)
可以通过下标遍历列表。用len()函数获得列表长度,再用range()函数获得所有下标的集合,实现代码如下:
#依次输出:"cathy",10,138.5,True,None
for i in range(len(cathy)):
print(cathy[i])
2.遍历字典
score = {"math":90,"chinese":100,"english":98,"PE":95} #成绩字典
#键的遍历,不按顺序输出:"math","chinese","english","PE"
for key in score:
print(key)
#键和值的遍历,不按顺序输出:math : 90,chinese : 100,english : 98,PE : 95
for key,value in score.items():
print(key,":",value)
程序如果执行多次,会发现输出的顺序不一定一致,这是因为字典是无序的。
3.遍历元组
sex = ("male","female")
#依次输出:"male","female"
for b in sex:
print(b)
1.4 Python模块化设计
在编写程序的时候,读者会不会被一个问题所困扰?有些功能多处要用到,实现时不得不复制和粘贴相同的代码。这不但会使程序代码冗余、容易出错,而且维护起来十分困难。因此,可以将这段重复使用的代码打包成一个可重用的模块,根据需要调用这个模块,而不是复制和粘贴现有的代码,这个模块就是Python的函数。
另外,我们有时希望将模块和模块所要处理的数据相关联,就跟Python内置的数据结构一样,能有效地组织和操作数据。Python允许创建并定义面向对象的类,类可以用来将数据与处理的数据功能相关联。
Scrapy爬虫框架正是基于Python模块化(函数和类)的设计模式进行组织和架构的。而且几乎所有爬虫功能的实现,都是基于函数和类的。可以说,Python模块化设计是理解Scrapy爬虫框架及掌握爬虫编程技术的重要前提。
1.4.1 函数
函数是组织好的,可重复使用的,用来实现单一或相关联功能的代码段。它有一个入口,用于输入数据,还有一个出口,用于输出结果。当然,根据实际需求,入口和出口是可以省略的。下面先看几个例子。
(1)判断闰年的函数。
def is_leap(year): #函数定义
if (year % 4 == 0 and year % 100 != 0) or (year % 4 == 0 and year % 400 == 0):
return 1 #闰年
else:
return 0 #非闰年
【重点说明】
- 函数以def 关键字开头,后接函数名、圆括号(( ))和冒号(:)。
- 圆括号内用于定义参数(也可以没有参数)。
- 代码块必须缩进。
- 使用return结束函数,并将返回值传给调用方。
需要注意的是,函数只有被调用才会被执行。以下代码实现了函数的调用:
year = int(input("请输入年份:")) #控制台输入年份
result = is_leap(year) #函数调用,传递参数year
if result == 1:
print("%d年是闰年"%year)
else:
print("%d年不是闰年"%year)
【重点说明】
- 通过is_leap(year)调用函数,其中,year是传递给函数的参数(叫做实参)。
- 当函数执行完后,会通过return返回结果,赋给result。
(2)实现打印任意同学信息的函数。
def print_student(name,age,sex="女"): #性别使用了默认值,必须放最后面
print("name:",name)
print("age:",age)
print("sex:",sex)
print_student("cathy",10) #函数调用,性别使用了默认设置
print_student("terry",20,"男") #函数调用
【重点说明】
- 函数可以定义多个参数,用逗号隔开。
- 参数可以设置默认值,但是必须放在最后。
- 在调用函数时,要按定义时的顺序放置参数,函数会按照顺序将实参传递给形参。
(3)求任意几门功课的平均成绩的函数。
def get_avg(scores): #scores前面加,表示可变长参数
sum = 0 #总成绩,初始值为0
for one in scores:
sum+=one
return (sum/len(scores)) #计算出平均值,返回给调用方
avg = get_avg(80,90,95) #调用函数,求3门课的平均分
avg1 = get_avg(77,88) #调用函数,求2门课的平均分
print(avg) #结果:88.33333333333333
print(avg1) #结果:82.5
【重点说明】
- 在参数个数不确定的情况下,可以使用可变长参数。方法是在变量前面加上*号。
- 可变长参数类似于一个列表,无论输入多少个数据,都会被存储于这个可变参数中。因此,可以使用for循环遍历这个可变参数,获取所有的数据。
1.4.2 迭代器(iterator)
大家都知道,通过网络爬虫提取的数据,数据量往往都很大。如果将所有数据都保存到列表或字典中,将会占用大量的内存,严重影响主机的运行效率,这显然不是一个好方法。遇到这种情况,就需要考虑使用迭代器(iterator)了。
迭代器相当于一个函数,每次调用都可以通过next()函数返回下一个值,如果迭代结束,则抛出StopIteration异常。从遍历的角度看这和列表没什么区别,但它占用内存更少,因为不需要一下就生成整个列表。
能够使用for循环逐项遍历数据的对象,我们把它叫做可迭代对象。例如列表、字典和rang()函数都是可迭代对象。可以通过内置的iter()函数来获取对应的迭代器对象。例如,使用迭代器获取列表中的每个元素,代码如下:
cathy = ["cathy",10,138.5,True,None]
iter1 = iter(cathy) #生成迭代器对象
print(next(iter1)) #得到下一个值:"cathy"
print(next(iter1)) #得到下一个值:10
1.4.3 生成器(Generator)
在Python中,把使用了yield的函数称为生成器(generator)。生成器是一种特殊的迭代器,它在形式上和函数很像,只是把return换成了yield。函数在遇到return关键字时,会返回值并结束函数。而生成器在遇到yield关键字时,会返回迭代器对象,但不会立即结束,而是保存当前的位置,下次执行时会从当前位置继续执行。
下面来看一个著名的斐波那契数列,它以0、1开头,后面的数是前两个数的和,下面展示的是前20个斐波那契数列的数据。
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181
下面分别使用普通函数和生成器实现斐波那契数列的功能,以此来说明它们的不同之处。
(1)定义普通函数。
#普通斐波那契函数定义
def get_fibonacci(max): #max:数量
fib_list =[0,1] #保存斐波那契数列的列表,初始值为0和1
while len(fib_list) < max:
fib_list.append(fib_list[-1]+fib_list[-2]) #最后两个值相加
return fib_list
#主函数
if name == "__main__":
#函数调用,输出前10个斐波那契数列的值:0 1 1 2 3 5 8 13 21 34
for m in get_fibonacci(10):
print(m,end=" ")
因为函数只能返回一次,所以每次计算得到的斐波那契数必须全部存储到列表中,最后再使用return将其返回。
(2)使用带yield的函数——生成器。
#使用yield的斐波那契函数定义
def get_fibonacci2(max):
n1 = 0 #第一个值
n2 = 1 #第二个值
num = 0 #记录数量
while num < max:
yield n1
n1,n2 = n2,n1+n2
num+=1
#主函数
if name == "__main__":
#输出前10个斐波那契数列的值:0 1 1 2 3 5 8 13 21 34
for n in get_fibonacci2(10):
print(n,end=" ")
yield一次返回一个数,不断返回多次。先来看一下程序执行的流程图,如图1-10所示。
图1-10 斐波那契数列流程图
【重点说明】
- 通过函数get_fibonacci2()实现斐波那契数列时,没有将其保存于列表中,而是通过yield实时将其返回。
- 在主函数中,使用for循环遍历生成器。执行第一次循环,调用生成器函数get_fibonacci2(),函数运行到yield时,返回n1,函数暂停执行,并记录当前位置,然后执行for循环的循环体print(n,end=" "),打印n1的值。下一次循环,函数从上次暂停的位置继续执行,直到遇到yield,如此往复,直到结束。
- 使用yield可以简单理解为:对大数据量的操作,能够节省内存。
- 在使用Scrapy实现爬虫时,为了节省内存,总是使用yield提交数据。
1.4.4 类和对象
1.类和对象
我们希望尽量将函数和函数所要处理的数据相关联,就跟Python内置的数据结构一样,能有效地组织和操作数据。Python中的类就是这样的结构,它是对客观事物的抽象,由数据(即属性)和函数(即方法)组成。
就像函数必须调用才会执行一样,类只有实例化为对象后,才可以使用。也就是说,类只是对事物的设计,对象才是成品。
以下为描述人这个类的代码示例:
class People: #定义人的类
#构造函数,生成类的对象时自动调用
def __init__(self,my_name,my_age,my_sex):
self.name = my_name #姓名
self.age = my_age #年龄
self.sex = my_sex #性别
#方法:获取姓名
def get_name(self):
return self.name
#方法:打印信息
def get_information(self):
print("name:%s,age:%d,sex:%s"%(self.name,self.age,self.sex))
【重点说明】
- 使用class关键字定义一个类,其后接类名People,类名后面接冒号(:)。
- __init__()方法是一种特殊的方法,被称为类的构造函数或初始化方法,当创建了这个类的实例时就会调用该方法。注意,init两边分别有两个下划线。
- self代表类的实例。在定义类的方法时,self要作为参数传递进来,虽然在调用时不必传入相应的参数。
- 类的属性有:name、age和sex。使用属性时要在前面要加上self。
- 类的方法有:get_name(self)和get_information(self)。注意,这里要有参数self。
- 类的方法与普通的函数只有一个区别,它们必须有一个额外的第一个参数名称,按照惯例它是self。
重申一遍,类只有实例化为对象后才可以使用。例如要生成同学cathy的对象,实现代码如下:
#主函数
if name == "__main__":
#生成类的对象,赋初值
cathy = People("cathy",10,"女")
print(cathy.get_name()) #调用方法并打印,得到:"cathy"
cathy.get_information() #调用方法,得到:name:cathy,age:10,sex:女
【重点说明】
- 使用类名People,生成该类的对象cathy,并传入参数cathy,10和“女”。
- 实例化为对象cathy时,自动调用__init__()构造函数,并接收传入的参数。
- 使用点号(.)来访问对象的属性和方法,如cathy.get_name()。
2.继承
刚才定义了人这个类,如果还想再实现一个学生的类,是否需要重新设计呢?显然这会浪费很多时间,因为学生首先是人,具有人的所有属性和功能,再加上学生独有的一些特性,如年级、学校等即可。因此,我们没有必要重复“造轮子”,只要将人的类继承过来再加上自己的特性就生成了学生的类,这种机制叫做继承,其中学生类叫做子类,人的类叫做父类。类似于“子承父业”,即子类继承了父类所有的属性和方法。
学生类实现代码如下:
class Student(People):
def __init__(self,stu_name,stu_age,stu_sex,stu_class):
People.__init__(self,stu_name,stu_age,stu_sex) #初始化父类属性
self.my_class = stu_class #班级
#打印学生信息
def get_information(self):
print("name:%s,age:%d,sex:%s,class:%s"%(self.name,self.age,self.
sex,self.my_class))
#主函数
if name == "__main__":
#生成Student类的对象
cathy = Student("cathy",10,"女","三年二班")
#打印结果name:cathy,age:10,sex:女,class:三年二班
cathy.get_information()
- Student为学生类的类名,圆括号内是继承的父类。这样,Student类就继承了父类所有的属性和方法。
- 在构造函数中,为学生类新增了一个属性my_class,其余属性自动从父类继承而来。不过,需要调用父类的构造函数来初始化父类的属性。
- 新增的方法get_information(self)用于输出学生的信息。
1.4.5 文件与异常
1.文件操作
Python提供了文件操作的函数,用于将数据保存于文件中,或者从文件中读取数据。
以下代码实现了将学生列表数据保存到文件中的功能:
#学生列表
students=[["cathy",10,"女"],
["terry",9,"男"]]
#使用with…as…打开文件,文件会自动被关闭
with open("students.txt","a",encoding="utf-8") as f:
for one in students:
#以逗号隔开,连成一个长字符串
to_str = one[0]+","+str(one[1])+","+one[2]+"n"
f.write(to_str) #将字符串写入文件
【重点说明】
- open()函数用于打开文件,参数有:
文件名:students.txt。
打开方式:a表示追加,r表示只读,w表示只写。
编码方式:utf-8(支持中文)。
- 正常情况下,打开文件后,需要手动关闭文件(使用close()函数)。如果使用with…as…打开文件,系统会自动关闭文件。f为open()函数返回的可迭代的文件对象,用于处理文件。
- 如果文件不存在,会先自动生成一个空文件。程序运行后,在当前目录下就会生成students.txt文件,文件内容如图1-11所示。
图1-11 文件内容
以下代码实现了从文件中读取数据到列表的功能:
students1 = []
with open("students.txt","r",encoding="utf-8") as f:
for one in f: #f为可迭代文件对象,使用for循环,依次遍历
# 将读取到的字符串去除换行符,再转换为列表
one_list = one.strip("n").split(",")
one_list[1] = int(one_list[1]) #将年龄转为整型
students1.append(one_list) #增加到学生列表中
#输出结果:[['cathy', '10', '女'], ['terry', '9', '男']]
print(students1)
2.异常处理
上面的代码实现了从文件中读取数据到列表中的功能,但是,如果students.txt文件不存在,程序就会报错。这时可以使用try…except结构捕获异常,并对异常做出处理。
加入异常处理的代码如下:
students1 = []
try:
with open("students.txt","r",encoding="utf-8") as f:
for one in f: #f为可迭代文件对象,使用for循环,依次遍历
# 将读取到的字符串去除换行符,再转换为列表
one_list = one.strip("n").split(",")
one_list[1] = int(one_list[1]) #将年龄转换为整型
students1.append(one_list) #增加到学生列表中
#输出结果:[['cathy', '10', '女'], ['terry', '9', '男']]
print(students1)
except FileNotFoundError:
print("文件不存在!")
except:
print("其他错误!")
【重点说明】
- 当try中的代码模块运行出现异常时,将会执行except中的代码模块。
- except关键字可以有多个,FileNotFoundError代表文件不存在的异常。如果文件不存在,则输出“文件不存在!”;如果是其他错误,则输出“其他错误!”。
1.5 本 章 小 结
本章首先简单介绍了Python的历史及其在人工智能领域无可替代的优势;接着使用Anaconda“傻瓜式”地搭建了Python编程环境,并介绍了PyCharm集成开发环境;最后紧密围绕Scrapy网络爬虫开发需要,介绍了Python的基本语法、内置数据结构和模块化设计,为Scrapy网络爬虫开发打下坚实的编程基础。