python多線程下載文件


看到一篇多線程下載的文章,這里把自己的理解寫一篇多線程下載的文章。

 

我們訪問http://192.168.10.7/a.jpg時是get請求,response的head包含Content-Length: 37694

這個就是a.jpg文件的大小

抓包的話,server端是發送多個數據包(PDU)和一個文件信息,然后拼裝成了a.jpg圖片:

,部分截圖。

如果我用requests.head("http://192.168.10.7/a.jpg")時,server端只返回文件信息,而不會發送文件數據。

 response = requests.head(self.url)
 print(response.headers)

#
{'Keep-Alive': 'timeout=5, max=100', 'Accept-Ranges': 'bytes', 'Date': 'Sat, 18 Feb 2017 02:56:08 GMT', 'ETag': '"933e-548c4b0beff53"', 'Content-Type': 'image/jpeg', 'Content-Length': '37694', 'Last-Modified': 'S
at, 18 Feb 2017 02:21:39 GMT', 'Connection': 'Keep-Alive', 'Server': 'Apache/2.4.18 (Ubuntu)'}

文件a.jpg大小是37964字節

保存a.jpg文件后查看文件大小也是

 

好了,我們知道文件大小了的話,那如何多線程下載了?

假如我們用3個線程去下載a.jpg,那么我們會用線程1去下載1260x10=12600字節,線程2下載12601-25200字節,以此類推,還不夠就用線程1再去下載。

但是get請求不是會直接下載a.jpg文件了?怎么只獲取一部分文件的數據了?

我們可以在get請求的head部分加入“Range: bytes=0-12599”, 先測試下

# res.text 是將get獲取的byte類型數據自動編碼,是str類型, res.content是原始的byte類型數據
# 所以下面是直接write(res.content)
        headers = {"Range":"bytes=0-12599"}
        res = requests.get(self.url,headers=headers)
        # res.text 是將get獲取的byte類型數據自動編碼,是str類型, res.content是原始的byte類型數據
        # 所以下面是直接write(res.content)
        with open(self.filename,'wb') as f:
            f.write(res.content)

然后可以看到下載獲取的一部分圖片:

我們再獲取下一部分數據,

        headers = {"Range":"bytes=12600-25199"}
        res = requests.get(self.url,headers=headers)
        # res.text 是將get獲取的byte類型數據自動編碼,是str類型, res.content是原始的byte類型數據
        # 所以下面是直接write(res.content)
        with open(self.filename,'ab+') as f:
            print(f.tell())
            f.write(res.content)

可以看到文件:

我們知道:

r或rt 默認模式,文本模式讀
rb   二進制文件
  
w或wt 文本模式寫,打開前文件存儲被清空
wb  二進制寫,文件存儲同樣被清空
  
a  追加模式,只能寫在文件末尾
a+ 可讀寫模式,寫只能寫在文件末尾
  
w+ 可讀寫,與a+的區別是要清空文件內容
r+ 可讀寫,與a+的區別是可以寫到文件任何位置

如果是多線程的而下載的話,我們用open('file','rb+'),我先用這種模式繼續上面下載文件,上面下載到了25199字節,

那這次我從26000開始下載,f.seek(26000)后開始保存下載的文件,看文件是否能保存,看到的文件是否會中間出現空白:

        headers = {"Range":"bytes=26000-37694"}
        res = requests.get(self.url,headers=headers)
        # res.text 是將get獲取的byte類型數據自動編碼,是str類型, res.content是原始的byte類型數據
        # 所以下面是直接write(res.content)
        with open(self.filename,'rb+') as f:
            f.seek(26000)
            f.write(res.content)

下載后的文件:

這個,可能圖片顯示可能跟我們想象的不一樣,但是rb+肯定是可以從任意位置讀寫的。

還介紹一個知識點,可能在自己測試的時候用的到,就是:

 f.truncate(n):  從文件的首行首字符開始截斷,截斷文件為n個字符;無n表示從當前位置起截斷;截斷之后n后面的所有字符被刪除。

 好了,現在我們開始使用多線程下載文件:

設計思路是:

1、每個線程下載一部分數據

2、每個線程用rb+模式打開文件

3、每個線程下載數據后,用f.seek()到相應的位置,然后再寫數據。

直接f=open(),再多線程f.write()時會出現文件寫錯誤。

我們可以用os.dup()復制文件符合os.fsopen(fd,mode,buffer)來打開處理文件。

os.dup()和os.fdopen()的好處個人理解是os.dup()復制文件句柄,os.fdopen()先寫緩存,具體官方文檔還有待查證。

代碼:

版本 python3,

pip install requests

下面代碼可以拿來直接跑

#! -coding:utf8 -*-
import threading,sys
import requests
import time
import os

class MulThreadDownload(threading.Thread):
    def __init__(self,url,startpos,endpos,f):
        super(MulThreadDownload,self).__init__()
        self.url = url
        self.startpos = startpos
        self.endpos = endpos
        self.fd = f

    def download(self):
        print("start thread:%s at %s" % (self.getName(), time.time()))
        headers = {"Range":"bytes=%s-%s"%(self.startpos,self.endpos)}
        res = requests.get(self.url,headers=headers)
        # res.text 是將get獲取的byte類型數據自動編碼,是str類型, res.content是原始的byte類型數據
        # 所以下面是直接write(res.content)
        self.fd.seek(self.startpos)
        self.fd.write(res.content)
        print("stop thread:%s at %s" % (self.getName(), time.time()))
        # f.close()

    def run(self):
        self.download()

if __name__ == "__main__":
    url = sys.argv[1]
    #獲取文件的大小和文件名
    filename = url.split('/')[-1]
    filesize = int(requests.head(url).headers['Content-Length'])
    print("%s filesize:%s"%(filename,filesize))

    #線程數
    threadnum = 3
    #信號量,同時只允許3個線程運行
    threading.BoundedSemaphore(threadnum)
    # 默認3線程現在,也可以通過傳參的方式設置線程數
    step = filesize // threadnum
    mtd_list = []
    start = 0
    end = -1

    # 請空並生成文件
    tempf = open(filename,'w')
    tempf.close()
    # rb+ ,二進制打開,可任意位置讀寫
    with open(filename,'rb+') as  f:
        fileno = f.fileno()
        # 如果文件大小為11字節,那就是獲取文件0-10的位置的數據。如果end = 10,說明數據已經獲取完了。
        while end < filesize -1:
            start = end +1
            end = start + step -1
            if end > filesize:
                end = filesize
            # print("start:%s, end:%s"%(start,end))
            # 復制文件句柄
            dup = os.dup(fileno)
            # print(dup)
            # 打開文件
            fd = os.fdopen(dup,'rb+',-1)
            # print(fd)
            t = MulThreadDownload(url,start,end,fd)
            t.start()
            mtd_list.append(t)

        for i in  mtd_list:
            i.join()

執行結果:

python multiprocess_download.py http://192.168.10.7/of.tar.gz
of.tar.gz filesize:36578022
start thread:Thread-1 at 1487405833.7353075
start thread:Thread-2 at 1487405833.736311
start thread:Thread-3 at 1487405833.7378094
stop thread:Thread-1 at 1487405836.9561603
stop thread:Thread-3 at 1487405837.0016065
stop thread:Thread-2 at 1487405837.0116146

多次測試,下載后的文件都可以正常打開。

如果有多個站點有of.tar.gz文件,那更可以體現多線程下載的體驗。

根據上面的理論,我們應該可以做一個類似p2p的下載,比如10台機器,每台啟動一個agent,每個agent給server上報自己目錄下的文件信息,當有一個agent有下載文件時,會去server查詢哪些agent有這個文件,然后計算去哪些agent下載哪段數據。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM