利用requests与Threading编写python多线程HTTP下载器

准备

最近神经网络研究遇到了一点瓶颈,于是想着琢磨点其他的东西缓冲一下,正好那天遇到了一些关于下载的问题,我就想着这些网络上的下载器,不是垃圾就是广告太多,还慢,既然这样,那就自己写一个呗!
基本思路就是利用requests这个模块向路由请求数据,然后open打开文件把数据写进去。考虑到多线程,那得利用open函数里的seek()去指定每个线程负责写入那一块数据,然后大家一起工作最后就拼凑成一个完整的文件。
但是,既然是写一个下载器,那么,文件中断下载了,那也不能重新下啊。于是我就增加了一个data文件,用来记录本次下载的数据指针,这样就算中途断开也能从这个data文件里读取到上一次下载了那些数据,然后继续从上次断开的地方继续下载!

分段下载的requests请求

多线程下载的关键就是如何从服务器分段请求数据,这个我也是从别人哪里看来的:

headers = {'Range': 'bytes=%d-%d'%(startSeek,endSeek)}
re = requests.get(self.url, headers=headers, stream=True)

像这样请求,服务器就会返回你 ‘bytes’ 标段的数据段。

分段存储open()

有了分开下载以后,自然是得分开存储:

with open(filePath, 'r+b') as file:
    file.seek(startSeek)
    file.write(data)

这样打开文件,利用**seek()**函数,将指针指向你所需要的位置,就会从你指定的位置开始存储数据。

下载情况的实时显示

首先既然要实时显示,要么就像pip下载东西一样,一个动态的进度条,要么就动态的跳数字,这里我想简单一点,打算在屏幕上动态跳几个数字来展示下载到什么地步了就行。
在python里,想要动态显示屏幕信息,我只知道两种方法(直接用python的动画模块除外,不过效果可能也不错的哦!)

1.就是利用大家常见的print!

print("\r 第 %d 次迭代中,第 %d 个样本的 %d 次循环迭代!"%(goNumber,i+1,cishu),end="",flush=True)

一个就是像这样利用 “\r” 以及 end="", flush=True 来告诉print函数你需要清屏。这样就可以动态显示你需要的画面了。

2.利用sys模块的write函数来实现,说实话,一开始看到这个函数的时候我以为我穿越到了java。

sys.stdout.write("\r文件下载进度:%.2f%%(%.2f M/%.2f M) - %.2f KB/s" % (getSize*100/fileSize, self.changeFormat(getSize), self.changeFormat(fileSize), self.changeFormat(speed)*1024))
sys.stdout.flush()

关于这个函数我就不多做解释了,效果与print函数是一样的。但是这个表达方式,乍一看,跟java还真的很像呢!

现在,我们需要显示出来的动态数据,包括下载数据大小,下载进度,以及实时网速。
下载数据大小,我们利用累加写入文件的data的长度来计算。

 for data in re.iter_content():
     file.write(data)
     getSize = getSize + len(data)

利用一个for循环,将数据从response里取出来,然后累加计算每个写入文件的数据段长度,这样就有了已经下载的数据大小。

计算进度则很简单了,先根据请求头,获取文件总大小,然后除一下就有了。

re = requests.head(url)
fileSize = int(re.headers['content-length'])

利用fileSize去除我们的getSize再取百分数,就有了我们的下载进度百分比。

现在计算网速。

        startTime = time.time()
        headers = {'Range': 'bytes=%d-%d'%(self.startSeek,self.endSeek)}
        re = requests.get(self.url, headers=headers, stream=True)
        getSize = self.startSeek
        timeGetSize = 0
        with open(self.filePath, 'r+b') as file:
            file.seek(getSize)
            for data in re.iter_content():
                file.write(data)
                getSize = getSize + len(data)
                timeGetSize = timeGetSize + len(data)
                endTime = time.time()
                if (endTime-startTime)>=1:
                    startTime = endTime
                    speed = timeGetSize
                    timeGetSize = 0

这是完整代码里的一整块。其中利用time模块来计算经过的时间,然后新建一个变量timeGetSize来累加计算在一秒的时间里,我们接收到多少数据。待到endTime-startTime等于一秒后,输出speed

好了,现在基本的问题都解决了,还剩一个,如何统计多线程的网速和下载信息?
没法,我只能想到利用全局变量的办法统计。

global getSizeAll
global speedAll

在程序的一开始,新建两个全局变量,然后在每一个线程里引用他,并且每个线程对应自己的索引:

getSizeAll[self.threadID] = getSize
speedAll[self.threadID] = speed

然后在外面的循环里统计这个全局变量,这样就有了多线程的网速和下载进度了。

断点下载问题

既然是一个完整的下载器,自然得有断点恢复下载的能力,在这里我的做法实时保存现在的下载进度,当现在的下载中断了以后,从保存的数据里恢复下载进度,继续下载。
因此,只要保存这个getSizeAll就行了,即每个线程自己下载到哪儿了,下次恢复的时候只要继续请求从这里开始的数据就行。
存储上,因为要存一个list,要么转二进制存文件,然后二进制解码,要么用json编码成字符,然后再json解码,这里我就用我熟悉的json编码了:

savaMsg = json.dumps(getSizeAll,ensure_ascii=False)
            with open(msgFilePath,'w') as f:
                f.write(savaMsg)

然后解码:

        with open(msgFilePath, 'r') as f:
            getSizeAll = json.loads(f.read())

遇到的坑。。。

第一个就是open函数的读写,因为涉及多线程,所以要分块读写数据,但是我一开始用的 ‘wb’ 模式来读写,最后在下载文件以后频频出现文件损坏无法打开的问题,而且下载的文件很容易莫名其妙少了一块数据,导致损坏,后来折腾了半天才发现这个问题。

r或rt 默认模式,文本模式读
rb 二进制文件

w或wt 文本模式写,打开前文件存储被清空
wb 二进制写,文件存储同样被清空

a 追加模式,只能写在文件末尾
a+ 可读写模式,写只能写在文件末尾

w+ 可读写,与a+的区别是要清空文件内容
r+ 可读写,与a+的区别是可以写到文件任何位置

完整代码

#-------多线程可断点下载器----------------
class HttpDownloadThreading(threading.Thread):
    def __init__(self, url, filePath, startSeek, endSeek, threadID):
        threading.Thread.__init__(self)
        self.url = url
        self.filePath = filePath
        self.startSeek = startSeek
        self.endSeek = endSeek
        self.threadID = threadID
    def run(self):
        global getSizeAll
        global speedAll
        startTime = time.time()
        headers = {'Range': 'bytes=%d-%d'%(self.startSeek,self.endSeek)}
        re = requests.get(self.url, headers=headers, stream=True)
        getSize = self.startSeek
        timeGetSize = 0
        with open(self.filePath, 'r+b') as file:
            file.seek(getSize)
            for data in re.iter_content():
                file.write(data)
                getSize = getSize + len(data)
                timeGetSize = timeGetSize + len(data)
                endTime = time.time()
                if (endTime-startTime)>=1:
                    startTime = endTime
                    speed = timeGetSize
                    timeGetSize = 0
                    getSizeAll[self.threadID] = getSize
                    speedAll[self.threadID] = speed
                    #sys.stdout.write("\r文件下载进度:%.2f%%(%.2f M/%.2f M) - %.2f KB/s" % (getSize*100/fileSize, self.changeFormat(getSize), self.changeFormat(fileSize), self.changeFormat(speed)*1024))
                    #sys.stdout.flush()
        getSizeAll[self.threadID] = self.endSeek
        speedAll[self.threadID] = 0
def changeFormat(x):
        return x/(1024*1024)
def HttpDownloadThreadingFunction(url, filePath, threadNum):
    msgFilePath = filePath + '.data'
    global getSizeAll
    global speedAll
    speedAll = []
    for i in range(0,threadNum):
        speedAll.append(0)
    re = requests.head(url)
    fileSize = int(re.headers['content-length'])
    if os.path.isfile(filePath):
        with open(msgFilePath, 'r') as f:
            getSizeAll = json.loads(f.read())
        sizeForThread = []
        for i in range(0, threadNum):
            sizeForThread.append(i * int(fileSize / threadNum))
        sizeForThread.append(fileSize + 1)
        for i in range(0,threadNum):
            h = HttpDownloadThreading(url,filePath,getSizeAll[i],sizeForThread[i+1], i)
            h.start()
        while 1:
            time.sleep(1)
            getSum = 0
            speedSum = 0
            for i in range(0,threadNum):
                getSum = getSum + getSizeAll[i] - sizeForThread[i]
                speedSum = speedSum + speedAll[i]
            sys.stdout.write("\r文件下载进度:%.2f%%(%.2f M/%.2f M) - %.2f KB/s" % (getSum*100/fileSize, changeFormat(getSum), changeFormat(fileSize), changeFormat(speedSum)*1024))
            sys.stdout.flush()
            if int((getSum+threadNum)/fileSize) >= 1:
                break
            savaMsg = json.dumps(getSizeAll,ensure_ascii=False)
            with open(msgFilePath,'w') as f:
                f.write(savaMsg)
    else:
        f =  open(filePath,'wb')
        f.truncate(fileSize)
        f.close()
        sizeForThread = []
        getSizeAll = []
        for i in range(0,threadNum):
            sizeForThread.append(i*int(fileSize/threadNum))
            getSizeAll.append(i*int(fileSize/threadNum))
        sizeForThread.append(fileSize+1)
        for i in range(0,threadNum):
            h = HttpDownloadThreading(url,filePath,sizeForThread[i],sizeForThread[i+1], i)
            h.start()
        while 1:
            time.sleep(1)
            getSum = 0
            speedSum = 0
            for i in range(0,threadNum):
                getSum = getSum + getSizeAll[i] - sizeForThread[i]
                speedSum = speedSum + speedAll[i]
            sys.stdout.write("\r文件下载进度:%.2f%%(%.2f M/%.2f M) - %.2f KB/s" % (getSum*100/fileSize, changeFormat(getSum), changeFormat(fileSize), changeFormat(speedSum)*1024))
            sys.stdout.flush()
            if int(getSum/fileSize) >= 1:
                break
            savaMsg = json.dumps(getSizeAll,ensure_ascii=False)
            with open(msgFilePath,'w') as f:
                f.write(savaMsg)
    try:
        os.remove(msgFilePath)
    except:
        pass
    print('下载完成')

开始下载试试:

url = 'https://d2.xia12345.com/down/109/2019/04/2HgugAHm.mp4'
HttpDownloadThreadingFunction(url, 'move.mp4', 20)

在这里插入图片描述
效果还是不错的!另外,这个网址是个彩蛋。想看的可以下载看看哦!

结尾

最后,经过我的测试,发现这个下载器性能也一般,特别在下载大文件的时候,经常直接卡主,然后线程好像直接就没了,不知道是不是python全局锁的原因,还在研究中。
另外,既然是下载器,怎么也得能使用迅雷下载地址啊。研究中。

### 回答1: Python是一种功能强大且易于学习的编程语言,它具有丰富的库和工具,可用于各种开发任务。M3U8是一种多媒体播放列表文件格式,常用于网络视频的流媒体传输。在Python中,我们可以使用多线程来实现一个M3U8多线程下载。 首先,我们需要使用requests库来获取M3U8文件的内容。使用requests库发送HTTP请求,并将M3U8文件的内容保存到本地。 接下来,我们需要解析M3U8文件,提取出其中的所有视频片段的URL。可以使用正则表达式或其他方法来实现。 然后,我们可以使用多线程来并发地下载视频片段。通过创建多个线程,并分配给每个线程不同的视频片段URL,可以同时下载多个视频片段,从而加快下载速度。 在每个线程中,我们可以使用requests库来发送HTTP请求,并将视频片段保存到本地。可以自定义保存位置和文件名。 最后,我们可以等待所有线程下载完成后,合并所有视频片段,生成完整的视频文件。可以使用Python的文件操作来实现。 需要注意的是,在多线程下载时,我们还需要处理线程间的同步和互斥问题,以确保线程安全。 总结起来,一个Python M3U8多线程下载的实现过程包括获取M3U8文件、解析M3U8文件、并发下载视频片段、合并视频片段等步骤。使用Python多线程编程可以提高下载速度,并且可以灵活地根据需要进行调整和优化。 ### 回答2: Python m3u8多线程下载是一个用Python编写的工具,用于下载m3u8视频文件。 m3u8是一种视频播放列表文件格式,它包含了视频的分片链接地址。通常情况下,m3u8文件会将一个完整的视频分成多个小的ts文件,并将这些小文件的下载地址放在m3u8文件中。因此,如果我们想要下载一个m3u8视频,我们需要逐个下载这些小文件,并将它们合并成一个完整的视频。 多线程下载是一种并发的下载方式,它可以加快下载速度。使用多线程下载可以同时下载多个分片文件,并且可以利用计算机的多核心处理,实现更高效的下载Python提供了多线程编程的支持,我们可以利用Pythonthreading模块来实现多线程下载。首先,我们需要解析m3u8文件,获取其中的分片文件链接地址。然后,我们创建多个下载线程,每个线程负责下载一个分片文件。每个线程下载完一个分片文件后,将其保存在本地存储。最后,我们可以使用其他工具将这些分片文件合并成一个完整的视频文件。 多线程下载的好处是可以减少下载时间,提高下载效率。然而,需要注意的是,多线程下载也会增加网络带宽的使用,可能会对服务造成一定的负载。因此,在使用多线程下载时,我们需要注意合理设置线程数量,避免对服务造成过大的压力。 总之,Python m3u8多线程下载是一个方便快捷的工具,可以帮助我们高效地下载m3u8视频文件。 ### 回答3: Python m3u8多线程下载是一个用Python编写的工具,用于下载m3u8视频文件。m3u8是一种基于HTTP的流媒体传输协议,常用于视频直播和点播。 使用多线程下载可以加快下载速度,提高效率。程序首先需要解析m3u8文件,获取视频的各个分片(或者说ts文件)的URL地址。然后,使用多线程技术,同时从不同的服务下载不同的分片,并将它们合并成一个完整的视频文件。 在Python中,我们可以使用多线程库(如threading)来创建和管理线程。在每个线程中,我们可以使用HTTP库(如requests)来发送下载请求,并将分片保存到本地。 为了提高下载速度,我们可以根据网络环境和计算机配置来确定线程的数量,通常建议使用2到4个线程。 此外,还可以通过设置超时时间和重试机制来处理下载中的错误和异常情况,并在下载完成后进行一些清理工作。 总的来说,Python m3u8多线程下载是一个方便实用的工具,可以帮助用户快速下载m3u8视频文件,并提供了一些自定义选项来满足不同的需求。使用这个工具,用户可以更方便地获取和保存自己想要的视频内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值