Python爬虫开发与项目实战(1)

第一章 回顾Python编程

1.1 安装Python(略)

1.2 搭建开发环境(略)

1.3 IO编程

文件读写

  • 打开文件

    open(name[.mode[.buffering]])
    
    • 参数mode和buffering是可选的
    • 默认模式是读模式,默认缓冲区是无
  • mode参数

    ‘r’‘w’‘a’‘b’‘+’
    读模式写模式追加模式二进制模式(可添加到其他模式中使用)读/写模式(可添加到其他模式中使用)
    • ‘b’参数一般用来处理二进制文件,如mp3音乐或者图像
  • 缓冲区

    • 如果无缓冲区,那么I/O操作会将数据直接写到硬盘上
    • 如果参数为正数,则代表缓冲区的大小,数据先写到内存里,使用flush或者close函数才会将数据更新到硬盘
    • 如果参数为负数,则取缓冲区的默认大小
  • 文件读取

    • read()方法可以一次性将文件内容全部读到内存中
    • read(size)方法可以一次读取至多size个字节
    • 如果文件是文本文件,readline()方法可以每次读取一行内容
    • readlines()方法可以一次读取所有内容并按行返回列表
    filename = '/file'
    
    #######################
    # VER 1
    #######################
    f = open(filename)
    f.read() # 文件内容的str对象
    f.close()
    
    #######################
    # VER 2
    #######################
    try:
        f = open(filename)
        print(f.read())
    finally:
        if(f):
            f.close()
            
    #######################
    # VER 3
    #######################
    with open(filename) as f:
        print(f.read())
    
    #######################
    # VER 4
    #######################
    with open(filename) as f:
        for line in f.readlines():
            print(line.strip())
    
  • 文件写入

    filename = '/file'
    str = 'write content'
    
    #######################
    # VER 1
    #######################
    f = open(filename, 'w')
    f.write(str)
    f.close()
    
    #######################
    # VER 2
    #######################
    with open(filename, 'w') as f:
        f.write(str)
    

操作文件和目录

操作作用
os.getcwd()获得当前Python脚本工作的目录路径
os.listdir()返回指定目录下的所有文件和目录名
os.remove(path)删除一个文件
os.removedirs(dir)删除多个空目录
os.path.isfile(filepath)检验给出的路径是否是一个文件
os.path.isdir(dirpath)检验给出的路径是否是一个目录
os.isabs()判断是否是绝对路径
os.path.exists()检验路径是否真的存在
os.path.split()分离一个路径的目录名和文件名
os.path.splitext()分离扩展名
os.path.dirname(filepath)获取路径名
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(dir)创建多级目录
os.mkdir(dir)创建单个目录
os.stat(file)获取文件属性
os.chmod(file)修改文件权限与时间戳
os.path.getsize(filename)获取文件大小
shutil.copytree(olddir, newdir)复制文件夹,两个参数都必须是目录,且newdir必须不存在
shutil.copyfile(oldfile, newfile), shutil.copy(oldfile, newfile)复制文件,copyfile()中两个参数都必须是文件;copy()中,oldfile只能是文件,而newfile可以是文件也可以是目标目录
shutil.move(oldpos, newpos)移动文件或目录
os.rmdir(dir), shutil.rmtree(dir)删除目录,os.rmdir()只能删除空目录;shutil.rmtree()可以删除空目录或有内容的目录

序列化操作

  • 把内存中的变量编程可存储或可传输的过程,就是序列化

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

  • 在Python中提供了两个模块来实现序列化:cPickle和pickle,前者是使用C语言编写的,效率比后者高很多

  • 一般编写程序时,会先导入cPickle模块,如果不存在该模块,再导入pickle模块:

    try:
        import cPickle as pickle
    expect ImportError:
        import pickle
    
  • dumps()方法可以将任意对象序列化成一个str,loads()方法可以读取这样的str并将其转换为对象

  • dump()方法可以将任意对象序列化并写入文件中,load()方法可以读取这样的文件并将其转换为对象

    try:
        import cPickle as pickle
    expect ImportError:
        import pickle
    
    data = '要序列化的任意格式数据'
    filename = './file'
    
    #######################
    # dumps
    #######################
    pickle.dumps(data)
    
    #######################
    # dump
    #######################
    f = open(filename, 'wb')
    pickle.dump(data, f)
    f.close()
    
    #######################
    # loads
    #######################
    loads(str) # 经过dumps得到的str
    
    #######################
    # dump
    #######################
    f = open(filename, 'rb')
    data = pickle.load(f)
    f.close()
    

1.4 进程和线程

多进程

  • Python主要有两种方法实现多进程
  • os.fork(),仅支持Unix/Linux操作系统
  • multiprocessing模块中的Process类,是跨平台的实现方式
os模块中的fork方法
  • fork方法是调用一次,返回两次。fork方法会在当前进程中复制出一份几乎完全相同的子进程

  • 子进程永远返回0,父进程返回子进程的ID

  • 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 child process (%s).'%(os.getpid(), pid))
    
    # 输出:
    # current Process (70923) start ...
    # I(70923) created a child process (70924).
    # I am child process (70924) and my parent process is (70923)
    

    image-20201018105557050

multiprocessing中的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 128285.
    # Process will start
    # Process will start
    # Process will start
    # Process will start
    # Process will start
    # Child process 1 (128287) Running...
    # Child process 0 (128286) Running...
    # Child process 4 (128290) Running...
    # Process end
    # Child process 3 (128289) Running...
    # Child process 2 (128288) Running...
    

    image-20201018153756263

multiprocessing模块进程池Pool类
  • Pool可以提供指定数量的进程供用户调用,默认大小是CPU的核数

  • 当有新的请求到来时:

    • 如果进程池未满,那么会创构建一个新的进程来执行请求
    • 如果进程池已满,那么会等待已有进程结束,再创建新的进程来执行请求
  • apply_async()方法可以向进程池请求一个进程

  • Pool对象调用join()方法会等待所有子程序执行完毕

  • 调用join()方法之前必须先调用close()方法,调用close()之后就不能继续添加新的Process了

    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) # 设置进程池的最大进程数为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 128323.
    # Waiting for all subprocesses done...
    # Task 0 (pid = 128324) is running...
    # Task 1 (pid = 128326) is running...
    # Task 2 (pid = 128325) is running...
    # Task 2 end.
    # Task 3 (pid = 128325) is running...
    # Task 1 end.
    # Task 4 (pid = 128326) is running...
    # Task 4 end.
    # Task 0 end.
    # Task 3 end.
    # All subprocesses done.
    

    image-20201018154431659

进程间通信
  • Python创建了多种进程间通信的方式,如Queue,Pipe,Value+Array等。本书主要讲解前两种方式。

  • Queue和Pipe的区别在于Pipe常用来在两个进程间通信,而Queue用来在多个进程间实现通信

  • Queue:

    • Queue是多进程安全的队列
    • put()方法用于插入数据到队列中
      • 可选参数blocked和timeout:如果blocked为True(默认),且timeout为正值,则该方法会阻塞timeout的时间,直到该队列有剩余的空间,如果超时,会抛出Queue.Full异常。如果blocked为False,且队列已满,会立即抛出Queue.Full异常
    • get()方法可以从队列中读取并删除一个元素
      • 可选参数blocked和timeout:如果blocked为True(默认),且timeout为正值,则该方法会阻塞timeout的时间,直到该队列有可选的元素,如果超时,会抛出Queue.Empty异常。如果blocked为False,且队列为空,会立即抛出Queue.Empty异常
    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(128409) is writing...
    # Put url_1 to queue...
    # Process(128410) is writing...
    # Put url_4 to queue...
    # Process(128411) 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_3 to queue...
    # Get url_3 from queue.
    # Put url_6 to queue...
    # Get url_6 from queue.
    

    image-20201018160221727

  • Pipe:

    • Pipe常用来在两个进程间进行通信,两个进程分别位于管道的两端
    • Pipe方法返回(conn1, conn2),代表一个管道的两个端
    • duplex参数:如果为True,则为全双工模式,即管道两端均可收发;如果为False,则conn1只负责接收消息,conn2只负责发送消息
    • send()方法:发送消息
    • recv()方法:接收消息
    • 如果没有消息可接收,recv方法会一直阻塞,如果管道已被关闭,那么recv方法会抛出EOFError
    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.terminate()
        
    # 输出:
    # Process(128467) send: url_0
    # Process(128468) rev:url_0
    # Process(128467) send: url_1
    # Process(128468) rev:url_1
    # Process(128467) send: url_2
    # Process(128468) rev:url_2
    # Process(128467) send: url_3
    # Process(128468) rev:url_3
    # Process(128467) send: url_4
    # Process(128467) send: url_5
    # Process(128468) rev:url_4
    # Process(128467) send: url_6
    # Process(128468) rev:url_5
    # Process(128467) send: url_7
    # Process(128467) send: url_8
    # Process(128468) rev:url_6
    # Process(128468) rev:url_7
    # Process(128468) rev:url_8
    # Process(128467) send: url_9
    # Process(128468) rev:url_9
    

    image-20201018161937256

多线程

  • 多线程类似于同时执行多个不同程序
    • 可以把运行时间长的任务放到后台去处理
    • 程序的运行速度可能加快
    • 在一些需要等待的任务实现上,如用户输入、文件读写和网络收发数据等情况下,可以释放一些珍贵的资源如内存占用等
  • Python标准库为多线程提供了两个模块:thread和threading,前者是低级模块;后者是高级模块,对前者进行了封装。绝大多数情况下只需要使用threading模块
用threading模块创建多线程:
  • threading模块一般通过两种方式创建多线程:
    • 把一个函数传入并创建Thread实例,然后调用start方法开始执行
    • 直接从thread.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_2 ---->>> url_5
# Thread_1 ---->>> url_2
# Thread_2 ---->>> url_6
# Thread_1 ---->>> url_3
# Thread_2 ended.
# Thread_1 ended.
# MainThread ended.

image-20201018161913670

  • 第二种方法:
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_2 ---->>> url_6
# Thread_1 ---->>> url_3
# Thread_1 ended.
# Thread_2 ended.
# MainThread ended.

image-20201018164106225

线程同步
  • 如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步

  • 使用Thread对象的Lock和RLock可实现简单的线程同步

    • 这两个对象都有acquire和release方法,对于那些每次只允许一个线程操作的数据,可以将其放到acquire和release之间
  • 对于Lock对象,如果一个线程连续两次进行acquire操作,由于第一次acquire没有release,第二次acquire将挂起线程,导致永远都不会release,形成死锁

  • 对于RLock对象,允许一个线程多次进行acquire操作,其内部有一个counter维护acquire的次数,而且每次acquire必须对应于一个release,在完成所有的release操作后,别的线程才能申请RLock对象

    import threading
    mylock = threading.RLock()
    num=0
    class myThread(threading.Thread):
        def __init__(self, name):
            threading.Thread.__init__(self,name=name)
    
        def run(self):
            global num
            while True:
                mylock.acquire()
                print('%s locked, Number: %d'%(threading.current_thread().name, num))
                if num>=4:
                    mylock.release()
                    print('%s released, Number: %d'%(threading.current_thread().name, num))
                    break
                num+=1
                print('%s released, Number: %d'%(threading.current_thread().name, num))
                mylock.release()
    
    
    if __name__== '__main__':
        thread1 = myThread('Thread_1')
        thread2 = myThread('Thread_2')
        thread1.start()
        thread2.start()
        
    # 输出:
    # Thread_1 locked, Number: 0
    # Thread_1 released, Number: 1
    # Thread_1 locked, Number: 1
    # Thread_1 released, Number: 2
    # Thread_1 locked, Number: 2
    # Thread_1 released, Number: 3
    # Thread_1 locked, Number: 3
    # Thread_1 released, Number: 4
    # Thread_1 locked, Number: 4
    # Thread_1 released, Number: 4
    # Thread_2 locked, Number: 4
    # Thread_2 released, Number: 4
    

    image-20201018173021507

全局解释器锁(GIL)
  • 在Python的原始解释器CPython中存在这GIL(Global Interpreter Lock全局解释器锁)
  • 由于GIL的存在,在进行多线程操作的时候,不能调用多个CPU内核,只能利用一个内核
  • 因此在进行CPU密集型操作的时候,不推荐使用多线程,而更倾向于多进程
  • 对于IO密集型操作,如Python爬虫的开发,多线程可以明显提高效率。(绝大多数时间爬虫是在等待socket返回数据,网络IO的操作延时比CPU大得多)

协程

  • 协程,又称微线程,纤程,是一种用户级的轻量级线程

  • 协程拥有自己的寄存器上下文和栈

  • 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态

  • 在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局数据和其他资源

  • 协程需要用户自己来编写调度逻辑。对于CPU来说,协程其实是单线程,所以CPU不用处理调度、切换上下文,这就省去了CPU的切换开销

  • Python通过yield提供了对协程的基本支持,但是不完全。而使用第三方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是一种合理安排的串行方式

  • 有了gevent自动切换协程,就保证总有greenlet在运行,而不是等待IO,这就是协程比一般多线程效率高的原因

  • spawn()方法可以用来形成协程

  • joinall()方法可以添加这些协程任务并启动运行

  • 从运行结果来看,3个网络操作是并发执行的,而且结束顺序不同,但其实只有一个线程

    from gevent import monkey; monkey.patch_all()
    import gevent
    import urllib.request
    
    def run_task(url):
        print('Visit --> %s' % url)
        try:
            response = urllib.request.urlopen(url)
            data = response.read()
            print('%d bytes received from %s.' % (len(data), url))
        except Exception as e:
            print(e)
    if __name__=='__main__':
        urls = ['https://github.com/','https://www.python.org/','http://www.cnblogs.com/']
        greenlets = [gevent.spawn(run_task, url) for url in urls  ]
        gevent.joinall(greenlets)
        
    # 输出:
    # Visit --> https://github.com/
    # Visit --> https://www.python.org/
    # Visit --> http://www.cnblogs.com/
    # 71025 bytes received from http://www.cnblogs.com/.
    # 219611 bytes received from https://github.com/.
    # 49982 bytes received from https://www.python.org/.
    

    image-20201018174333160

  • gevent中还提供了对池的支持

  • 当拥有动态数量的greenlet需要进行并发管理时,就可以使用池

    from gevent import monkey
    monkey.patch_all()
    import urllib.request
    from gevent.pool import Pool
    def run_task(url):
        print('Visit --> %s' % url)
        try:
            response = urllib.request.urlopen(url)
            data = response.read()
            print('%d bytes received from %s.' % (len(data), url))
        except Exception as e:
            print(e)
        return 'url:%s --->finish' % url
    
    if __name__=='__main__':
        pool = Pool(2)
        urls = ['https://github.com/','https://www.python.org/','http://www.cnblogs.com/']
        results = pool.map(run_task,urls)
        print(results)
        
    # 输出:
    # Visit --> https://github.com/
    # Visit --> https://www.python.org/
    # 219605 bytes received from https://github.com/.
    # Visit --> http://www.cnblogs.com/
    # 71065 bytes received from http://www.cnblogs.com/.
    # 49982 bytes received from https://www.python.org/.
    # ['url:https://github.com/ --->finish', 'url:https://www.python.org/ --->finish', 'url:http://www.cnblogs.com/ --->finish']
    

    image-20201018174912912

分布式进程

  • 分布式进程指的是将Process进程分布到多台机器上,充分利用多台机器的性能完成复杂的任务

  • multiprocessing的managers子模块支持把多进程分布到多台机器上

  • 通过一个服务进程作为调度者,将任务分布到其他多个进程中,依靠网络通信进行管理

  • 举个例子:要爬取一个网站上所有的图片

    • 如果使用多进程,一般是一个进程负责抓取图片的链接地址,其他进程负责从这些地址中进行下载和储存
    • 如果使用分布式,则是一台机器负责抓取链接,其他机器负责下载存储
    • 分布式遇到的问题是,抓取到的链接需要暴露在网络中,才能被其他机器访问到
    • 分布式进程将这一过程进行了封装,即“本地队列的网络化”
  • 实现上面的例子,创建分布式进程(服务进程)需要分为六个步骤

    • 建立队列Queue,用于进程间通信。在分布式多进程环境下,必须通过QueueManager获得的Queue接口来添加任务
    • 将队列在网络上注册,暴露给其他进程(主机),注册后获得网络队列,相当于本地队列的映像
    • 建立一个对象(QueueManager(BaseManager))实例manager,绑定端口和验证口令
    • 启动管理manager,监管信息通道
    • 通过管理实例的方法获得通过网络访问的Queue对象,即把网络队列实体化成可以使用的本地队列
    • 创建任务到本地队列中,自动上传任务到网络队列中,分配给任务进程进行处理
  • 创建任务进程需要以下步骤:

    • 使用QueueManager注册用于获取Queue的方法名称,任务进程只能通过名称来在网络上获取Queue
    • 使用端口和验证口令连接服务器
    • 从网络上获取Queue,进行本地化
    • 从task队列中获取任务,并把结果写入result队列
  • 服务进程样例(Linux版):

    import random,time
    import queue as Queue
    from multiprocessing.managers import BaseManager
    #实现第一步:建立task_queue和result_queue,用来存放任务和结果
    task_queue=Queue.Queue()
    result_queue=Queue.Queue()
    
    class Queuemanager(BaseManager):
        pass
    #实现第二步:把创建的两个队列注册在网络上,利用register方法,callable参数关联了Queue对象,
    # 将Queue对象在网络中暴露
    Queuemanager.register('get_task_queue',callable=lambda:task_queue)
    Queuemanager.register('get_result_queue',callable=lambda:result_queue)
    
    #实现第三步:绑定端口8001,设置验证口令‘qiye’。这个相当于对象的初始化
    manager=Queuemanager(address=('',8001),authkey=bytes('qiye', encoding='utf-8'))
    
    #实现第四步:启动管理,监听信息通道
    manager.start()
    
    #实现第五步:通过管理实例的方法获得通过网络访问的Queue对象
    task=manager.get_task_queue()
    result=manager.get_result_queue()
    
    #实现第六步:添加任务
    for url in ["ImageUrl_"+str(i) for i in range(10)]:
        print('put task %s ...' %url)
        task.put(url) 
    #获取返回结果
    print('try get result...')
    for i in range(10):
        print('result is %s' %result.get(timeout=10))
    #关闭管理
    manager.shutdown()
    
    # 输出:
    # put task ImageUrl_0 ...
    # put task ImageUrl_1 ...
    # put task ImageUrl_2 ...
    # put task ImageUrl_3 ...
    # put task ImageUrl_4 ...
    # put task ImageUrl_5 ...
    # put task ImageUrl_6 ...
    # put task ImageUrl_7 ...
    # put task ImageUrl_8 ...
    # put task ImageUrl_9 ...
    # try get result...
    # result is ImageUrl_0--->success
    # result is ImageUrl_1--->success
    # result is ImageUrl_2--->success
    # result is ImageUrl_3--->success
    # result is ImageUrl_4--->success
    # result is ImageUrl_5--->success
    # result is ImageUrl_6--->success
    # result is ImageUrl_7--->success
    # result is ImageUrl_8--->success
    # result is ImageUrl_9--->success
    
  • 服务进程样例(Windows版):

    # taskManager.py for windows
    import queue as Queue
    from multiprocessing.managers import BaseManager
    from multiprocessing import freeze_support
    #任务个数
    task_number = 10
    #定义收发队列
    task_queue = Queue.Queue(task_number)
    result_queue = Queue.Queue(task_number)
    def get_task():
        return task_queue
    def get_result():
         return result_queue
    # 创建类似的QueueManager:
    class QueueManager(BaseManager):
        pass
    def win_run():
        #windows下绑定调用接口不能使用lambda,所以只能先定义函数再绑定
        QueueManager.register('get_task_queue',callable = get_task)
        QueueManager.register('get_result_queue',callable = get_result)
        #绑定端口并设置验证口令,windows下需要填写ip地址,linux下不填默认为本地
        manager = QueueManager(address = ('127.0.0.1',8001),authkey = b'qiye')
        #启动
        manager.start()
        try:
            #通过网络获取任务队列和结果队列
            task = manager.get_task_queue()
            result = manager.get_result_queue()
            #添加任务
            for url in ["ImageUrl_"+str(i) for i in range(10)]:
                print('put task %s ...' %url)
                task.put(url)
            print('try get result...')
            for i in range(10):
                print('result is %s' %result.get(timeout=10))
        except:
            print('Manager error')
        finally:
            #一定要关闭,否则会爆管道未关闭的错误
            manager.shutdown()
    
    if __name__ == '__main__':
        #windows下多进程可能会有问题,添加这句可以缓解
        freeze_support()
        win_run()
    
  • 任务进程样例:

    #coding:utf-8
    import time
    from multiprocessing.managers import BaseManager
    # 创建类似的QueueManager:
    class QueueManager(BaseManager):
        pass
    # 实现第一步:使用QueueManager注册获取Queue的方法名称
    QueueManager.register('get_task_queue')
    QueueManager.register('get_result_queue')
    # 实现第二步:连接到服务器:
    server_addr = '127.0.0.1'
    print('Connect to server %s...' % server_addr)
    # 端口和验证口令注意保持与服务进程设置的完全一致:
    m = QueueManager(address=(server_addr, 8001), authkey=bytes('qiye', encoding='utf-8'))
    # 从网络连接:
    m.connect()
    # 实现第三步:获取Queue的对象:
    task = m.get_task_queue()
    result = m.get_result_queue()
    # 实现第四步:从task队列取任务,并把结果写入result队列:
    while(not task.empty()):
            image_url = task.get(True,timeout=5)
            print('run task download %s...' % image_url)
            time.sleep(1)
            result.put('%s--->success'%image_url)
    
    # 处理结束:
    print('worker exit.')
    
    # 输出:
    # Connect to server 127.0.0.1...
    # run task download ImageUrl_0...
    # run task download ImageUrl_1...
    # run task download ImageUrl_2...
    # run task download ImageUrl_3...
    # run task download ImageUrl_4...
    # run task download ImageUrl_5...
    # run task download ImageUrl_6...
    # run task download ImageUrl_7...
    # run task download ImageUrl_8...
    # run task download ImageUrl_9...
    # worker exit.
    
    • 服务进程(Linux版)输出:

      image-20201018182602532

    • 任务进程输出:

      image-20201018182242381

1.5 网络编程

Socket

  • Socket(套接字)是网络编程的一个抽象概念,用于表示“打开了一个网络连接”

  • 建立一个socket链接需要知道目标IP地址和端口号,以及指定协议类型

  • Python提供了两个基本的socket模块:

    • Socket:提供了标准的BSD Sockets API
    • SocketServer:提供了服务器中心类,可以简化网络服务器的开发
  • 套接字格式为:socket(family, type[, protocal]),使用给定的地址族,套接字类型,协议编号(默认为0)来创建套接字

  • 套接字类型:

    Socket类型描述
    socket.AF_UNIX只能用于单一的Unix系统进程间通信
    socket.AF_INET服务器之间网络通信
    socket.AF_INET6IPv6
    socket.SOCK_STREAM流式socket,用于TCP
    socket.SOCK_DGRAM数据报式socket,用于UDP
    socket.SOCK_RAW原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而原始套接字可以。其次,SOCK_RAW可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头
    socket.SOCK_SEQPACKET可靠的连续数据包服务
    创建TCP Sockets=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    创建UDP Sockets=socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  • Socket函数:

    Socket函数描述
    服务端Socket函数
    s.bind(address)将套接字绑定到地址,在AF_INET下,以元组(host, port)的形式表示地址
    s.listen(backlog)开始监听TCP传入连接。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量,至少为1,大部分应用程序设为5就可以了
    s.accept()接受TCP连接并返回(conn, address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址
    客户端Socket函数
    s.connect(address)连接到address的套接字。一般address的格式为元组(hostname, port),如果连接出错,返回socket.error错误
    s.connect_ex(address)功能与s.connect相同,但成功返回0,失败返回errno的值
    公共Socket函数
    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套接字的数据。与s.recv()类似,但返回值是(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)设置套接字操作的超时期,timeout是一个浮点数,单位是秒,为None时表示没有超时期。一般应在刚创建套接字时设置,因为可能会用于连接操作。
    s.setblocking(flag)如果flag非0,则将套接字设为非阻塞模式,否则设为阻塞模式(默认)。非阻塞模式下,send无法发送数据,或recv无法接收数据,将引起socket.error异常

TCP编程

  • TCP是一种面向连接的通信方式,主动发起的连接叫客户端,被动响应连接的叫服务端

  • 服务端创建和运行TCP连接需要以下步骤:

    • 创建socket,绑定socket到本地IP与端口
    • 开始监听连接
    • 进入循环,不断接收客户端的连接请求
    • 接收传来的数据,并发送给对方数据
    • 传输完毕后,关闭socket
    #coding:utf-8
    import socket
    import threading
    import time
    def dealClient(sock, addr):
        #第四步:接收传来的数据,并发送给对方数据
        print('Accept new connection from %s:%s...' % addr)
        sock.send(b'Hello,I am server!')
        while True:
            data = sock.recv(1024)
            time.sleep(1)
            if not data or data.decode('utf-8') == 'exit':
                break
            print('-->>%s!' % data.decode('utf-8'))
            sock.send(('Loop_Msg: %s!' % data.decode('utf-8')).encode('utf-8'))
        #第五步:关闭套接字
        sock.close()
        print('Connection from %s:%s closed.' % addr)
    if __name__=="__main__":
        #第一步:创建一个基于IPv4和TCP协议的Socket
        # 套接字绑定的IP(127.0.0.1为本机ip)与端口
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.bind(('127.0.0.1', 9999))
        #第二步:监听连接
        s.listen(5)
        print('Waiting for connection...')
        while True:
            # 第三步:接受一个新连接:
            sock, addr = s.accept()
            # 创建新线程来处理TCP连接:
            t = threading.Thread(target=dealClient, args=(sock, addr))
            t.start()
    
    # 输出:
    # Waiting for connection...
    # Accept new connection from 127.0.0.1:37092...
    # -->>Hello,I am a client!
    # Connection from 127.0.0.1:37092 closed.
    
  • 客户端创建和运行TCP连接需要以下步骤:

    • 创建socket,连接远程地址
    • 连接后发送数据和接收数据
    • 传输完毕后,关闭socket
    #coding:utf-8
    import socket
    #初始化Socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #连接目标的ip和端口
    s.connect(('127.0.0.1', 9999))
    # 接收消息
    print('-->>'+s.recv(1024).decode('utf-8'))
    # 发送消息
    s.send(b'Hello,I am a client')
    print('-->>'+s.recv(1024).decode('utf-8'))
    s.send(b'exit')
    #关闭套接字
    s.close()
    
    # 输出:
    # -->>Hello,I am server!
    # -->>Loop_Msg: Hello,I am a client!
    
  • 服务器端输出:

    image-20201018191757479

  • 客户端输出

    image-20201018191817058

UDP编程

  • TCP是面向连接的协议,需要建立连接,以流的形式发送数据

  • UDP无连接的协议,但发送数据后无法确保数据能够到达目的端

  • UDP具有不可靠性,但速度比TCP快得多

  • 服务端创建和运行UDP需要以下步骤:

    • 创建socket,绑定指定的IP和端口
    • 直接发送数据和接收数据
    • 关闭socket
    #coding:utf-8
    import socket
    #创建Socket,绑定指定的ip和端口
    #SOCK_DGRAM指定了这个Socket的类型是UDP。绑定端口和TCP一样。
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.bind(('127.0.0.1', 9999))
    print('Bind UDP on 9999...')
    while True:
        # 直接发送数据和接收数据
        data, addr = s.recvfrom(1024)
        print('Received from %s:%s.' % addr)
        s.sendto(b'Hello, %s!' % data, addr)
    
    # 输出:
    # Bind UDP on 9999...
    # Received from 127.0.0.1:59835.
    # Received from 127.0.0.1:59835.
    
  • 客户端创建和运行UDP需要以下步骤:

    • 创建socket
    • 与服务端进行数据交换
    • 关闭socket
    #coding:utf-8
    import socket
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    for data in [b'Hello', b'World']:
        # 发送数据:
        s.sendto(data, ('127.0.0.1', 9999))
        # 接收数据:
        print(s.recv(1024).decode('utf-8'))
    s.close()
    # 输出:
    # Hello, Hello!
    # Hello, World!
    
  • 服务器端输出:

    image-20201018192953274

  • 客户端输出:

image-20201018192942338

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值