news 2026/10/16 3:57:06

用Python实现Human3.6M数据集断点续传与并发下载

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python实现Human3.6M数据集断点续传与并发下载

简介:面向Python开发与机器学习初学者,资源包详解了如何获取并预处理Human3.6M 3D人体姿态数据集,覆盖从环境配置、数据下载解压,到利用h5py读取HDF5文件、提取3D关节坐标、完成数据标准化与训练/验证/测试划分的完整流程。包体为zip压缩包,共14个文件,以4个Python脚本为核心,分别承担批量下载、完整性校验、自动解压与数据提取任务,另含Dockerfile与docker-compose.yml用于一键搭建可复现的容器环境,以及requirements.txt依赖清单和README说明文档,整体仅34KB,轻量且便于快速接入。已有3632人学习下载,内容紧贴3D人体姿态估计实战,能够帮助读者将原始公共数据集转化为可直接用于模型训练的格式,同时掌握多视角动作数据的组织方式与可视化对比方法,适合正在开展姿态识别项目或准备数据集建模的Python开发者。

1. 折腾过 Human3.6M 的人,都知道下载这一步有多烦

做 3D 人体姿态估计的同行,对 Human3.6M 数据集肯定不陌生——它几乎是人手一份的基线数据。但很多人没意识到,真正的门槛不在模型训练,而在怎么把这份数据完整拿下来。这个数据集体积以 GB 计,官网按 Subject 和 Action 分成几十个子目录,逐个点着下载不仅慢,中途断一次网就得从头再来。我早期做模拟项目X时,因为下载中断丢过三天的工作量,后来干脆写了一套完整的 Python 获取脚本,把断点续传、并发下载、格式校验和预处理一次搞定。这篇笔记就把这套流程拆开讲透:数据目录长什么样、怎么用 Python 稳定下载、并发参数怎么设、哪些坑我踩过之后才绕开。

2. Human3.6M 的目录结构:搞懂你下载的到底是什么

2.1 数据集的整体组织方式与命名规则

Human3.6M 的核心数据按受试者编号划分,常见的编号包括 S1、S5、S6、S7、S8、S9、S11 这些。每个受试者目录下再按动作类别细分,动作名基本是语义化的单词,比如 Directions、Discussion、Eating、Greeting、Phoning、Photo、Posing、Purchases、Sitting、SittingDown、Smoking、Waiting、WalkDog、Walking、WalkTogether。每个动作名称下还会继续按执行次数切分成多个子目录或文件,命名里带数字编号,代表这个动作做了第几遍。

训练集和测试集的分法很多人在写代码时才反应过来:通常是 S1、S5、S6、S7、S8 作为训练数据,S9、S11 作为测试数据。这里有个容易踩的细节——同一个动作在训练和测试里都存在,但它们的文件编号不连续,不能按顺序盲切,必须按受试者维度去过滤。

每个动作的子目录里,除了视频文件,更关键的是对应的 Mat 标注文件。Mat 文件里存的是关节点数据,包括 2D 关节点坐标、3D 关节点坐标、相机内外参数、动作标签这些字段。如果你只是做 3D 姿态估计的训练验证,完全可以只下载 Mat 文件跳过视频,体积能省掉七成以上。

2.2 数据格式选型:FirstName 与直接解析原始包

Mat 文件本身是 MATLAB 的 HDF5 格式,Python 这边用 scipy.io.loadmat 就能读,但版本不同会碰到两个问题:一是新版本 scipy 对旧版 MATLAB 的 v7.3 文件支持有变化,读出来是 HDF5 对象而不是直接可用的数组;二是字段名在不同子目录下的一致性依赖官方文档,很多初学者拿到手才发现字段名对不上。

我一般会先在本地建一个清单文件,记录每个受试者、每个动作、每个 trial 对应的文件路径和官方标注的字段名,再写统一的解析函数。这样后续不管是转成 COCO 格式还是直接喂给模型,都有一份干净的元数据兜底。

还有个常见做法是把原始 Mat 转成中间格式再存,比如先转成 numpy 的 npz 或者 JSON 结构,目的是避免每次训练都要重新解析大的 Mat 文件。转格式不只是为了省时间,更重要的是把不同受试者的数据统一成一个 schema,省得训练时写一堆分支判断。

3. 用 Python 实现断点续传下载:requests 与分块写盘

3.1 为什么不用 wget 硬拉数据

很多人的第一反应是直接拿 wget 递归镜像下来,真去试就会发现不好使。这个数据集的下载链接不是静态目录映射,URL 里往往带有时效性的参数,递归抓取容易抓到一堆 403 或者跳转到错误页面。

另一个问题在于断点续传。wget 的 -c 参数能做续传,但遇到服务器返回重定向响应时,Range 头处理不严谨的情况时有发生。我用 wget 硬拉过一个 2GB 以上的大文件,中途断了续传,结果服务器从头开始发数据,前几秒钟的流量全部白费。还有更隐蔽的情况:服务器已经返回了完整内容,但响应头里没带 Content-Length,本地文件大小对不上,你根本不知道数据有没有损坏。

所以最后我选择直接用 Python 的 requests 写下载器,理由很简单:每个请求的 Header 可控,Range 逻辑透明,重试策略完全由自己定义,出了问题能看到具体是哪一步挂了。

3.2 实现一个支持 Range 续传的单文件下载器

先写一个基础版本,单文件下载,带断点续传和大小校验:

import os import requests def download_file(url, local_path, chunk_size=1024 * 512): """ 下载单个文件,支持断点续传。 url: 文件直链 local_path: 本地保存路径 chunk_size: 每次写入磁盘的块大小,默认 512KB """ # 目录不存在先创建 os.makedirs(os.path.dirname(local_path), exist_ok=True) # 已存在的部分文件大小,用于拼接 Range 头 existing_size = 0 if os.path.exists(local_path): existing_size = os.path.getsize(local_path) headers = {'Range': f'bytes={existing_size}-'} # stream=True 保证响应体不一次性加载进内存 resp = requests.get(url, headers=headers, stream=True, timeout=30) if resp.status_code == 416: # 416 表示 Range 超出文件范围,说明本地文件已经完整 print(f'{local_path} 已完整,跳过') return True if resp.status_code not in (200, 206): print(f'下载失败,状态码: {resp.status_code}') return False mode = 'ab' if existing_size > 0 else 'wb' total = int(resp.headers.get('Content-Length', 0)) + existing_size with open(local_path, mode) as f: downloaded = existing_size for chunk in resp.iter_content(chunk_size=chunk_size): if not chunk: continue f.write(chunk) downloaded += len(chunk) done = downloaded / total * 100 print(f'\r进度: {done:.1f}%', end='', flush=True) print() return True

这段代码的核心逻辑是先检查本地已有文件的大小,拼到 Range 头里发给服务器,服务器返回 206 就说明它认这个断点,从指定位置继续发数据。如果本地文件已经完整,服务器会返回 416,这时候直接在代码里判定为已完成。

有几处参数值得说明。chunk_size 控制每次从网络读多少字节后写一次磁盘,设得太小比如 4KB 会让磁盘写入频繁、CPU 占用偏高,太大则内存占用上升。512KB 到 1MB 之间是比较平衡的区间。timeout=30 表示连接阶段最多等 30 秒,但下载过程中没有数据返回不代表超时,真正的大文件传输瓶颈往往在服务器响应慢,这个参数应该配合下面的重试机制一起用。

3.3 传输中断后的恢复策略与状态码解读

实际跑下载时最崩溃的往往是网络闪断。requests 在连接层面抛出的异常和 HTTP 状态码是两回事,状态码说明服务器收到了请求,异常说明请求根本没到服务器。这导致重试逻辑必须分层设计。

我一般按这个思路写:遇到 requests.exceptions.ConnectionError,说明网络层断了,等五秒重试一次,连续失败三次就放弃当前文件并记录日志。遇到状态码 403 或 404,直接放弃当前文件,这种错误重试一百次也没用。遇到 500 或 502,等十秒重试,因为服务器可能在临时维护。

另外有个细节容易翻车:代理配置。如果本机设置了 HTTP 代理,requests 会默认走代理,但代理服务器有时会剥掉 Range 头,导致明明写了断点续传却每次都从头下载。血泪经验是先确认环境变量里没有 http_proxy、https_proxy,再跑脚本。

4. 并发下载加速:线程池与连接数控制

4.1 数据集多文件并发下载的整体思路

Human3.6M 数据集文件数量很多,如果串行下载几十个大文件,即使每个文件都是几 GB 级别,时间也会拉得很长。我算过一笔账:串行下载 30 个文件、平均带宽 10MB/s,每天跑满也只有 25 个 TB 级别的文件,折腾不起。线程池方案能把这些文件划分到多个连接里,让资源利用率明显提升。

但并发不是越多越好。服务器对同一个 IP 的并发连接数量是有限制的,连接数超过阈值会直接返回 503 或者丢掉部分请求,导致下载的文件损坏。实践下来,4 到 8 个并发是比较稳妥的区间,超过 12 个以后收益增长很小,失败率反而上升。

这个数据集的服务器响应速度不算快,每个请求要经过几次重定向才能拿到真实文件地址。所以并发策略应该是:先拿到完整的文件清单,再按固定并发数跑线程池,每个线程处理一个文件的下载,而不是每个请求单独开线程。

4.2 ThreadPoolExecutor 实现批量下载与进度回显

import os import threading from concurrent.futures import ThreadPoolExecutor, as_completed import requests # 线程锁用于保证进度打印不串行 print_lock = threading.Lock() def download_with_progress(url, local_path, chunk_size=1024 * 512): """带进度回显的单文件下载,失败返回文件路径方便二次处理""" try: ok = download_file(url, local_path, chunk_size) if ok: with print_lock: print(f'[完成] {local_path}') else: with print_lock: print(f'[失败] {local_path}') except Exception as e: with print_lock: print(f'[异常] {local_path} - {e}') def batch_download(file_list, max_workers=6): """ 批量下载入口。 file_list: [(url, local_path), ...] max_workers: 并发线程数 """ total = len(file_list) finished = 0 with ThreadPoolExecutor(max_workers=max_workers) as executor: future_to_path = { executor.submit(download_with_progress, url, path): path for url, path in file_list } for future in as_completed(future_to_path): finished += 1 with print_lock: print(f'总进度: {finished}/{total}')

这个并发版的逻辑很简单:把文件列表映射成线程池任务,as_completed 在有任务完成时直接返回,主线程就能更新总进度。每个任务的异常都在 download_with_progress 内部捕获,不会影响整个线程池的运行。

这里有个容易被忽略的参数:ThreadPoolExecutor 的 max_workers 不只是线程数,它同时决定最多同时存在多少个未完成的任务。如果你的文件列表里有几百个文件,max_workers=6 意味着同时只有 6 个请求在网络上传输,不会出现瞬间连接数暴涨的问题。

4.3 并发参数的边界与调优经验

把 max_workers 从 1 调到 6,下载速度提升并不是线性的。受限于服务器的带宽分配策略,前 4 个并发时收益最明显,再往上提升幅度就很小了。我一般观察两个指标:单位时间完成文件数,以及失败重试次数。如果失败率超过 5%,说明并发数过高或者服务器在限流,先降并发再看。

还有一点:网络带宽本身也是瓶颈。如果你的本地带宽只有 50Mbps,跑 6 个并发和跑 3 个并发最终下载总时长差别不大,反而多占了本地 CPU 和磁盘 IO。逻辑上应该先测一下单文件下载能达到的峰值速度,再决定并发数。测速的办法很简单,下载一个小文件记录耗时就行。

5. 避坑指南:Human3.6M 下载与预处理中的五个常见问题

5.1 下载到一半断掉,文件大小对不上

现象:文件下载完成后,本地文件大小和服务器返回的 Content-Length 差了几 MB,但下载器没有报错。

原因:部分服务器在返回重定向后的真实资源时,Content-Length 只在最后一次响应头中携带,而重定向过程中的响应头被忽略。本地文件写完后没有做最终大小校验。

解决:下载完成后强制比对本地文件大小与最后一次响应头里的 Content-Length。比对不上就删掉重下,不要抱有侥幸心理。我在脚本里加了这段校验后,再没出现过文件损坏但无感知的情况。

5.2 压缩包解压报 CRC 校验失败

现象:好不容易下完一个压缩包,解压到一半报 CRC 错误,整个包作废。

原因:这个数据集的大文件走 HTTP 传输时,如果中途网络抖动,TCP 重传机制会消耗很长时间。下载器自身没有对分块数据进行完整性校验,只有最后比大小,而有些文件大小一致但内容已经损坏。

解决:下载过程中对每个 chunk 计算 MD5 校验,但这会拖慢速度。折中方案是下完立刻用脚本解压,解压失败就自动重下。如果是大包损坏,直接用支持分块校验的下载方式拆成多个小段分别下,哪个段坏了就重下哪段。

5.3 训练集和测试集的动作切分搞反

现象:训练时模型精度一直上不去,后来发现训练数据里混进了测试受试者的样本。

原因:官方推荐的分法是按受试者划分,不是按文件编号顺序划分。很多人用 os.listdir 拿到文件列表后按字符串排序,S10、S11 这类编号排在了 S1 前面,直接切出错误的数据子集。

解决:写一个映射表,显式指定哪些受试者进训练、哪些进测试,不要让程序自动推断。映射表写死以后,换数据集版本也不会因为文件命名规则变化而出问题。

5.4 解压后文件名带空格或特殊字符导致路径报错

现象:代码里用 glob.glob 或者 os.listdir 遍历文件,某些文件路径解析失败,报 FileNotFoundError。

原因:这个数据集里部分子目录名带空格,还有一些包含 Unicode 字符。在 Windows 上如果路径拼接时用了硬编码分隔符,或者在 Linux 上文件名编码不一致,就会踩坑。

解决:统一用 pathlib.Path 处理路径,不要直接拼字符串。Path 对象对特殊字符的兼容性要好得多,而且跨平台行为一致。

5.5 老版本的解析代码在 Python 3 下直接报错

现象:网上找到的解析示例代码大量使用 Python 2 语法和较老的 scipy 接口,直接复制运行会报很多错。

原因:这批代码写的时间比较早,python2 和 python3 在字符串编码、dict 遍历、range 返回值这些细节上差异很大。老代码里的 from collections import Iterable 在 Python 3.10 之后也已经被移除。

解决:用 Python 3.8 以上的环境跑,遇到报错就手动改。老代码通常改动量不大,主要是把 iteritems 改成 items、把 xrange 改成 range、把 np.int 改成 np.int64 这些机械性替换。

6. 进阶:原始 Mat 格式转成训练友好的 npz

6.1 写一个干净的转换脚本

下载完数据只是第一步,真正让数据可用还得把 Mat 转成模型能直接读的格式。下面是我用的转换脚本,把指定受试者的某个动作转换成一个 npz 文件,里面包含 3D 坐标和对应的帧号索引:

import numpy as np from scipy.io import loadmat def mat_to_npz(mat_path, npz_path, pose_key='pose3d'): """ 把 Human3.6M 的 Mat 文件转成 npz。 pose_key: 3D 姿态在 Mat 里的字段名,不同版本数据集可能不同 """ mat_data = loadmat(mat_path) # 字段名可能带 '_' 前缀,按实际数据调整 if pose_key not in mat_data: actual_key = [k for k in mat_data.keys() if pose_key in k][0] pose_key = actual_key pose = mat_data[pose_key] # 常见 shape 是 (帧数, 关节点数, 3),也可能带通道维 if pose.ndim == 4: pose = pose[:, 0, :, :] # 转 float16 可以省一半存储空间,但精度略降,按需选择 pose = pose.astype(np.float32) np.savez_compressed(npz_path, pose=pose) print(f'转换完成,shape: {pose.shape}, 已保存: {npz_path}')

这段代码的关键在于字段名的自适应匹配。不同版本的 Human3.6M 数据,Mat 内部的字段名有时带下划线前缀,有时是全名。直接用固定字段名去取,第一次跑就能撞上 KeyError,所以加了一段自动匹配逻辑。

shape 处理也是一个隐蔽的坑。部分动作的 Mat 数据保存时带了额外的维度,直接拿原始数组去训练,会在 batch 维度上多出一层。代码里统一降到三维,后续训练逻辑就能保持一致。

6.2 转换后的验证方法

转换完以后别急着喂模型,先做两个快速验证。第一,打印 pose 的数值范围,3D 坐标通常以米为单位,数值范围应该在 -2 到 2 之间。如果出现上百的数,多半是单位不对或者字段取错了。第二,随机选一帧,算一下各关节点的相对位置是否符合人体结构的基本约束——比如相邻关节点的距离应该在一定范围内,太离谱说明数据索引对应错了。

从那以后我每次拿到新版本的数据集,都会强制走一遍同样的流程:先下载两个小文件验证网络通断,再跑单文件断点续传测试,最后全量并发下载。这个过程能筛掉八成以上的玄学问题。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 3:56:32

堆栈(Stack)核心原理解析与高频面试题实战指南

1. 堆栈是什么:从日常场景到数据结构核心程序员的面试清单里,堆栈永远是那个“看似简单、实则年年必考”的存在。不管是刚准备秋招的在校生,还是工作三年的开发,几乎都会在堆栈相关问题上栽过跟头。我在带团队面试时,给…

作者头像 李华
网站建设 2026/10/16 3:56:31

文件时间属性修改全攻略:批量修正创建/修改/访问时间

整理素材库的时候,我差点被文件时间属性整崩溃。从旧硬盘拷贝出来的几千个文件,时间清一色变成了拷贝当天,原本按拍摄日期排列的素材序列全乱了。后来花了一整天研究文件时间属性,才把整个素材库的时间戳批量修正回来。这段经历让…

作者头像 李华
网站建设 2026/10/16 3:54:51

JDK 1.8免安装版实战:解压即用、环境变量配置与多版本共存

简介:JDK 1.8 免安装版资源包面向 Java 开发者,省去安装向导和环境变量配置,解压即可投入编写、编译与调试,适合快速搭建开发环境或在受限系统中使用。包体共 1475 个文件,约 173.74MB,涵盖 722 个 jar 运行…

作者头像 李华
网站建设 2026/10/16 3:54:40

Pulsar 2.9.1二进制包部署指南:从单机到三节点集群

简介:Apache Pulsar 2.9.1 二进制发行包,专为分布式消息队列与流处理场景设计,面向需要高吞吐、低延迟、可扩展消息中间件的开发者、架构师及运维团队。压缩包为 gz 格式,体积约 321.53MB,内含服务端启动程序、客户端 …

作者头像 李华
网站建设 2026/10/16 3:54:40

Spark DataFrame列重命名的三种方式与执行原理深度解析

1. 项目概述:为什么给DataFrame列重命名这件事值得专门写一篇长文?在 Spark SQL 实际开发中,我见过太多人把withColumnRenamed当成万能解药——结果跑着跑着报错AnalysisException: cannot resolve xxx given input columns,或者发…

作者头像 李华
网站建设 2026/10/16 3:54:10

AVPlayer 构建音乐播放器实战:从选型到后台播放

做音乐播放器这几年,AVPlayer 几乎成了我每次技术选型时绕不开的选项。它不像 AVAudioPlayer 那样一条 API 就能出声,但正因为它的能力边界足够宽,才让“歌单、进度条、缓冲、切歌、锁屏控制”这一整套需求都有了统一答案。这篇文章不是 API …

作者头像 李华