批量爬虫下载时,单线程下载文件有时慢有时快有那以稳定,有点浪费了我200M的带宽嘿嘿。写一个简单的多线程分块下载文件工具,从网上找了几个代码,试了一下发现有些奇怪的问题,刚开始不知道线程锁,线程安全,颇为苦恼的查了一些资料,最终调试成功,工具还是有一些不完美的,比如没有用Sterm=True模式写硬盘,而是从内存中写入文件,下载文件超过内存的大文件时,内存较小的有可能会报错。不过稍改一下也就可以了。代码备注比较完整,可以直接使用。给自已记录一下,也留给需要的童鞋们。
需要注意的就是线程锁的事情,局部变量是线程安全的。网上有的代码用的是类全局变量打开文件但在多线程中并未加锁,会导致文件有一定几率出现大小和源文件不同,即使文件大小相同,MD5值也不同,中间有一段是坏的,在图片和音频中可能只是其中一段损坏,exe,rar之类的就直接打不开了。 附上单线程下载(带进度条显示)和多线程代码的时间比较,实测多线程能有效提升下载效率,效果还是比较不错的。
# -!- coding: utf-8 -!-
import os, requests, time, threading
from queue import Queue
# import logging
from tqdm import tqdm
# lock = threading.Lock() # 改成局部变量写文件,就无需线程锁
# ===================================================================================================================
# 单线程下载文件,download_from_url(url,"./aa.mp3")
def single_thread_download(url, dst):
"""
@param: url to download file
@param: dst place to put the file
"""
#file_size = int(urlopen(url).info().get('Content-Length', -1))
file_size = int(requests.head(url).headers['Content-Length'])
if os.path.exists(dst):
first_byte = os.path.getsize(dst)
else:
first_byte = 0
if first_byte >= file_size:
return file_size
header = {"Range": "bytes=%s-%s" % (first_byte, file_size)}
pbar = tqdm(
total=file_size, initial=first_byte,
unit='B', unit_scale=True, desc=url.split('/')[-1])
req = requests.get(url, headers=header, stream=True)
with(open(dst, 'ab')) as f:
for chunk in req.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
pbar.update(1024)
pbar.close()
return file_size
class ManyThreadDownload:
def __init__(self, num=10):
self.num = num # 线程数,默认10
self.url = '' # url
self.name = '' # 目标地址
self.total = 0 # 文件大小
# 获取每个线程下载的区间
def get_range(self):
ranges = []
offset = int(self.total/self.num)
for i in range(self.num):
if i == self.num-1:
ranges.append((i*offset, ''))
else:
ranges.append(((i * offset), (i + 1) * offset - 1))
return ranges # [(0,99),(100,199),(200,"")]
# 通过传入开始和结束位置来下载文件
def download(self, ts_queue):
while not ts_queue.empty():
start_, end_ = ts_queue.get()
headers = {
'Range': 'Bytes=%s-%s' % (start_, end_),
'Accept-Encoding': '*'
}
flag = False
while not flag:
try:
# 设置重连次数
requests.adapters.DEFAULT_RETRIES = 10
# s = requests.session() # 每次都会发起一次TCP握手,性能降低,还可能因发起多个连接而被拒绝
# # 设置连接活跃状态为False
# s.keep_alive = False
# 默认stream=false,立即下载放到内存,文件过大会内存不足,大文件时用True需改一下码子
res = requests.get(self.url, headers=headers)
res.close() # 关闭请求 释放内存
except Exception as e:
print((start_, end_, "出错了,连接重试:%s", e, ))
time.sleep(1)
continue
flag = True
print("\r", ("%s-%s download success" % (start_, end_)), end="", flush=True)
# with lock:
with open(self.name, "rb+") as fd:
fd.seek(start_)
fd.write(res.content)
# self.fd.seek(start_) # 指定写文件的位置,下载的内容放到正确的位置处
# self.fd.write(res.content) # 将下载文件保存到 fd所打开的文件里
def run(self, url, name):
self.url = url
self.name = name
self.total = int(requests.head(url).headers['Content-Length'])
# file_size = int(urlopen(self.url).info().get('Content-Length', -1))
file_size = self.total
if os.path.exists(name):
first_byte = os.path.getsize(name)
else:
first_byte = 0
if first_byte >= file_size:
return file_size
self.fd = open(name, "wb") # 续传时直接rb+ 文件不存在时会报错,先wb再rb+
self.fd.truncate(self.total) # 建一个和下载文件一样大的文件,不是必须的,stream=True时会用到
self.fd.close()
# self.fd = open(self.name, "rb+") # 续传时ab方式打开时会强制指针指向文件末尾,seek并不管用,应用rb+模式
thread_list = []
ts_queue = Queue() # 用队列的线程安全特性,以列表的形式把开始和结束加到队列
for ran in self.get_range():
start_, end_ = ran
ts_queue.put((start_, end_))
for i in range(self.num):
t = threading.Thread(target=self.download, name='th-' + str(i), kwargs={'ts_queue': ts_queue})
t.setDaemon(True)
thread_list.append(t)
for t in thread_list:
t.start()
for t in thread_list:
t.join() # 设置等待,全部线程完事后再继续
self.fd.close()
if __name__ == '__main__':
start = time.perf_counter()
single_thread_download('http://wechatapppro-1252524126.file.myqcloud.com/appsVcR0oga2638/audio/ka239k150wxmes16vkc.mp3', './1.mp3')
end = time.perf_counter()
print('[Message] Running time: %s Seconds' % (end - start))
many_thread_download = ManyThreadDownload()
start = time.perf_counter()
many_thread_download.run('http://wechatapppro-1252524126.file.myqcloud.com/appsVcR0oga2638/audio/ka239k150wxmes16vkc.mp3', './2.mp3')
end = time.perf_counter()
print('\n')
print('[Message] Running time: %s Seconds' % (end - start))