10min手寫(b六):b面試題解析丨Python實現多連接下載器
作者:蝸牛 shengxinjing (woniuppp) · GitHub
今天群里看到有人問關於python多線程寫文件的問題,聯想到這是reboot的架構師班的入學題,我想了一下,感覺坑和考察的點還挺多,可以當成一個面試題來問,簡單說一下我的想法和思路吧,涉及的代碼和注釋在github 跪求star
當年的網路螞蟻「多點同時下載、並支持斷點續傳」的奧秘就在這道面試題里了。
1998年,中國互聯網的主旋律是下載。國內互聯網超過90%的流量,來自於一群早期的電腦發燒友,從有限的中國出口帶寬裡面,不顧一切地下載來自國外的各種軟體。33.6的Modem最可能碰到的問題是掉線,辛苦下載了2-3個小時的20M文件,又要重新下載。
所以使用人數最高的軟體,不是ICQ(聊天軟體),也不是Foxmail(郵件客戶端),甚至不是Netscape(網景瀏覽器),而是下載軟體,但是國外老牌的下載軟體放到國內一旦斷線,網速慢和重新下載便讓人極為煩躁。
很快,一個符合中國國情的下載軟體「網路螞蟻」嶄露頭角。網路螞蟻是世界第一款使用了多點同時下載、並支持斷點續傳的軟體。
From: 創業故事:還記得「網路螞蟻」嗎?
本文需要一定的python基礎,希望大家對下面幾個知識點有所了解
- python文件處理,open write
- 簡單了解http協議頭信息
- os,sys模塊
- threading模塊多線程
- requests模塊發請求
題目既然是多線程下載,首先要解決的就是下載問題,為了方便測試,我們先不用QQ安裝包這麼大的,直接用pc大大英明神武又很內涵的頭像舉例,大概是這個樣子(http://51reboot.com/src/blogimg/pc.jpg)
下載
python的requests模塊很好的封裝了http請求,我們選擇用它來發送http的get請求,然後寫入本地文件即可(關於requests和http,以及python處理文件的具體介紹,可以百度或者持續關注,後面我會寫),思路既然清楚了,代碼就呼之欲出了
# 簡單粗暴的下載nimport requestsnnres=requests.get(http://51reboot.com/src/blogimg/pc.jpg)nwith open(pc.jpg,w) as f:n f.write(res.content)n
運行完上面的代碼,文件夾下面多了個pc.jpg 就是你想要的圖片了
上面代碼功能太少了,注意 ,我們的要求是多線程下載,這種簡單粗暴的下載完全不符合要求,所謂多線程,你可以理解為倉庫里有很多很多袋奧利奧餅乾,老闆讓我去都搬到公司來放好,而且要按照原順序放好
上面的代碼,大概就是我一個人去倉庫,把所有奧利奧一次性拿回來,大概流程如下(圖不清戳大)
我們如果要完成題目多線程的要求,首先就要把任務拆解,拆成幾個子任務,子任務之間可以並行執行,並且執行結果可以匯總成最終結果
拆解任務
為了完成這個任務,我們首先要知道數據到底有多大,然後把數據分塊去取就OK啦,我們要對http協議有一個很好的了解
- 用head方法請求數據,返回只有http頭信息,沒有主題部分
- 我們從頭信息Content-length的值,知道資源的大小,比如有50位元組
- 比如我們要分四個線程,每個線程去取大概1/4即可
- 50/4=12,所以前幾個線程每人取12個位元組,最後一個現成取剩下的即可
- 每個線程取到相應的內容,文件中seek到相應的位置再寫入即可
- file.seek
- 為了方便理解,一開始我們先用單線程的跑通 流程圖大概如下(圖不清戳大)
思路清晰了,代碼也就呼之欲出了,我們先測試一下range頭信息
http頭信息中的Range信息,用於請求頭中,指定第一個位元組的位置和最後一個位元組的位置,如1-12,如果省略第二個數,就認為取到最後,比如36-
# range測試代碼nimport requestsn# http頭信息,指定獲取前15000個位元組nheaders={Range:Bytes=0-15000,Accept-Encoding:*}nres=requests.get(http://51reboot.com/src/blogimg/pc.jpg,headers=headers)nnwith open(pc.jpg,w) as f:n f.write(res.content)n
我們得到了頭像的前15000個位元組,如下圖,目測range是對的
繼續豐富我們的代碼
- 要先用requests.head方法去獲取數據的長度
- 確認開幾個線程後,給每個線程確認要獲取的數據區間,即Range欄位的值
- seek寫文件
- 功能比較複雜了,我們需要用面向對象來組織一下代碼
- 先寫單線程,逐步優化
- 代碼呼之欲出了
import requestsn# 下載器的類nclass downloader:n # 構造函數n def __init__(self):n # 要下載的數據連接n self.url=http://51reboot.com/src/blogimg/pc.jpgn # 要開的線程數n self.num=8n # 存儲文件的名字,從url最後面取n self.name=self.url.split(/)[-1]n # head方法去請求urln r = requests.head(self.url)n # headers中取出數據的長度n self.total = int(r.headers[Content-Length])n print type(total is %s % (self.total))n def get_range(self):n ranges=[]n # 比如total是50,線程數是4個。offset就是12n offset = int(self.total/self.num)n for i in range(self.num):n if i==self.num-1:n # 最後一個線程,不指定結束位置,取到最後n ranges.append((i*offset,))n else:n # 沒個線程取得區間n ranges.append((i*offset,(i+1)*offset))n # range大概是[(0,12),(12,24),(25,36),(36,)]n return rangesn def run(self):nn f = open(self.name,w)n for ran in self.get_range():n # 拼出Range參數 獲取分片數據n r = requests.get(self.url,headers={Range:Bytes=%s-%s % ran,Accept-Encoding:*})n # seek到相應位置n f.seek(ran[0])n # 寫數據n f.write(r.content)n f.close()nnif __name__==__main__:n down = downloader()n down.run()n
多線程
多線程和多進程是啥在這就不多說了,要說明白還得專門寫個文章,大家知道threading模塊是專門解決多線程的問題就OK了,大概的使用方法如下,更詳細的請百度或者關注後續文章
- threading.Thread創建線程,設置處理函數
- start啟動
- setDaemon 設置守護進程
- join設置線程等待
- 代碼如下
import requestsnimport threadingnnclass downloader:n def __init__(self):n self.url=http://51reboot.com/src/blogimg/pc.jpgn self.num=8n self.name=self.url.split(/)[-1]n r = requests.head(self.url)n self.total = int(r.headers[Content-Length])n print total is %s % (self.total)n def get_range(self):n ranges=[]n offset = int(self.total/self.num)n for i in range(self.num):n if i==self.num-1:n ranges.append((i*offset,))n else:n ranges.append((i*offset,(i+1)*offset))n return rangesn def download(self,start,end):n headers={Range:Bytes=%s-%s % (start,end),Accept-Encoding:*}n res = requests.get(self.url,headers=headers)n print %s:%s download success%(start,end)n self.fd.seek(start)n self.fd.write(res.content)n def run(self):n self.fd = open(self.name,w)n thread_list = []n n = 0n for ran in self.get_range():n start,end = rann print thread %d start:%s,end:%s%(n,start,end)n n+=1n thread = threading.Thread(target=self.download,args=(start,end))n thread.start()n thread_list.append(thread)n for i in thread_list:n i.join()n print download %s load success%(self.name)n self.fd.close()nif __name__==__main__:n down = downloader()n down.run()n
執行python downloader效果如下
total is 21520nthread 0 start:0,end:2690nthread 1 start:2690,end:5380nthread 2 start:5380,end:8070nthread 3 start:8070,end:10760nthread 4 start:10760,end:13450nthread 5 start:13450,end:16140nthread 6 start:16140,end:18830nthread 7 start:18830,end:n0:2690 is endn2690:5380 is endn13450:16140 is endn10760:13450 is endn5380:8070 is endn8070:10760 is endn18830: is endn16140:18830 is endndownload pc.jpg load successn
run函數做了修改,加了多線程的東西,加了一個download函數專門用來下載數據塊,這倆函數詳細解釋如下
def download(self,start,end):n #拼接Range欄位,accept欄位支持所有編碼n headers={Range:Bytes=%s-%s % (start,end),Accept-Encoding:*}n res = requests.get(self.url,headers=headers)n print %s:%s download success%(start,end)n #seek到start位置n self.fd.seek(start)n self.fd.write(res.content)ndef run(self):n # 保存文件打開對象n self.fd = open(self.name,w)n thread_list = []n #一個數字,用來標記列印每個線程n n = 0n for ran in self.get_range():n start,end = rann #列印信息n print thread %d start:%s,end:%s%(n,start,end)n n+=1n #創建線程 傳參,處理函數為downloadn thread = threading.Thread(target=self.download,args=(start,end))n #啟動n thread.start()n thread_list.append(thread)n for i in thread_list:n # 設置等待n i.join()n print download %s load success%(self.name)n #關閉文件n self.fd.close()n
持續可以優化的點
- 一個文件描述符多個進程用會出問題
- 建議用os.dup複製文件描述符和os.fdopen來打開處理文件
- 要下載的資源地址和線程數,應該做成命令行傳進來的
- 用sys.argv獲取命令行參數
- 支持python downloader.py url num這種寫法
- 參數數量不對或者格式不對時報錯
- 各種容錯處理
- 正所謂女人的迪奧,男人的奧利奧,這篇文章,你值得擁有
大概就是這樣了,我也是正在學習python,文章代表我個人看法,有錯誤不可避免,歡迎大家指正,共同學習,本文完整代碼在github,跪求大家star
最後做個小廣告,歡迎大家關注公共號,高品質運維開發,我們每周五晚上還會做線上公開課,加QQ群 368573673 報名即可,都是關於linux,運維,python和前端的相關內容
運維開發交流QQ群: 238757010
歡迎大家關注公共號,高品質運維開發
基情授權,非授權拒絕任何形式的轉載。
關於Reboot:
專註於互聯網運維開發分享、交流,讓更多的運維工程師更加專註於自動化,為國內公有雲開發、監控、運維貢獻自己的力量。這裡聚集著國內一線互聯網工程師,樂於分享與交流 。發現文章不錯的話請關注我們。
推薦閱讀: