一.爬虫Python准备

安装python,拥有一个好的IDE会大大提高效率,Eclipse或PyCharm

  1. I/O编程:
    IO在计算机中指的是Input/Output, 也就是输人输出。凡是用到数据交换的地方,都会涉及IO编程,例如磁盘、网络的数据传输。在IO编程中,Stream( 流)是一种重要的概念,分为输人流( Input Stream )和输出流( Output Stream)。 我们可以把流理解为一个水管,数据相当于水管中的水,但是只能单向流动,所以数据传输过程中需要架设两个水管,一个负责输人,一个负责输出,这样读写就可以实现同步。
    打开文件:open(python中使用with更便捷)
    文件模式:‘r’:读模式
    ‘w’:写模式
    ‘a’:追加模式
    ‘b’:二进制模式
    ‘+’:读写模式
    open函数中第三个可选参数buffering控制着文件的缓冲。如果参数是0, I/O 操作就是无缓冲的,直接将数据写到硬盘上;如果参数是1, IO操作就是有缓冲的,数据先写到内存里,只有使用flush函数或者close函数才会将数据更新到硬盘;如果参数为大于1的数字则代表缓冲区的大小(单位是字节),-1 (或者是任何负数)代表使用默认缓冲区的大小。
    文件读取
    文件读取主要是分为按字节读取和按行进行读取,经常用到的方法有read(), readlines(),close()。
    Read一次将文件内容读到内存,对于大文件可以采用read(size),readline每次读取每一行,readlines读取后按行返回列表
    Python中常用到的模块是:os和shutil
    1.1序列化操作
    对象的序列化在很多高级编程语言中都有相应的实现,程序运行时,所有的变量都是在内存中的,例如在程序中声明一个dict 对象,里面存储着爬取的页面的链接、页面的标题、页面的摘要等信息:
    d = dict (ur1=‘index.html’ ,title=‘首页’,content=‘首页’)
    在程序运行的过程中爬取的页面的链接会不断变化,比如把url 改成了second.html, 但是程序一结束或意外中断,程序中的内存变量都会被操作系统进行回收。如果没有把修改过的url存储起来,下次运行程序的时候,url 被初始化为index.html, 又是从首页开始,这是我们不愿意看到的。所以把内存中的变量变成可存储或可传输的过程,就是序列化。
    将内存中的变量序列化之后,可以把序列化后的内容写人磁盘,或者通过网络传输到别的机器上,实现程序状态的保存和共享。反过来,把变量内容从序列化的对象重新读取到内存,称为反序列化。
    在Python中提供了两个模块: cPickle 和pickle来实现序列化,前者是由C语言编写的,效率比后者高很多,但是两个模块的功能是一样的。编写程序的时候,采取的方案是先导入cPickle模块,如果此模块不存在,再导入pickle模块。
    pickle实现序列化主要使用的是dumps方法或dump方法。dumps方法可以将任意对象序列化成一个str,然后可以将这个str写人文件进行保存。如果使用dump,可以将序列化后的对象直接写入文件中。实现反序列则是使用load或loads。
    假如我们想在不同的编程语言之间传递对象,把对象序列化为标准格式是关键,例如XML,但是现在更加流行的是序列化为JSON格式,既可以被所有的编程语言读取解析,也可以方便地存储到磁盘或者通过网络传输。
    2.多进程
    Python中支持多进程有两种,一种是os中的fork方法,二是multiprocessing(windows)
    fork方法是调用一次,返回两次,原因在于操作系统将当前进程(父进程)复制出一份进程(子进程),这两个进程几乎完全相同,于是fork方法分别在父进程和子进程中返回。子进程中永远返回0,父进程中返回的是子进程的ID。下面举个例子,对Python使用fork方法创建进程进行讲解。其中os模块中的getpid方法用于获取当前进程的ID, getppid 方法用于获取父进程的ID。
    multiprocessing模块创建多进程
    multiprocessing模块提供了一个Process类来描述一个进程对象。 创建子进程时,只需要传入一个执行函数和函数的参数,即可完成一个Process 实例的创建,用start()方法启动进程,用join()方法实现进程间的同步。
    以上介绍了创建进程的两种方法,但是要启动大量的子进程,使用进程池批量创建子进程的方式更加常见,因为当被操作对象数不大时,可以直接利用multiprocessing中的Process动态生成多个进程,如果是上百个、上千个目标,手动去限制进程数量却又太过繁琐,这时候进程池Pool发挥作用的时候就到了。
    multiprocessing模块提供了一个Pool类来代表进程池对象
    Pool可以提供指定数量的进程供用户调用,默认大小是CPU的核数。当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来处理它。
    2.1进程间通信
    Python:Queue(多个进程) Pipe(两个进程)
    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异常。
    Pipe 的通信机制,Pipe常用来在两个进程间进行通信,两个进程分别位于管道的两端。Pipe方法返回( conn1, conn2)代表一个管道的两个端。Pipe 方法有duplex参数,如果duplex参数为True (默认值),那么这个管道是全双工模式,也就是说conn1和conn2均可收发。若duplex为False, conn1 只负责接收消息,conn2 只负责发送消息。send 和recv方法分别是发送和接收消息的方法。例如,在全双工模式下,可以调用conn1.send 发送消息,conn1.recv接收消息。如果没有消息可接收,recv 方法会一直阻塞。 如果管道已经被关闭,那么recv会抛出EOFError
    2.3多线程
    多线程类似于同时执行多个不同程序,多线程运行有如下优点:
    可以把运行时间长的任务放到后台去处理。
    用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。口程序的运行速度可能加快。
    在一些需要等待的任务实现上,如用户输人、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放- .些珍贵的资源,如内存占用等。
    Python的标准库提供了两个模块: thread 和threading, 绝大多数情况下,我们只需要使用threading这个高级模块。
    用threading模块创建多线程
    第一种方式是把一个函数传人并创建Thread实例,然后调用start 方法开始执行;第二种方式是直接从threading.Thread继承并创建线程类,然后重写__init__ 方法和 run方法。
    2.4线程同步
    如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。使用Thread对象的Lock和RLock可以实现简单的线程同步,这两个对象都有acquire 方法和release方法,对于那些每次只允许-一个线程操作的数据,可以将其操作放到acquire和release方法之间。
    对于Lock对象而言,如果一个线程连续两次进行acquire操作,那么由于第一次acquire之后没有release,第二次acquire将挂起线程。这会导致Lock对象永远不会release,使得线程死锁。RLock对象允许一个线程多次对其进行acquire操作,因为在其内部通过一个counter变量维护着线程acquire的次数。而且每一次的acquire操作必须有一个release操作与之对应,在所有的release操作完成之后,别的线程才能申请该RLock对象。
    2.5全局解释器锁(GIL)
    在Python的原始解释器CPython中存在着GIL( Global Interpreter Lock,全局解释器锁),因此在解释执行Python代码时,会产生互斥锁来限制线程对共享资源的访问,直到解释器遇到I/O操作或者操作次数达到一定数目时才会释放GIL。由于全局解释器锁的存在,在进行多线程操作的时候,不能调用多个CPU内核,只能利用一个内核,所以在进行CPU密集型,操作的时候,不推荐使用多线程,更加倾向于多进程。那么多线程适合什么样的应用场景呢?对于IO密集型操作,多线程可以明显提高效率,例如.Python爬虫的开发,绝大多数时间爬虫是在等待socket返回数据,网络IO的操作延时比CPU大得多。
    2.6协程
    协程( coroutine),又称微线程,纤程,是一种用户级的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,每次过程重人时,就相当于进入上一次调用的状态。在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局数据和其他资源。
    协程需要用户自己来编写调度逻辑,对于CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一-定程度上又好于多线程。那么在Python中是如何实现协程的呢?
    Python通过yield 提供了对协程的基本支持,但是不完全,而使用第三方gevent库是更好的选择, gevent提供了比较完善的协程支持。gevent是一个基于协程的Python网络函数库,使用greenlet在libev事件循环顶部提供了一个有高级别并发性的API。主要特性有以下几点:
    口基于libev的快速事件循环,Linux 上是epoll机制。口基于greenlet的轻量级执行单元。口API复用了Python标准库里的内容。口支持SSL的协作式sockets。
    口可通过线程池或c-ares实现DNS查询。
    口通过monkey patching功能使得第三方模块变成协作式。
    gevent对协程的支持,本质上是greenlet在实现切换工作。greenlet 工作流程如下:假如进行访问网络的IO操作时,出现阻塞,greenlet 就显式切换到另一段没有被阻塞的代码段执行,直到原先的阻塞状况消失以后,再自动切换回原来的代码段继续处理。因此,greenlet是一种合理安排的串行方式。
    由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待I0,这就是协程一般比多线程效率高的原因。由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,将一些常见的阻塞,如socket、select 等地方实现协程跳转,这一过程在启 动时通过monkey patch完成。
    2.7 分布式进程
    分布式进程指的是将Process 进程分布到多台机器上,充分利用多台机器的性能完成复杂的任务。我们可以将这一点应用到分布式爬虫的开发中。
    分布式进程在Python中依然要用到multiprocessing模块。multipocessing 模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。可以写一个服务进程作为调度者,将任务分布到其他多个进程中,依靠网络通信进行管理。举个例子:在做爬虫程序时,常常会遇到这样的场景,我们想抓取某个网站的所有图片,如果使用多进程的话,一般是一个进程负责抓取图片的链接地址,将链接地址存放到Queue中,另外的进程负责从Queue中读取链接地址进行下载和存储到本地。现在把这个过程做成分布式,一台机器上的进程负责抓取链接,其他机器上的进程负责下载存储。那么遇到的主要问题是将Queue暴露到网络中,让其他机器进程都可以访问。
    2.8创建分布式进程需要分为六个步骤: ;
    1)建立队列Queue, 用来进行进程间的通信。服务进程创建任务队列task_ queue, 用来作为传递任务给任务进程的通道;服务进程创建结果队列result queue,作为任务进程完成任务后回复服务进程的通道。在分布式多进程环境下,必须通过由Queuemanager获得的Queue ;接口来添加任务。
    2)把第一步中建立的队列在网络上注册,暴露给其他进程( 主机),注册后获得网络队列,相当于本地队列的映像。
    3)建立一个对象( Queuemanager(BaseManager) )实例manager,绑定端口和验证口令。
    4)启动第三步中建立的实例,即启动管理manager,监管信息通道。
    5)通过管理实例的方法获得通过网络访问的Queue 对象,即再把网络队列实体化成可以使用的本地队列。
    6)创建任务到“本地”队列中,自动上传任务到网络队列中,分配给任务进程进行处理。
    接下来编写任务进程( taskWorker.py ),创建任务进程的步骤相对较少,需要四个步骤:
    1 )使用QueueManager注册用于获取Queue的方法名称,任务进程只能通过名称来在网络上获取Queue。
    2)连接服务器,端口和验证口令注意保持与服务进程中完全-致。
    3)从网络上获取Queue,进行本地化。
    4)从task队列获取任务,并把结果写入result队列。
    3.网络编程
    计算机网络是把各个计算机连接到一起,让网络中的计算机可以互相通信。网络编程就是如何在程序中实现两台计算机的通信。例如当你使用浏览器访问谷歌网站时,你的计算机就和谷歌的某台服务器通过互联网建立起了连接,然后谷歌服务器会把把网页内容作为数据通过互联网传输到你的电脑上。网络编程对所有开发语言都是一样的,Python也不例外。使用Python进行网络编程时,实际上是在Python程序本身这个进程内,连接到指定服务器进程的通信端口进行通信,所以网络通信也可以看做两个进程间的通信。提到网络编程,必须提到的一个概念是Socket。 Socket (套接字)是网络编程的一个抽象概念,通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。Python 提供了两个基本的Socket模块:
    口Socket,提供了标准的BSD Sockets API。
    口SocketServer,提供了服务器中心类,可以简化网络服务器的开发。
    3.1Socket类型
    套接字格式为: socket(family,type[,protocal]), 使用给定的地址族、套接字类型、协议编号(默认为0)来创建套接字。
    Socket类型及说明
    socket . AF_ UNIX: 只能够用于单一的Unix系统进程间通信
    socket. AF_ INET: 服务器之间网络通信
    socket.AF_ INET6: IPv6
    socket. SOCK_ STREAM: 流式socket,用于TCP
    socket . SOCK_ DGRAM: 数据报式socket,用于UDP
    socket . SOCK_ RAW: 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_ RAW可以;其次,SOCK RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_ HDRINCL 套接字选项由用户构造IP头
    socket. SOCK SEQPACKET: 可靠的连续数据包服务
    创建TCP Socket: s=socket.socket(socket.AF INET,socket.SOCK_ STREAM)
    创建UDP Socket: s=socket.socket(socket.AF_ INET,socket.SOCK DGRAM)
    3.2Socket函数
    s.bind(address):将套接字绑定到地址,在AF INET 下,以元组( host,port)的形式表示地址
    s.listen(backlog):开始监听TCP传人连接。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了
    s.accept():接受TCP连接并返回( conn,address ),其中conn是新的套接字对象,可以用来接收和发送数据。address 是连接客户端的地址
    s.connect(address):连接到address处的套接字。一般address的格式为元组( hostname,port),如果连接出错,返回socket.error错误
    s.connect_ex( adddress):功能与connectaddress)相同,成功返回0,失败返回errno的值
    s.recv(bufsize[flag]):接受TCP套接字的数据。数据以字符串形式返回,bufsize指定要接收的最大数据量。flag 提供有关消息的其他信息,通常可以忽略
    s.send(string[,flag]):发送TCP数据。将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小
    s.sendall(string[,flag]): 完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常
    s.recvfrom(bufsize[. flag]): 接受UDP套接字的数据。与recv0类似,但返回值是( data,address)。其中data是包含接收数据的字符串,address 是发送数据的套接字地址
    s.sendto(string[,flag],address): 发送UDP数据。将数据发送到套接字,address 是形式为( ipaddr, port) 的元组,指定远程地址。返回值是发送的字节数
    s.close(): 关闭套接字
    s.getpeername():返回连接套接字的远程地址。返回值通常是元组( ipaddr,port )
    s.getsockname(): 返回套接字自己的地址。通常是一个元组( ipaddr,port)
    s.setsockopt(level,optname,value): 设置给定套接字选项的值
    s.getsockopt(level,optname[. buflen]): 返回套接字选项的值
    s.settimeout(timeout): 设置套接字操作的超时期,timneout 是一个浮点数,单位是秒。值为None表示没有超时期。一般超时期应该在刚创建套接字时设置,因为它们可能会用于连接操作(如connect()
    s.setblocking(flag): 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send0调用无法立即发送数据,将引起socketerror异常
    3.3TCP编程
    网络编程一般包括两部分:服务端和客户端。TCP是一种面向连接的通信方式,主动发起连接的叫客户端,被动响应连接的叫服务端。首先说一下服务端,创建和运行TCP服务端一般需要五个步骤:
    1)创建Socket, 绑定Socket到本地IP与端口。
    2)开始监听连接。
    3)进人循环,不断接收客户端的连接请求。4)接收传来的数据,并发送给对方数据。5)传输完毕后,关闭Socket。
    接着编写客户端,与服务端进行交互,TCP客户端的创建和运行需要三个步骤:
    1)创建Socket, 连接远端地址
    2)连接后发送数据和接收数据
    3)传输完毕后,关闭Socket
    3.4UDP编程
    TCP通信需要一个建立可靠连接的过程,而且通信双方以流的形式发送数据。相对于TCP,UDP则是面向无连接的协议。使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包,但是不关心是否能到达目的端。虽然用UDP传输数据不可靠,但是由于它没有建立连接的过程,速度比TCP快得多,对于不要求可靠到达的数据,就可以使用UDP协议。
    使用UDP协议,和TCP一样,也有服务端和客户端之分。UDP编程相对于TCP编程比较简单,服务端创建和运行只需要三个步骤:
    1)创建Socket,绑定指定的IP和端口
    2)直接发送数据和接收数据
    3)关闭Socket
    客户端只需要创建socket就可以与服务端进行数据交换
发布了34 篇原创文章 · 获赞 34 · 访问量 1112

猜你喜欢

转载自blog.csdn.net/qq_45517916/article/details/100544310
今日推荐