行,是我写得太像稿子了,这个我认。
代码在这里,一个文件就能跑,只用标准库:
import os, sys, hashlib, urllib.request
from concurrent.futures import ThreadPoolExecutor
def head_total(url):
req = urllib.request.Request(url, method='HEAD')
with urllib.request.urlopen(req) as r:
cr = r.headers.get('Content-Range')
if cr and '/' in cr:
return int(cr.split('/')[-1])
n = r.headers.get('Content-Length')
return int(n) if n else 0
def fetch(url, path, parts=8):
total = head_total(url)
if not total:
raise SystemExit('拿不到文件总长,可能不支持 Range')
seg = total // parts
bounds = []
for i in range(parts):
s = i * seg
e = (total - 1) if i == parts - 1 else (s + seg - 1)
bounds.append((s, e))
def one(b):
s, e = b
done = 0
while done < (e - s + 1):
start = s + done
req = urllib.request.Request(url, headers={'Range': 'bytes=%d-%d' % (start, e)})
with urllib.request.urlopen(req) as r, open(path, 'r+b') as f:
f.seek(start)
while True:
chunk = r.read(1 << 20)
if not chunk:
break
f.write(chunk)
done += len(chunk)
return done
if not os.path.exists(path):
with open(path, 'wb') as f:
f.truncate(total)
with ThreadPoolExecutor(max_workers=parts) as ex:
list(ex.map(one, bounds))
size = os.path.getsize(path)
h = hashlib.sha256()
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(1 << 20), b''):
h.update(chunk)
print('size', size, 'of', total, 'sha256', h.hexdigest()[:16])
if __name__ == '__main__':
fetch(sys.argv[1], sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
几点实话:
- 这段只做分段、续传、校验。没写并发调度、重试退避、限速,也没处理服务端不支持 Range 的降级,那些是另一个量级的事。
- 段数别贪多,我这边 8 段最快,16/32 反而掉速(拥塞控制互相踩)。
- 断点续传这里靠的是"每段自己记 done 偏移",重启后再跑会从 0 开始覆盖,真要断电续传得把 done 落盘。
实测:15G 文件单连接 2 小时没下完,8 段并发 6 分多钟。 |