跳到主要内容
AZ Tools

NumPy .npy / .npz 检查器

.npy 就是一个很小的文件头加上一整块原始数字,而这个工具只需要那个文件头:6 字节魔数、版本号、头部长度,以及一段带有 descr、fortran_order 和 shape 的 Python 字典字面量——不是 JSON。因为只读开头 1KB,2GB 的数组和 2KB 的文件开得一样快,这正是重点:在决定要不要加载之前先弄清楚里面是什么。descr 按 numpy 的方式解析。首字符是字节序,< 是小端,> 是大端,| 表示不适用;大端文件会被标出来,因为读错了并不像出错,只是数字变成了另一些数字。接着是种类和元素大小:b1 布尔、i1 到 i8 与 u1 到 u8 整数、f2/f4/f8 浮点、c8 与 c16 复数、保留中间 NUL 但去掉末尾 NUL 的 S<n> 字节串、以 UCS-4 存储因而三个字符占十二字节的 U<n>、把单位写在方括号里的 M8 与 m8、原始的 V<n>,以及 O。O 意味着这个数组是用 allow_pickle=True 写的,加载它就会执行被序列化的内容,所以会醒目地警告,而不是一笔带过。结构化 dtype 以 (名称, 格式) 或 (名称, 格式, shape) 元组的列表出现,还可以嵌套;偏移量并不保存在任何地方,只是元素大小的累加和,这正是对齐的 dtype 会在自己的 descr 里带上无名填充字段的原因,这些字段连同它们占据的区间也一并显示。之后的数字都可以自己验算:由 shape 得到元素数,元素数乘元素大小得到应有字节数,再加上文件头之后实际存在的字节数。于是被失败的拷贝或写满的磁盘截断的文件,会表现为缺了多少字节,而不是一个看上去毫无问题的文件头。对于简单数值 dtype,页面会按元素在文件中的排列顺序解码开头的若干个值,因此 Fortran 顺序的数组是一列一列读出来的。.npz 是装着 .npy 成员的 zip,这里会列出每个成员的 dtype、shape、两种大小以及是否压缩。它不做的事同样明确:不解开 pickle,不告诉你这些数字是什么意思,也不检验数据是否有意义;它只报告文件自己的说法,并指出这些说法与文件大小矛盾的地方。

使用方法

  1. 把 .npy 或 .npz 拖进方框。只读文件头,所以文件多大都无所谓。
  2. 先看 dtype、shape 和元素数,这就是“里面装了什么”的答案,只花一次读取。
  3. 把“应有数据”和“实有数据”对照着看:少了说明文件被截断,多了说明数组后面被追加了东西。
  4. 如果是结构化 dtype,展开字段表:每个字段的格式、偏移和大小都在,包括对齐 dtype 插入的无名填充。
  5. 如果是 .npz,点击成员名查看那个数组;列表里同时给出它在包内和解压后的大小。

常见问题

.npy 的文件头里究竟有什么?
6 字节魔数(\x93NUMPY)、2 字节版本、头部长度,然后是头部本身:一个恰好三个键的 Python 字典字面量,descr、fortran_order 和 shape。它不是 JSON,JSON.parse 读不了:字符串用单引号,布尔值写作 True 和 False,shape 是元组,写成 (3,) 而不是 (3),并且右花括号前还有一个多余的逗号。字面量之后用空格补齐,使数组数据从 64 字节边界开始,numpy 正是靠这个填充才能用对齐读取把文件内存映射进来。本工具原样显示 descr,因此你可以直接和 numpy 的输出对照,不必怀疑中间是否被规范化过。
为什么我的字符串数组是字符数的四倍大?
因为 numpy 的 U dtype 用 UCS-4 存储:无论什么字符都固定占四字节,所以元素大小是声明长度的四倍。一个 U32 字段即使每个值都是 "ok",每个元素也要 128 字节。字节串 S 每字符一字节,但另有一个陷阱:读取时会去掉末尾的 NUL,因此真的以零字节结尾的值无法原样往返,而中间的 NUL 会保留。数据集大得离谱时,原因通常就是 U dtype,常见的做法是选定编码后用 S 保存,或者干脆把字符串单独放一个数组。
dtype 显示 object,这是什么意思,为什么要警告?
object 数组根本不存值,存的是指针,所以保存时 numpy 用 pickle 把整个数组序列化,加载时再反序列化。反序列化不是解析:它可以构造任意对象、调用任意代码,这也是 numpy.load 在没有 allow_pickle=True 时拒绝 object 数组的原因。descr 为 |O 的 .npy 既是数据也是程序,它的安全性等同于写它的人的安全性。本页面从不解开 pickle,只报告 dtype 和 pickle 的大小就停下。如果里面装的只是普通字符串或长短不齐的列表,通常值得改写成定宽 dtype,或者拆成 .npz 里的多个数组。
fortran_order 到底改变了什么?
只改变同样这些数字在文件里的排列顺序。为 False 时最后一个轴变化最快(C,行优先);为 True 时第一个轴变化最快(列优先),np.asfortranarray 的结果,或者来自 Fortran、MATLAB 的数据就是这样。两种情况下 shape 和 dtype 完全相同,所以忽略这个标志的读取器不会报错——它只会悄悄把你的数组转置,这比抛异常糟糕得多。这里的预览跟随文件而不是逻辑布局,因此 Fortran 顺序的 2×3 数组会按第 0 列、第 1 列、第 2 列读出。
为什么会有三个格式版本?
1.0 用两字节存头部长度,头部因此被限制在 65535 字节。2.0 在这不够用的少数情况下改用四字节:几千个字段的结构化 dtype 就会这样,头部能达到几百 KB。3.0 与 2.0 相同,只是把头部声明为 UTF-8 而不是 Latin-1,只有当字段名里出现 Latin-1 容纳不下的字符时 numpy 才会写它。numpy 总是选择放得下的最低版本,所以文件是 2.0 或 3.0 本身就说明了一些事。还有一点值得知道:numpy 自己的加载器在不调高 max_header_size 的情况下会拒绝超过 10000 字节的头部,于是一个合法的 2.0 文件可能在 Python 里打不开,在这里却能正常打开。
文件会被上传吗?很大的数组也能打开吗?
不上传,能打开。文件通过浏览器的 File API 读取,而且只读切片:头部约 1KB,值预览再多几 KB。2GB 的数组永远不会进内存,而这正是它存在的场景——别人给了你一个文件,你只想知道 dtype 和 shape,真去加载却要花上几分钟和大半内存。对 .npz 而言,只读文件末尾的 zip 目录和每个成员各自的头部,所以列出一个压缩包是两次读取加上每个成员一次。由于什么都不发送,页面加载完之后断网也照样能用。

相关工具