从大目录中随机加载文件

Lazily load files at random from large directory

提问人:postnubilaphoebus 提问时间:11/6/2023 更新时间:11/14/2023 访问量:83

问:

我的目录中大约有一百万个文件,它们的数量可能会增加。 对于机器学习,我想从这些文件中随机采样,而无需替换。我怎样才能非常快速地做到这一点?os.listdir(path) 对我来说太慢了。

python 随机 延迟加载

评论

1赞 John Gordon 11/6/2023
要么使用其他一些存储机制(如数据库),要么将文件划分为子目录。
0赞 Frank Yellin 11/6/2023
这确实是一个操作系统问题,而不是语言问题。我不相信 Linux/MacOS 为您提供了除顺序之外的任何方法来读取目录的内容。
0赞 postnubilaphoebus 11/6/2023
@JohnGordon 对我来说,数据库听起来像是一个可行的解决方案。您能否为我指出正确的方向,说明我可以用来构建高效数据库的库/包,从而使我的任务成为可能?
1赞 John Gordon 11/6/2023
任何 python 数据库库都应该工作。只需选择一个满足您需求的产品即可。Sqlite3 内置于 python 中,它似乎有很好的文档,所以我会从那里开始。

答:

2赞 Keiji 11/6/2023 #1

我的目录中有大约一百万个文件......os.listdir(path) 对我来说太慢了。

这是您问题的核心,它通过一种我通常听说的技术来解决,称为存储文件,尽管对此进行网络搜索似乎不是特别有用。

存储桶通常由需要存储大量没有任何特定结构的文件的程序使用,例如,MediaWiki实例(运行维基百科的软件)中的所有媒体文件(例如图像)。这是维基百科上的 Stack Overflow 标志:

https://upload.wikimedia.org/wikipedia/commons/0/02/Stack_Overflow_logo.svg

在 URL 中看到了吗?这就是水桶。维基百科中的所有文件都将通过某种算法进行哈希处理 - 例如sha256,尽管它不一定是这个 - 并且将是该哈希的前两个十六进制数字。(斜杠前的斜杠只是 的第一个数字;在本例中,它用作第二级存储桶。0/0202002

如果MediaWiki只是将每个文件存储在一个庞大的目录中,那么访问该目录中的文件会非常慢,因为尽管操作系统文件夹可以容纳任意数量的文件,但它们的设计不会超过几千个左右。通过对文件的内容进行哈希处理,你会得到一个看起来像是该文件唯一的十六进制数字的随机字符串,如果你然后把所有以相同的前两个十六进制数字开头的文件(比如在一个名为 的文件夹中,你会得到 256 个文件夹(前两个十六进制数字的每个可能值一个), 至关重要的是,这 256 个文件夹中的每一个都包含大致相同数量的文件0202

当你试图查找特定的文件时,就像MediaWiki一样,如果你以这种方式存储它,你显然需要知道哈希值才能得到文件。但在您的情况下,您只想加载随机文件。因此,这也同样有效:

  • 对所有文件进行哈希处理并存储它们(可能具有其他级别,例如,您可能需要 这样的文件,以便您有 65,536 个存储桶)。您可以使用 hashlib 或命令行工具(如)来获取文件哈希。您不需要重命名文件,只要您根据文件哈希值的前几个十六进制数字将它们分组到目录中即可。12/34/filename.extsha256sum
  • 现在,每次您想要一个随机文件时,请选择一个随机存储桶(如果您使用其他级别,则可能选择随机子存储桶),然后在该存储桶中选择一个随机文件。

这样做比在包含一百万个文件的目录上使用然后随机选择要快得多。listdir


注意:我在这里仅以MediaWiki为例,因为我熟悉它的一些内部结构;许多软件产品都做类似的事情。

0赞 Severin Pappadeux 11/14/2023 #2

让我提供一些替代方法。基本上,您想要的不是在一次大规模扫描中扫描和统计目录文件,而只是读取目录条目及其示例子集。在 linux/UNIX 上,这相当于 readdir() syscall,它很小,在 direntry 之后返回 direntry,直到它耗尽(返回 NULL)。然后,您可以使用储层取样进行实际取样

在 Python os.scandir 中,我相信调用类似 readdir 的东西。

所以,一些(未经测试的)伪代码

import os
import math

folder = 'AAA'

sampled = []
nof_samples = 10

with os.scandir(folder) as it:
    for i, entry in enumerate(it):
        if i < nof_samples:
            sampled.append(entry.name)
            continue

        n = math.random.randrange(i)
        if n < nof_samples:
            sampled[n] = entry.name