Python爬蟲開發與專案實戰(1)

_王泥煤發表於2020-10-18

第一章 回顧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

相關文章