目录

  1. Introduction
  2. How
  3. Alternative
  4. Thoughts
  5. Reference
  6. END

Introduction

在进行大规模数据爬取和下载时,经常需要下载大文件,这时候就需要一个可以显示下载进度和速度的工具,以便于我们能够更好地掌控下载的情况,同时也可以避免下载过程中出现问题导致浪费时间和流量。

因此,我们需要一个可以显示下载进度和速度的工具,以便于我们更好地掌控下载的情况,避免浪费时间和流量。

本文将介绍在 Python 中如何使用 tqdm 和 requests 来实现下载进度和速度的显示。

How

要使用 tqdm 和 requests 来显示下载进度和速度,我们需要先安装 tqdm 和 requests 模块。安装方法如下(如已安装请跳过):

1
pip install tqdm requests

安装完成后,我们可以使用以下代码来下载文件并显示下载进度和速度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from pathlib import Path

import requests
from tqdm import tqdm

def download(url, folder="./", headers=None) -> str:
"""下载文件并显示进度和速度。"""
Path(folder).mkdir(exist_ok=True, parents=True)
local_filepath = Path(folder) / url.split("/")[-1]
headers = headers or {
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36"
}
with requests.get(url, stream=True, headers=headers) as r:
r.raise_for_status()
size = int(r.headers["Content-Length"])
chunk_size = 8192
# 重点。
with tqdm(
unit="B", # 1
unit_scale=True, # 2
unit_divisor=1024, # 3
miniters=1, # 4
desc=f"Downloading {local_filepath.name}", # 5
total=size, # 6
) as pbar:
with open(local_filepath, "wb") as f:
for chunk in r.iter_content(chunk_size=chunk_size):
f.write(chunk)
pbar.update(len(chunk)) # 7

return local_filepath

这段代码的核心是 tqdm,我们使用 tqdm 来创建进度条,然后在循环中更新进度条,实现了下载进度和速度的显示。详细解释如下:

  1. unit="B":将进度条的单位设置为字节。
  2. unit_scale=True:开启自动缩放功能,根据文件大小自动转换为 KB、MB、GB 等单位,默认是用 SI 公制单位(即 1000 进制),如果想使用二进制单位,则可以指定下面的 unit_divisor
  3. unit_divisor=1024:设置单位缩放的除数为1024,即二进制单位。
  4. miniters=1:设置进度条更新的最小步数。对于小文件,进度条可能会更新得太频繁,这个参数可以控制更新频率。
  5. desc="Downloading":设置进度条的描述。
  6. total=size:设置要下载的文件的总大小,这个值从HTTP响应的 Content-Length 头部获取。
  7. pbar.update(len(chunk)):更新进度条,显示已下载的数据大小。len(chunk)表示当前下载的数据块大小(单位为字节)。

效果如下:

效果图。效果图。

Alternative

又是设置一堆 tqdm 参数又是需要在循环中增加 update 语句,可能有些人会觉得稍显麻烦,有点侵入性,那我这里有一个不那么麻烦的的近似方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def download(url, folder) -> str:
"""一个近似方法。"""
Path(folder).mkdir(exist_ok=True, parents=True)
local_filepath = Path(folder) / url.split("/")[-1]
if local_filepath.exists():
logger.warning(f"{local_filepath} exists, skip.")
return local_filepath
with requests.get(url, stream=True, headers=headers) as r:
r.raise_for_status()
size = int(r.headers["Content-Length"])
chunk_size = 8192
# 重点。
n_chunks = (size + chunk_size - 1) // chunk_size
with open(local_filepath, "wb") as f:
for chunk in tqdm(
r.iter_content(chunk_size=chunk_size),
total=n_chunks, # 1
unit="KB", # 2
unit_scale=chunk_size / 1024, # 3
):
f.write(chunk)
return local_filepath

这个程序只需要在原来的 r.iter_content() 外像平常一样包一层 tqdm,然后设置较少的参数即可,省掉了一些参数和 update 语句:

  1. total=n_chunks:由于我们是分块下载的,一个很明显的思路是 total 我们只需要设置为总的块数即可,和深度学习中的 batch 类似。
  2. unit="KB":这里我们门强制让其使用 KB 为单位,而不是自动转换。
  3. unit_scale=chunk_size / 1024 :由于 total 设置的是块的数量,默认速度就会显示每秒多少个块,但是我们想让其显示每秒多少 B 或者 KB 之类的,这里我们就用这个参数来让块数 ⨉ 块大小得出来 B 数,然后再除以 1024 的到 KB 数。如果你想得到 MB 数,只需再除以 1024,并设置 unit="MB"即可。

为什么说这是“近似”做法呢?这是因为我们 unit_scale 直接乘的是块大小,然而每次得到的数据量并不一定是块大小这么大,最后一个块可能会偏小。道理很简单,把 10 按块大小为 3 拆分,最后一个块大小为 1。但是由于我们的块大小不会太大,这个影响微乎其微。

Thoughts

进度和速度显示可能是很多程序员忽略的一个问题,但是我觉得非常重要,可以说 you always need ETA(tqdm)。但是在使用的时候要注意刚开始的速度可能并不准确,尤其是任务队列中的处理时间不尽相同时。

Reference

END