Python爬虫开发与项目实战(txt+pdf+epub+mobi电子书下载)


发布时间:2020-10-05 21:18:36

点击下载

作者:范传辉

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Python爬虫开发与项目实战

Python爬虫开发与项目实战试读:

前言

● 为什么写这本书

当你看前言的时候,不得不说你做出了一个聪明的选择,因为前言中有作者对整本书的概括和学习建议,这会对大家之后的阅读产生事半功倍的效果。在聊这本书之前,首先给大家一个本书所有配套源码和说明的链接:https://github.com/qiyeboy/SpiderBook。大家可以在Github中对不懂的内容进行提问,我会尽可能地帮助大家解决问题。其实在前言开头放这个链接是挺突兀的,不过确实是担心大家不会完整地看完前言。

接下来聊一聊这本书,写这本书的原因来自于我个人的微信公众号:七夜安全博客。我经常在博客园、知乎和微信平台上发布技术文章,分享一些知识和见解,有很多热心的朋友愿意和我进行交流讨论。记得2016年4月初的某一天,有一个朋友在微信后台留言,问我怎样将Python爬虫技术学好,有什么书籍可以推荐。我当时回答了好长一段建议,但是那个朋友依然希望能推荐一本书籍帮助入门和提高。其实我特别能理解初学者的心情,毕竟我也是从初学者走过来的,但是确实挺纠结,不知从何推荐。于是,我专门找了一下这方面的书籍,只找到一本外国人写的书,中文版刚出版没多久,名字为《Python网络数据采集》。我花了半天看了一下里面的内容,整本书条理比较清晰,容易理解,但是很多知识点都谈得很浅,系统的实战项目基本上没有,更多的是一些代码片段,仅仅适合一些刚刚入门的朋友。自从这件事情以后,我就下定决心写一本Python爬虫方面的书籍,既然国内还没有人写这方面的书籍,我愿意做一个抛砖引玉的人,帮助大家更好地学习爬虫技术。

有了写书的想法后,开始列提纲,确定书的主题和内容。由于爬虫是一项实践性很强的技术,因此书的主题是以实战项目为驱动,由浅及深地讲解爬虫技术,希望你看这本书的时候是个菜鸟,认真学习完之后不再是个菜鸟,可以自主地开发Python爬虫项目了。从写书的那一刻开始,我就知道在书写完之前,我应该是没有周末了。这本书写了大半年的时间,由于我平时有写笔记、做总结的习惯,因此写书的时间不是特别长,不过直到2017年年初我依然在更新内容,毕竟爬虫技术更新得比较快,我努力将比较新的知识贡献给大家。

在写书的过程中,我的内心变得越来越平静,越来越有耐心,不断地修改更新,对每个实战项目进行反复验证和敲定,尽可能地贴近初学者的需求,希望能帮助他们完成蜕变。

最后做一下自我介绍,本人是一位信息安全研究人员,比较擅长网络安全、软件逆向,同时对大数据、机器学习和深度学习有非常浓厚的兴趣,欢迎大家和我交流,共同进步。

前路多艰,学习的道路不可能一帆风顺,爬虫技术只是个开始,愿与诸君一道共克难关。● 本书结构

本书总共分为三个部分:

基础篇

、中级篇和深入篇。

基础篇包括第1~7章,主要讲解了什么是网络爬虫、如何分析静态网站、如何开发一个完整的爬虫。

第1~2章帮助大家回顾了Python和Web方面的知识,主要是为之后的爬虫学习打下基础,毕竟之后要和Python、Web打交道。

第3~5章详细介绍了什么是网络爬虫、如何分析静态网站、如何从HTML页面中提取出有效的数据,以及对如何将数据合理地存储成各类文件以实现持久化。

第6~7章包含了两个实战项目。第一个项目是基础爬虫,也就是一个单机爬虫,功能是爬取百度百科的词条,并据此讲解了一个爬虫所应该具有的全部功能组件以及编码实现。第二个项目是分布式爬虫,功能和基础爬虫一致,在单机爬虫的基础上进行分布式改进,帮助大家从根本上了解分布式爬虫,消除分布式爬虫的神秘感。

中级篇包括第8~14章,主要讲解了三种数据库的存储方式、动态网站的抓取、协议分析和Scrapy爬虫框架。

第8章详细介绍了SQLite、MySQL和MongoDB三种数据库的操作方式,帮助大家实现爬取数据存储的多样化。

第9章主要讲解了动态网站分析和爬取的两种思路,并通过两个实战项目帮助大家理解。

第10章首先探讨了爬虫开发中遇到的两个问题——登录爬取问题和验证码问题,并提供了解决办法和分析实例。接着对Web端的爬取提供了另外的思路,当在PC网页端爬取遇到困难时,爬取方式可以向手机网页端转变。

第11章接着延伸第10章的问题,又提出了两种爬取思路。当在网页站点爬取遇到困难时,爬取思路可以向PC客户端和移动客户端转变,并通过两个实战项目帮助大家了解实施过程。

第12~14章由浅及深地讲解了著名爬虫框架Scrapy的运用,并通过知乎爬虫这个实战项目演示了Scrapy开发和部署爬虫的整个过程。

深入篇为第15~18章,详细介绍了大规模爬取中的去重问题以及如何通过Scrapy框架开发分布式爬虫,最后又介绍了一个较新的爬虫框架PySpider。

第15章主要讲解了海量数据的去重方式以及各种去重方式的优劣比较。

第16~17章详细介绍了如何通过Redis和Scrapy的结合实现分布式爬虫,并通过云起书院实战项目帮助大家了解整个的实现过程以及注意事项。

第18章介绍了一个较为人性化的爬虫框架PySpider,并通过爬取豆瓣读书信息来演示其基本功能。

以上就是本书的全部内容,看到以上介绍之后,是不是有赶快阅读的冲动呢?不要着急,接着往下看。● 本书特点及建议

本书总体来说是一本实战型书籍,以大量系统的实战项目为驱动,由浅及深地讲解了爬虫开发中所需的知识和技能。本书是一本适合初学者的书籍,既有对基础知识点的讲解,也涉及关键问题和难点的分析和解决,本书的初衷是帮助初学者夯实基础,实现提高。还有一点要说明,这本书对编程能力是有一定要求的,希望读者尽量熟悉Python编程。

对于学习本书有两点建议,希望能引起读者的注意。第一点,读者可根据自己的实际情况选择性地学习本书的章节,假如之前学过Python或者Web前端的知识,前两章就可以蜻蜓点水地看一下。第二点,本书中的实战项目是根据当时网页的情况进行编写的,可能当书籍出版的时候,网页的解析规则发生改变而使项目代码失效,因此大家从实战项目中应该学习分析过程和编码的实现方式,而不是具体的代码,授人以渔永远比授人以鱼更加有价值,即使代码失效了,大家也可以根据实际情况进行修改。● 致谢

写完这本书,才感觉到写书不是一件容易的事情,挺耗费心血的。不过除此之外,更多的是一种满足感,像一种别样的创业,既紧张又刺激,同时也实现了我分享知识的心愿,算是做了一件值得回忆的事情。这是我写的第一本书,希望是一次有益的尝试。

感谢父母的养育之恩,是他们的默默付出支持我走到今天。

感谢我的女朋友,在每个写书的周末都没有办法陪伴她,正是她的理解和支持才让我如此准时地完稿。

感谢长春理工大学电子学会实验室,如果没有当年实验室的培养,没有兄弟们的同甘共苦,就没有今天的我。

感谢西安电子科技大学,它所营造的氛围使我的视野更加开阔,使我的技术水平更上一层楼。

感谢机械工业出版社的吴怡编辑,没有她的信任和鼓励,就没有这本书的顺利出版。

感谢Python中文社区的大力支持。

感谢本书中所用开源项目的作者,正是他们无私的奉献才有了开发的便利。

由于作者水平有限,书中难免有误,欢迎各位业界同仁斧正!基础篇

·第1章 回顾Python编程

·第2章 Web前端基础

·第3章 初识网络爬虫

·第4章 HTML解析大法

·第5章 数据存储(无数据库版)

·第6章 实战项目:基础爬虫

·第7章 实战项目:简单分布式爬虫第1章回顾Python编程

本书所要讲解的爬虫技术是基于Python语言进行开发的,拥有Python编程能力对于本书的学习是至关重要的,因此本章的目标是帮助之前接触过Python语言的读者回顾一下Python编程中的内容,尤其是与爬虫技术相关的内容。1.1 安装Python

Python是跨平台语言,它可以运行在Windows、Mac和各种Linux/Unix系统上。在Windows上编写的程序,可以在Mac和Linux上正常运行。Python是一种面向对象、解释型计算机程序设计语言,需要Python解释器进行解释运行。目前,Python有两个版本,一个是2.x版,一个是3.x版,这两个版本是不兼容的。现在Python的整体方向是朝着3.x发展的,但是在发展过程中,大量针对2.x版本的代码都需要修改才能运行,导致现在许多第三方库无法在3.x版本上直接使用,因此现在大部分的云服务器默认的Python版本依然是2.x版。考虑到上述原因,本书采用的Python版本为2.x,确切地说是2.7版本。1.1.1 Windows上安装Python

首先,从Python的官方网站www.python.org下载最新的2.7.12版本,地址是https://www.python.org/ftp/python/2.7.12/python-2.7.12.msi。然后,运行下载的MSI安装包,在选择安装组件时,勾选上所有的组件,如图1-1所示。图1-1 Python安装界面

特别要注意勾选pip和Add python.exe to Path,然后一路点击Next即可完成安装。

pip是Python安装扩展模块的工具,通常会用pip下载扩展模块的源代码并编译安装。

Add python.exe to Path是将Python添加到Windows环境中。

安装完成后,打开命令提示窗口,输入python后出现如图1-2情况,说明Python安装成功。

当看到提示符“>>>”就表示我们已经在Python交互式环境中了,可以输入任何Python代码,回车后会立刻得到执行结果。现在,输入exit()并回车,就可以退出Python交互式环境。1.1.2 Ubuntu上的Python

本书采用Ubuntu 16.04版本,系统自带了Python 2.7.11的环境,如图1-3所示,所以不需要额外进行安装。图1-2 Python命令行窗口图1-3 Python环境

拥有了Python环境,但为了以后方便安装扩展模块,还需要安装python-pip和python-dev,在shell中执行:sudo apt-get install python-pip python-dev即可安装,如图1-4所示。图1-4 安装pip和python-dev1.2 搭建开发环境

俗话说:“工欲善其事必先利其器”,在做Python爬虫开发之前,一个好的IDE将会使编程效率得到大幅度提高。下面主要介绍两种IDE:Eclipse和PyCharm,并以在Windows 7上安装为例进行介绍。1.2.1 Eclipse+PyDev

Eclipse是一个强大的编辑器,并通过插件的方式不断拓展功能。Eclipse比较常见的功能是编写Java程序,但是通过扩展PyDev插件,Eclipse就具有了编写Python程序的功能。所以本书搭建的开发环境是Eclipset+PyDev。

Eclipse是运行在Java虚拟机上的,所以要先安装Java环境。

第一步,安装Java环境。Java JDK的下载地址为:http://www.oracle.com/technetwork/java/javase/downloads/index.html。下载页面如图1-5所示。图1-5 JDK下载界面

下载好JDK之后,双击进行安装,一直点击“下一步”即可完成安装,安装界面如图1-6所示。

安装完JDK,需要配置Java环境变量。

1)首先右键“我的电脑”,选择“属性”,如图1-7所示。

2)接着在出现的对话框中选择“高级系统设置”,如图1-8所示。图1-6 JDK安装界面图1-7 电脑属性图1-8 高级系统设置

3)在出现的对话框中选择“环境变量”,如图1-9所示。

4)新建名为classpath的变量名,变量的值可以设置为:.;%JAVA_HOME\lib;%JAVA_HOME\lib\tools.jar,如图1-10所示。

5)新建名为JAVA_HOME的变量名,变量的值为之前安装的JDK路径位置,默认是C:\Program Files\Java\jdk1.8.0_101\,如图1-11所示。

6)在已有的系统变量path的变量值中加上:;%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin,如图1-12所示,自此配置完成。

下面检验是否配置成功,运行cmd命令,在出现的对话框中输入“java-version”命令,如果出现图1-13的结果,则表明配置成功。

第二步,下载Eclipse,下载地址为:http://www.eclipse.org/downloads/eclipse-packages/,下载完后,解压就可以直接使用,Eclipse不需要安装。下载界面如图1-14所示。

第三步,在Eclipse中安装pydev插件。启动Eclipse,点击Help->Install New Software...,如图1-15所示。图1-9 环境变量图1-10 classpath环境变量图1-11 JAVA_HOME环境变量图1-12 path环境变量图1-13 java-version图1-14 下载界面图1-15 安装新软件

在弹出的对话框中,点击Add按钮。在Name中填:Pydev,在Location中填http://pydev.org/updates,然后一步一步安装下去。过程如图1-16和图1-17所示。图1-16 安装过程1图1-17 安装过程2

第四步,安装完pydev插件后,需要配置pydev解释器。在Eclipse菜单栏中,点击Windows→Preferences。在对话框中,点击PyDev→Interpreter-Python。点击New按钮,选择python.exe的路径,打开后显示出一个包含很多复选框的窗口,点击OK即可,如图1-18所示。

经过上述四个步骤,Eclipse就可以进行Python开发了。如需创建一个新的项目,选择File→New→Projects...,再选择PyDev→PyDevProject并输入项目名称,点击Finish即可完成项目的创建,如图1-19所示。

然后新建PyDev Package,就可以写代码了,如图1-20所示。图1-18 配置PyDev图1-19 新建Python工程图1-20 新建Python包1.2.2 PyCharm

PyCharm是本人用过的Python编辑器中,比较顺手,而且可以跨平台,在MacOS、Linux和Windows下都可以用。PyCharm主要分为专业版和社区版,两者的区别在于专业版一开始有30天的试用期,之后就要收费;社区版一直免费,当然专业版的功能更加强大。我们进行Python爬虫开发,社区版基本上可以满足需要,所以接下来就以社区版为例。大家可以根据自己的系统版本,进行下载安装,下载地址为:http://www.jetbrains.com/pycharm/download/#。下载界面如图1-21所示。图1-21 下载界面

以Windows为例,下载后双击进行安装,一步一步点击Next,即可完成安装。安装界面如图1-22所示。

安装完成后,运行PyCharm,创建Python项目就可以进行Python开发了,如图1-23所示。图1-22 安装界面图1-23 创建项目开发1.3 IO编程

IO在计算机中指的是Input/Output,也就是输入输出。凡是用到数据交换的地方,都会涉及IO编程,例如磁盘、网络的数据传输。在IO编程中,Stream(流)是一种重要的概念,分为输入流(Input Stream)和输出流(Output Stream)。我们可以把流理解为一个水管,数据相当于水管中的水,但是只能单向流动,所以数据传输过程中需要架设两个水管,一个负责输入,一个负责输出,这样读写就可以实现同步。本节主要讲解磁盘IO操作,网络IO操作放到之后的1.5节进行讨论。1.3.1 文件读写1.打开文件

读写文件是最常见的IO操作。Python内置了读写文件的函数,方便了文件的IO操作。

文件读写之前需要打开文件,确定文件的读写模式。open函数用来打开文件,语法如下: open(name[.mode[.buffering]])

open函数使用一个文件名作为唯一的强制参数,然后返回一个文件对象。模式(mode)和缓冲区(buffering)参数都是可选的,默认模式是读模式,默认缓冲区是无。

假设有个名为qiye.txt的文本文件,其存储路径是c:\text(或者是在Linux下的~/text),那么可以像下面这样打开文件。在交互式环境的提示符“>>>”下,输入如下内容: >>> f = open(r'c:\text\qiye.txt')

如果文件不存在,将会看到一个类似下面的异常回溯: Traceback (most recent call last): File "", line 1, in IOError: [Errno 2] No such file or directory: 'C:\\qiye.txt'2.文件模式

下面主要说一下open函数中的mode参数(如表1-1所示),通过改变mode参数可以实现对文件的不同操作。表1-1 open函数中的mode参数

这里主要是提醒一下‘b’参数的使用,一般处理文本文件时,是用不到‘b’参数的,但处理一些其他类型的文件(二进制文件),比如mp3音乐或者图像,那么应该在模式参数中增加‘b’,这在爬虫中处理媒体文件很常用。参数‘rb’可以用来读取一个二进制文件。3.文件缓冲区

open函数中第三个可选参数buffering控制着文件的缓冲。如果参数是0,I/O操作就是无缓冲的,直接将数据写到硬盘上;如果参数是1,I/O操作就是有缓冲的,数据先写到内存里,只有使用flush函数或者close函数才会将数据更新到硬盘;如果参数为大于1的数字则代表缓冲区的大小(单位是字节),-1(或者是任何负数)代表使用默认缓冲区的大小。4.文件读取

文件读取主要是分为按字节读取和按行进行读取,经常用到的方法有read()、readlines()、close()。

在“>>>”输入f=open(r‘c:\text\qiye.txt’)后,如果成功打开文本文件,接下来调用read()方法则可以一次性将文件内容全部读到内存中,最后返回的是str类型的对象: >>> f.read() "qiye"

最后一步调用close(),可以关闭对文件的引用。文件使用完毕后必须关闭,因为文件对象会占用操作系统资源,影响系统的IO操作。 >>> f.close()

由于文件操作可能会出现IO异常,一旦出现IO异常,后面的close()方法就不会调用。所以为了保证程序的健壮性,我们需要使用try...finally来实现。 try: f = open(r'c:\text\qiye.txt','r') print f.read() finally: if f: f.close()

上面的代码略长,Python提供了一种简单的写法,使用with语句来替代try...finally代码块和close()方法,如下所示: with open(r'c:\text\qiye.txt','r') as fileReader: print fileReader.read()

调用read()一次将文件内容读到内存,但是如果文件过大,将会出现内存不足的问题。一般对于大文件,可以反复调用read(size)方法,一次最多读取size个字节。如果文件是文本文件,Python提供了更加合理的做法,调用readline()可以每次读取一行内容,调用readlines()一次读取所有内容并按行返回列表。大家可以根据自己的具体需求采取不同的读取方式,例如小文件可以直接采取read()方法读到内存,大文件更加安全的方式是连续调用read(size),而对于配置文件等文本文件,使用readline()方法更加合理。

将上面的代码进行修改,采用readline()的方式实现如下所示: with open(r'c:\text\qiye.txt','r') as fileReader: for line in fileReader.readlines(): print line.strip()5.文件写入

写文件和读文件是一样的,唯一的区别是在调用open方法时,传入标识符‘w’或者‘wb’表示写入文本文件或者写入二进制文件,示例如下: f = open(r'c:\text\qiye.txt','w') f.write('qiye') f.close()

我们可以反复调用write()方法写入文件,最后必须使用close()方法来关闭文件。使用write()方法的时候,操作系统不是立即将数据写入文件中的,而是先写入内存中缓存起来,等到空闲时候再写入文件中,最后使用close()方法就将数据完整地写入文件中了。当然也可以使用f.flush()方法,不断将数据立即写入文件中,最后使用close()方法来关闭文件。和读文件同样道理,文件操作中可能会出现IO异常,所以还是推荐使用with语句: with open(r'c:\text\qiye.txt','w') as fileWriter: fileWriter.write('qiye')1.3.2 操作文件和目录

在Python中对文件和目录的操作经常用到os模块和shutil模块。接下来主要介绍一些操作文件和目录的常用方法:

·获得当前Python脚本工作的目录路径:os.getcwd()。

·返回指定目录下的所有文件和目录名:os.listdir()。例如返回C盘下的文件:os.listdir(“C:\\”)

·删除一个文件:os.remove(filepath)。

·删除多个空目录:os.removedirs(r“d:\python”)。

·检验给出的路径是否是一个文件:os.path.isfile(filepath)。

·检验给出的路径是否是一个目录:os.path.isdir(filepath)。

·判断是否是绝对路径:os.path.isabs()。

·检验路径是否真的存在:os.path.exists()。例如检测D盘下是否有Python文件夹:os.path.exists(r“d:\python”)

·分离一个路径的目录名和文件名:os.path.split()。例如:

·os.path.split(r“/home/qiye/qiye.txt”),返回结果是一个元组:(‘/home/qiye’,‘qiye.txt’)。

·分离扩展名:os.path.splitext()。例如os.path.splitext(r“/home/qiye/qiye.txt”),返回结果是一个元组:(‘/home/qiye/qiye’,‘.txt’)。

·获取路径名:os.path.dirname(filepah)。

·获取文件名:os.path.basename(filepath)。

·读取和设置环境变量:os.getenv()与os.putenv()。

·给出当前平台使用的行终止符:os.linesep。Windows使用‘\r\n’,Linux使用‘\n’而Mac使用‘\r’。

·指示你正在使用的平台:os.name。对于Windows,它是‘nt’,而对于Linux/Unix用户,它是‘posix’。

·重命名文件或者目录:os.rename(old,new)。

·创建多级目录:os.makedirs(r“c:\python\test”)。

·创建单个目录:os.mkdir(“test”)。

·获取文件属性:os.stat(file)。

·修改文件权限与时间戳:os.chmod(file)。

·获取文件大小:os.path.getsize(filename)。

·复制文件夹:shutil.copytree(“olddir”,“newdir”)。olddir和newdir都只能是目录,且newdir必须不存在。

·复制文件:shutil.copyfile(“oldfile”,“newfile”),oldfile和newfile都只能是文件;shutil.copy(“oldfile”,“newfile”),oldfile只能是文件,newfile可以是文件,也可以是目标目录。

·移动文件(目录):shutil.move(“oldpos”,“newpos”)。

·删除目录:os.rmdir(“dir”),只能删除空目录;shutil.rmtree(“dir”),空目录、有内容的目录都可以删。1.3.3 序列化操作

对象的序列化在很多高级编程语言中都有相应的实现,Python也不例外。程序运行时,所有的变量都是在内存中的,例如在程序中声明一个dict对象,里面存储着爬取的页面的链接、页面的标题、页面的摘要等信息: d = dict(url='index.html',title='首页',content='首页')

在程序运行的过程中爬取的页面的链接会不断变化,比如把url改成了second.html,但是程序一结束或意外中断,程序中的内存变量都会被操作系统进行回收。如果没有把修改过的url存储起来,下次运行程序的时候,url被初始化为index.html,又是从首页开始,这是我们不愿意看到的。所以把内存中的变量变成可存储或可传输的过程,就是序列化。

将内存中的变量序列化之后,可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上,实现程序状态的保存和共享。反过来,把变量内容从序列化的对象重新读取到内存,称为反序列化。

在Python中提供了两个模块:cPickle和pickle来实现序列化,前者是由C语言编写的,效率比后者高很多,但是两个模块的功能是一样的。一般编写程序的时候,采取的方案是先导入cPickle模块,如果此模块不存在,再导入pickle模块。示例如下: try: import cPickle as pickle except ImportError: import pickle

pickle实现序列化主要使用的是dumps方法或dump方法。dumps方法可以将任意对象序列化成一个str,然后可以将这个str写入文件进行保存。在Python Shell中示例如下: >>> import cPickle as pickle >>> d = dict(url='index.html',title='首页',content='首页') >>> pickle.dumps(d) "(dp1\nS'content'\np2\nS'\\xca\\xd7\\xd2\\xb3'\np3\nsS'url'\np4\nS'index.html'\n p5\nsS'title'\np6\ng3\ns."

如果使用dump方法,可以将序列化后的对象直接写入文件中: >>> f=open(r'D:\dump.txt','wb') >>> pickle.dump(d,f) >>> f.close()

pickle实现反序列化使用的是loads方法或load方法。把序列化后的文件从磁盘上读取为一个str,然后使用loads方法将这个str反序列化为对象,或者直接使用load方法将文件直接反序列化为对象,如下所示: >>> f=open(r'D:\dump.txt','rb') >>> d=pickle.load(f) >>> f.close() >>> d {'content': '\xca\xd7\xd2\xb3', 'url': 'index.html', 'title': '\xca\xd7\xd2\xb3'}

通过反序列化,存储为文件的dict对象,又重新恢复出来,但是这个变量和原变量没有什么关系,只是内容一样。以上就是序列化操作的整个过程。

假如我们想在不同的编程语言之间传递对象,把对象序列化为标准格式是关键,例如XML,但是现在更加流行的是序列化为JSON格式,既可以被所有的编程语言读取解析,也可以方便地存储到磁盘或者通过网络传输。对于JSON的操作,将在第5章进行讲解。1.4 进程和线程

在爬虫开发中,进程和线程的概念是非常重要的。提高爬虫的工作效率,打造分布式爬虫,都离不开进程和线程的身影。本节将从多进程、多线程、协程和分布式进程等四个方面,帮助大家回顾Python语言中进程和线程中的常用操作,以便在接下来的爬虫开发中灵活运用进程和线程。1.4.1 多进程

Python实现多进程的方式主要有两种,一种方法是使用os模块中的fork方法,另一种方法是使用multiprocessing模块。这两种方法的区别在于前者仅适用于Unix/Linux操作系统,对Windows不支持,后者则是跨平台的实现方式。由于现在很多爬虫程序都是运行在Unix/Linux操作系统上,所以本节对两种方式都进行讲解。1.使用os模块中的fork方式实现多进程

Python的os模块封装了常见的系统调用,其中就有fork方法。fork方法来自于Unix/Linux操作系统中提供的一个fork系统调用,这个方法非常特殊。普通的方法都是调用一次,返回一次,而fork方法是调用一次,返回两次,原因在于操作系统将当前进程(父进程)复制出一份进程(子进程),这两个进程几乎完全相同,于是fork方法分别在父进程和子进程中返回。子进程中永远返回0,父进程中返回的是子进程的ID。下面举个例子,对Python使用fork方法创建进程进行讲解。其中os模块中的getpid方法用于获取当前进程的ID,getppid方法用于获取父进程的ID。代码如下: import os if __name__ == '__main__': print 'current Process (%s) start ...'%(os.getpid()) pid = os.fork() if pid < 0: print 'error in fork' elif pid == 0: print 'I am child process(%s) and my parent process is (%s)',(os.getpid(), os.getppid()) else: print 'I(%s) created a chlid process (%s).',(os.getpid(),pid)

运行结果如下: current Process (3052) start ... I(3052) created a chlid process (3053). I am child process(3053) and my parent process is (3052)2.使用multiprocessing模块创建多进程

multiprocessing模块提供了一个Process类来描述一个进程对象。创建子进程时,只需要传入一个执行函数和函数的参数,即可完成一个Process实例的创建,用start()方法启动进程,用join()方法实现进程间的同步。下面通过一个例子来演示创建多进程的流程,代码如下: import os from multiprocessing import Process # 子进程要执行的代码 def run_proc(name): print 'Child process %s (%s) Running...' % (name, os.getpid()) if __name__ == '__main__': print 'Parent process %s.' % os.getpid() for i in range(5): p = Process(target=run_proc, args=(str(i),)) print 'Process will start.' p.start() p.join() print 'Process end.'

运行结果如下: Parent process 2392. Process will start. Process will start. Process will start. Process will start. Process will start. Child process 2 (10748) Running... Child process 0 (5324) Running... Child process 1 (3196) Running... Child process 3 (4680) Running... Child process 4 (10696) Running... Process end.

以上介绍了创建进程的两种方法,但是要启动大量的子进程,使用进程池批量创建子进程的方式更加常见,因为当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态生成多个进程,如果是上百个、上千个目标,手动去限制进程数量却又太过繁琐,这时候进程池Pool发挥作用的时候就到了。3.multiprocessing模块提供了一个Pool类来代表进程池对象

Pool可以提供指定数量的进程供用户调用,默认大小是CPU的核数。当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来处理它。下面通过一个例子来演示进程池的工作流程,代码如下: from multiprocessing import Pool import os, time, random def run_task(name): print 'Task %s (pid = %s) is running...' % (name, os.getpid()) time.sleep(random.random() * 3) print 'Task %s end.' % name if __name__=='__main__': print 'Current process %s.' % os.getpid() p = Pool(processes=3) for i in range(5): p.apply_async(run_task, args=(i,)) print 'Waiting for all subprocesses done...' p.close() p.join() print 'All subprocesses done.'

运行结果如下: Current process 9176. Waiting for all subprocesses done... Task 0 (pid = 11012) is running... Task 1 (pid = 12464) is running... Task 2 (pid = 11260) is running... Task 2 end. Task 3 (pid = 11260) is running... Task 0 end. Task 4 (pid = 11012) is running... Task 1 end. Task 3 end. Task 4 end. All subprocesses done.

上述程序先创建了容量为3的进程池,依次向进程池中添加了5个任务。从运行结果中可以看到虽然添加了5个任务,但是一开始只运行了3个,而且每次最多运行3个进程。当一个任务结束了,新的任务依次添加进来,任务执行使用的进程依然是原来的进程,这一点通过进程的pid就可以看出来。注意 Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。4.进程间通信

假如创建了大量的进程,那进程间通信是必不可少的。Python提供了多种进程间通信的方式,例如Queue、Pipe、Value+Array等。本节主要讲解Queue和Pipe这两种方式。Queue和Pipe的区别在于Pipe常用来在两个进程间通信,Queue用来在多个进程间实现通信。

首先讲解一下Queue通信方式。Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。有两个方法:Put和Get可以进行Queue操作:

·Put方法用以插入数据到队列中,它还有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。

·Get方法可以从队列读取并且删除一个元素。同样,Get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,分两种情况:如果Queue有一个值可用,则立即返回该值;否则,如果队列为空,则立即抛出Queue.Empty异常。

下面通过一个例子进行说明:在父进程中创建三个子进程,两个子进程往Queue中写入数据,一个子进程从Queue中读取数据。程序示例如下: from multiprocessing import Process, Queue import os, time, random # 写数据进程执行的代码: def proc_write(q,urls): print('Process(%s) is writing...' % os.getpid()) for url in urls: q.put(url) print('Put %s to queue...' % url) time.sleep(random.random()) # 读数据进程执行的代码: def proc_read(q): print('Process(%s) is reading...' % os.getpid()) while True: url = q.get(True) print('Get %s from queue.' % url) if __name__=='__main__': # 父进程创建Queue,并传给各个子进程: q = Queue() proc_writer1 = Process(target=proc_write, args=(q,['url_1', 'url_2', 'url_3'])) proc_writer2 = Process(target=proc_write, args=(q,['url_4','url_5','url_6'])) proc_reader = Process(target=proc_read, args=(q,)) # 启动子进程proc_writer,写入: proc_writer1.start() proc_writer2.start() # 启动子进程proc_reader,读取: proc_reader.start() # 等待proc_writer结束: proc_writer1.join() proc_writer2.join() # proc_reader进程里是死循环,无法等待其结束,只能强行终止: proc_reader.terminate()

运行结果如下: Process(9968) is writing... Process(9512) is writing... Put url_1 to queue... Put url_4 to queue... Process(1124) is reading... Get url_1 from queue. Get url_4 from queue. Put url_5 to queue... Get url_5 from queue. Put url_2 to queue... Get url_2 from queue. Put url_6 to queue... Get url_6 from queue. Put url_3 to queue... Get url_3 from queue.

最后介绍一下Pipe的通信机制,Pipe常用来在两个进程间进行通信,两个进程分别位于管道的两端。

Pipe方法返回(conn1,conn2)代表一个管道的两个端。Pipe方法有duplex参数,如果duplex参数为True(默认值),那么这个管道是全双工模式,也就是说conn1和conn2均可收发。若duplex为False,conn1只负责接收消息,conn2只负责发送消息。send和recv方法分别是发送和接收消息的方法。例如,在全双工模式下,可以调用conn1.send发送消息,conn1.recv接收消息。如果没有消息可接收,recv方法会一直阻塞。如果管道已经被关闭,那么recv方法会抛出EOFError。

下面通过一个例子进行说明:创建两个进程,一个子进程通过Pipe发送数据,一个子进程通过Pipe接收数据。程序示例如下: import multiprocessing import random import time,os def proc_send(pipe,urls): for url in urls: print "Process(%s) send: %s" %(os.getpid(),url) pipe.send(url) time.sleep(random.random()) def proc_recv(pipe): while True: print "Process(%s) rev:%s" %(os.getpid(),pipe.recv()) time.sleep(random.random()) if __name__ == "__main__": pipe = multiprocessing.Pipe() p1 = multiprocessing.Process(target=proc_send, args=(pipe[0],['url_'+str(i) for i in range(10) ])) p2 = multiprocessing.Process(target=proc_recv, args=(pipe[1],)) p1.start() p2.start() p1.join() p2.join()

运行结果如下: Process(10448) send: url_0 Process(5832) rev:url_0 Process(10448) send: url_1 Process(5832) rev:url_1 Process(10448) send: url_2 Process(5832) rev:url_2 Process(10448) send: url_3 Process(10448) send: url_4 Process(5832) rev:url_3 Process(10448) send: url_5 Process(10448) send: url_6 Process(5832) rev:url_4 Process(5832) rev:url_5 Process(10448) send: url_7 Process(10448) send: url_8 Process(5832) rev:url_6 Process(5832) rev:url_7 Process(10448) send: url_9 Process(5832) rev:url_8 Process(5832) rev:url_9注意 以上多进程程序运行结果的打印顺序在不同的系统和硬件条件下略有不同。1.4.2 多线程

多线程类似于同时执行多个不同程序,多线程运行有如下优点:

·可以把运行时间长的任务放到后台去处理。

·用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。

·程序的运行速度可能加快。

·在一些需要等待的任务实现上,如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源,如内存占用等。

Python的标准库提供了两个模块:thread和threading,thread是低级模块,threading是高级模块,对thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。1.用threading模块创建多线程

threading模块一般通过两种方式创建多线程:第一种方式是把一个函数传入并创建Thread实例,然后调用start方法开始执行;第二种方式是直接从threading.Thread继承并创建线程类,然后重写__init__方法和run方法。

首先介绍第一种方法,通过一个简单例子演示创建多线程的流程,程序如下: import random import time, threading # 新线程执行的代码: def thread_run(urls): print 'Current %s is running...' % threading.current_thread().name for url in urls: print '%s ---->>> %s' % (threading.current_thread().name,url) time.sleep(random.random()) print '%s ended.' % threading.current_thread().name print '%s is running...' % threading.current_thread().name t1 = threading.Thread(target=thread_run, name='Thread_1',args=(['url_1','url_2',' url_3'],)) t2 = threading.Thread(target=thread_run, name='Thread_2',args=(['url_4','url_5',' url_6'],)) t1.start() t2.start() t1.join() t2.join() print '%s ended.' % threading.current_thread().name

运行结果如下: MainThread is running... Current Thread_1 is running... Thread_1 ---->>> url_1 Current Thread_2 is running... Thread_2 ---->>> url_4 Thread_1 ---->>> url_2 Thread_2 ---->>> url_5 Thread_2 ---->>> url_6 Thread_1 ---->>> url_3 Thread_1 ended. Thread_2 ended. MainThread ended.

第二种方式从threading.Thread继承创建线程类,下面将方法一的程序进行重写,程序如下: import random import threading import time class myThread(threading.Thread): def __init__(self,name,urls): threading.Thread.__init__(self,name=name) self.urls = urls def run(self): print 'Current %s is running...' % threading.current_thread().name for url in self.urls: print '%s ---->>> %s' % (threading.current_thread().name,url) time.sleep(random.random()) print '%s ended.' % threading.current_thread().name print '%s is running...' % threading.current_thread().name t1 = myThread(name='Thread_1',urls=['url_1','url_2','url_3']) t2 = myThread(name='Thread_2',urls=['url_4','url_5','url_6']) t1.start() t2.start() t1.join() t2.join() print '%s ended.' % threading.current_thread().name

运行结果如下: MainThread is running... Current Thread_1 is running... Thread_1 ---->>> url_1 Current Thread_2 is running... Thread_2 ---->>> url_4 Thread_2 ---->>> url_5 Thread_1 ---->>> url_2 Thread_1 ---->>> url_3 Thread_2 ---->>> url_6 Thread_2 ended. Thread_1 ended.2.线程同步

如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。使用Thread对象的Lock和RLock可以实现简单的线程同步,这两个对象都有

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载