带你读《从零开始学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网络爬虫开发打下坚实的编程基础。

相关文章
|
12天前
|
数据采集 存储 API
网络爬虫与数据采集:使用Python自动化获取网页数据
【4月更文挑战第12天】本文介绍了Python网络爬虫的基础知识,包括网络爬虫概念(请求网页、解析、存储数据和处理异常)和Python常用的爬虫库requests(发送HTTP请求)与BeautifulSoup(解析HTML)。通过基本流程示例展示了如何导入库、发送请求、解析网页、提取数据、存储数据及处理异常。还提到了Python爬虫的实际应用,如获取新闻数据和商品信息。
|
13天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
13天前
|
程序员 开发者 Python
Python网络编程基础(Socket编程) 错误处理和异常处理的最佳实践
【4月更文挑战第11天】在网络编程中,错误处理和异常管理不仅是为了程序的健壮性,也是为了提供清晰的用户反馈以及优雅的故障恢复。在前面的章节中,我们讨论了如何使用`try-except`语句来处理网络错误。现在,我们将深入探讨错误处理和异常处理的最佳实践。
|
16天前
|
数据采集 Python
【python】爬虫-西安医学院-校长信箱
本文以西安医学院-校长信箱为基础来展示爬虫案例。来介绍python爬虫。
【python】爬虫-西安医学院-校长信箱
|
22天前
|
数据采集 安全 Python
python并发编程:Python实现生产者消费者爬虫
python并发编程:Python实现生产者消费者爬虫
24 0
python并发编程:Python实现生产者消费者爬虫
|
1月前
|
数据采集 数据挖掘 调度
异步爬虫实践攻略:利用Python Aiohttp框架实现高效数据抓取
本文介绍了如何使用Python的Aiohttp框架构建异步爬虫,以提升数据抓取效率。异步爬虫利用异步IO和协程技术,在等待响应时执行其他任务,提高效率。Aiohttp是一个高效的异步HTTP客户端/服务器框架,适合构建此类爬虫。文中还展示了如何通过代理访问HTTPS网页的示例代码,并以爬取微信公众号文章为例,说明了实际应用中的步骤。
|
2天前
|
数据采集 存储 JSON
Python爬虫面试:requests、BeautifulSoup与Scrapy详解
【4月更文挑战第19天】本文聚焦于Python爬虫面试中的核心库——requests、BeautifulSoup和Scrapy。讲解了它们的常见问题、易错点及应对策略。对于requests,强调了异常处理、代理设置和请求重试;BeautifulSoup部分提到选择器使用、动态内容处理和解析效率优化;而Scrapy则关注项目架构、数据存储和分布式爬虫。通过实例代码,帮助读者深化理解并提升面试表现。
11 0
|
5天前
|
数据采集 JavaScript 前端开发
使用Python打造爬虫程序之破茧而出:Python爬虫遭遇反爬虫机制及应对策略
【4月更文挑战第19天】本文探讨了Python爬虫应对反爬虫机制的策略。常见的反爬虫机制包括User-Agent检测、IP限制、动态加载内容、验证码验证和Cookie跟踪。应对策略包括设置合理User-Agent、使用代理IP、处理动态加载内容、验证码识别及维护Cookie。此外,还提到高级策略如降低请求频率、模拟人类行为、分布式爬虫和学习网站规则。开发者需不断学习新策略,同时遵守规则和法律法规,确保爬虫的稳定性和合法性。
|
12天前
|
网络协议 Java API
Python网络编程基础(Socket编程)Twisted框架简介
【4月更文挑战第12天】在网络编程的实践中,除了使用基本的Socket API之外,还有许多高级的网络编程库可以帮助我们更高效地构建复杂和健壮的网络应用。这些库通常提供了异步IO、事件驱动、协议实现等高级功能,使得开发者能够专注于业务逻辑的实现,而不用过多关注底层的网络细节。
|
16天前
|
数据采集 存储 前端开发
Python爬虫如何快速入门
写了几篇网络爬虫的博文后,有网友留言问Python爬虫如何入门?今天就来了解一下什么是爬虫,如何快速的上手Python爬虫。
20 0