带你读《从零开始学Scrapy网络爬虫》之一:Python基础-阿里云开发者社区

开发者社区> 华章出版社> 正文
登录阅读全文

带你读《从零开始学Scrapy网络爬虫》之一:Python基础

简介: 本书共13章。其中,第1~4章为基础篇,介绍了Python基础、网络爬虫基础、Scrapy框架及基本的爬虫功能。第5~10章为进阶篇,介绍了如何将爬虫数据存储于MySQL、MongoDB和Redis数据库中;如何实现异步AJAX数据的爬取;如何使用Selenium和Splash实现动态网站的爬取;如何实现模拟登录功能;如何突破反爬虫技术,以及如何实现文件和图片的下载。第11~13章为高级篇,介绍了使用Scrapy-Redis实现分布式爬虫;使用Scrapyd和Docker部署分布式爬虫;使用Gerapy管理分布式爬虫,并实现了一个抢票软件的综合项目。

从零开始学Scrapy网络爬虫
(视频教学版)
点击查看第二章
点击查看第三章
image.png

张涛 编著

第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所示。
image.png

图1-1 Anaconda下载页面

  网速慢的读者可在清华大学开源软件镜像站下载,网址为https://mirrors.tuna.tsinghua. edu.cn/anaconda/archive/,如图1-2所示。
  (2)Anaconda是跨平台的,有Windows、Linux和Mac OS版本,请根据自己的操作系统及系统类型(32/64位),下载最新版本的Anaconda。
image.png

图1-2 清华大学开源软件Anaconda下载页面


  (3)安装过程比较简单,直接双击安装包,按照提示安装即可。在安装过程中,有两处需要注意:
  一是勾选Add Anaconda to my PATH environment variable复选框,将Anaconda注册到环境变量中,如图1-3所示。
image.png

图1-3 设置环境变量

  二是忽略下载VSCode,即单击Skip按钮,如图1-4所示。VSCode(Visual Studio Code),是微软推出的一款轻量级代码编辑器,这里用不到。
  3.验证安装是否成功
  如何验证Anaconda是否已经安装成功了呢?很简单,打开控制台,输入命令:python。如果显示如图1-5所示的Python版本的信息,说明Anaconda已经成功安装,这时即可进入Python的解释器界面。
image.png

图1-4 忽略安装VSCode


image.png

图1-5 验证安装是否成功

  4.编写第一行Python代码
  在解释器界面就可以进行Python编程了,如输入print("hello Python!"),回车,就会打印出字符串"hello Python!",如图1-6所示。自己动手试一试吧。
image.png

图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所示。
image.png

图1-7 选择自己的操作系统类型


  4.编写第一个Python代码hello Python!
  下面在PyCharm中编写Python程序。首先新建一个名为hello的项目(Project),一个项目中可以包含多个Python源文件,然后在hello项目中新建一个名为hello.py的源文件,在源文件中输入print("hello Python!"),最后在源文件中右击,在弹出的快捷菜单中选择run ‘hello’选项,即可执行程序。结果显示在信息显示区,如图1-8所示。
image.png

图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 运算符及其使用说明


image.png
image.png

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 关系运算符

image.png

  • Python中用于连接多个条件的逻辑运算符,如表1-3所示。
    表1-3 逻辑运算符

image.png

  下面来看一个判断闰年的例子。要判断是否是闰年,只要看年份是否满足条件:能被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所示。

image.png

  

图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所示。
image.png

图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所示。
    image.png

图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网络爬虫开发打下坚实的编程基础。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享: