NumPy实时过滤读取文件行

9

我有一个包含大量数字的CSV文件,需要加载其中的一部分数组。从概念上讲,我想调用np.genfromtxt(),然后对生成的数组进行行切片,但是:

  1. 该文件太大,可能无法放入RAM中。
  2. 相关行数可能很少,因此没有必要解析每一行。

MATLAB有函数textscan(),可以使用文件描述符并只读取文件的一部分。NumPy中是否有类似的东西?

目前,我定义了以下函数,仅读取满足给定条件的行:

def genfromtxt_cond(fname, cond=(lambda str: True)):
  res = []
  with open(fname) as file:
    for line in file:
      if cond(line):
        res.append([float(s) for s in line.split()])

  return np.array(res, dtype=np.float64)

这种解决方案存在以下几个问题:
  • 不够通用:只支持float类型,而 genfromtxt 可以检测每一列的类型,因此类型可能会发生变化;同时还需要考虑缺失值、转换器、跳过等其他问题;
  • 效率不高:当条件较为复杂时,每行可能需要解析两次,同时所使用的数据结构和读取缓存也可能不够优化;
  • 需要编写代码。
是否有标准函数实现过滤或者与 MATLAB 的 textscan 相对应的功能?

1
为什么不直接使用“yield line”或“yield line.split()”,而是要构建“res”,这样它就不会受到“line”中数据的影响。 - sotapme
@sotapme 这有什么好处吗?你是说生成器比append更快吗?无论如何,我最终需要筛选后的np.array()。 - Roman Shapovalov
你说你的只支持“float”,因此既然您已经泛化了“cond”,我认为产生该行将允许您使用相同的“genfromtxt_cond”不考虑行数据。 我在考虑代码重用而不是性能。 - sotapme
3个回答

16
我能想到两种方法可以提供你所需的一些功能:
  1. 分块读取/每次读取n行等方式读取文件:
    你可以将一个生成器函数传递给numpy.genfromtxtnumpy.loadtxt。这样,你可以在保留这两个函数的所有方便的解析功能的同时,以内存高效的方式从文本文件中加载大型数据集。

  2. 只从符合正则表达式匹配条件的行中读取数据:
    你可以使用numpy.fromregex来使用正则表达式精确地定义应该加载哪些输入文件行中的标记。不符合模式的行将被忽略。

为了说明这两种方法,我将使用我的研究背景下的一个例子。
我经常需要加载具有以下结构的文件:

6
 generated by VMD
  CM         5.420501        3.880814        6.988216
  HM1        5.645992        2.839786        7.044024
  HM2        5.707437        4.336298        7.926170
  HM3        4.279596        4.059821        7.029471
  OD1        3.587806        6.069084        8.018103
  OD2        4.504519        4.977242        9.709150
6
 generated by VMD
  CM         5.421396        3.878586        6.989128
  HM1        5.639769        2.841884        7.045364
  HM2        5.707584        4.343513        7.928119
  HM3        4.277448        4.057222        7.022429
  OD1        3.588119        6.069086        8.017814

这些文件可能非常巨大(以GB计),我只对数值数据感兴趣。所有数据块的大小都相同--在此示例中为6--并且它们始终由两行分隔。因此,块的stride8
使用第一种方法:
首先,我将定义一个生成器,该生成器会过滤掉不需要的行:
def filter_lines(f, stride):
    for i, line in enumerate(f):
        if i%stride and (i-1)%stride:
            yield line

然后我打开文件,创建一个名为filter_lines的生成器(这里需要知道stride),并将该生成器传递给genfromtxt 函数:

with open(fname) as f:
    data = np.genfromtxt(filter_lines(f, 8),
                         dtype='f',
                         usecols=(1, 2, 3))

这个操作非常简单。请注意,我可以使用usecols来删除数据的第一列。同样,您还可以使用genfromtxt的其他功能——检测类型,列与列之间的变化类型,缺失值,转换器等。

在此示例中,data.shape(204000, 3),而原始文件由272000行组成。

在这里,使用generator筛选同质跨度的行,但人们也可以想象它基于(简单的)条件过滤出不均匀的行块。

使用第二种方法:

这是我要使用的regexp

regexp = r'\s+\w+' + r'\s+([-.0-9]+)' * 3 + r'\s*\n'

组 -- 即在 () 中 -- 定义了从给定行中提取的令牌。 接下来,fromregex 执行此任务并忽略不符合模式的行:

data = np.fromregex(fname, regexp, dtype='f')

结果与第一种方法完全一样。


这太棒了,谢谢。所以如果我理解正确的话,使用生成器而不是直接文件路径的主要好处是我们节省了存储数据的空间。我假设读取数据是不可避免的,因为这是我们唯一知道是否要过滤它的方法。这样做会显著加快文件加载速度吗?还是会更慢,因为我们需要先过滤,然后将可迭代对象传递给genfromtxt? - Lucas
我知道这个答案是2013年的,所以我不确定事情是否有所改变。目前我在文档中找不到关于生成器的任何信息。最好的猜测是转换器,但它们似乎与这个答案相比提供了有限的功能。这仍然有效吗?谢谢。 - divmermarlav

1
如果您传递一个类型列表(格式条件),使用try块并使用yield将genfromtxt用作生成器,我们应该能够复制textscan()。
def genfromtext(fname, formatTypes):
    with open(fname, 'r') as file:
        for line in file:
            try:
                line = line.split(',')  # Do you care about line anymore?
                r = []
                for type, cell in zip(formatTypes, line):
                    r.append(type(cell))
            except:
                pass  # Fail silently on this line since we hit an error
            yield r

编辑:我忘了添加异常处理块。现在它可以正常运行,您可以像这样使用genfromtext作为生成器(使用我手头随机的CSV日志):

>>> a = genfromtext('log.txt', [str, str, str, int])
>>> a.next()
['10.10.9.45', ' 2013/01/17 16:29:26', '00:00:36', 0]
>>> a.next()
['10.10.9.45', ' 2013/01/17 16:22:20', '00:08:14', 0]
>>> a.next()
['10.10.9.45', ' 2013/01/17 16:31:05', '00:00:11', 3]

我应该指出,我正在使用zip将逗号拆分的行和formatSpec捆绑在一起,这将使这两个列表成为元组(当其中一个列表用尽时停止),以便我们可以一起迭代它们,避免循环依赖于len(line)或其他类似的东西。

你的回答很相关,但我真正想要的是使用可用的库函数来解决我的问题,就像genfromtxt一样高效和强大。自定义实现——无论是过滤版本(在Q中)还是textscan版本(在你的A中)——都会遇到我上面列出的几个问题。 - Roman Shapovalov
除了作为标准函数之外,textscan提供了哪些优化?相对简单的是添加一个块以跳过前导行并仅从文件中产生N行,转换会尽可能快地退出一行(甚至可以删除转换并返回原始字符串以节省append),整个过程非常懒惰,因此内存使用量很小。您可以潜在地使用linecache来提高访问速度,但代价是内存使用,但我无法对其使用进行评论。 - m.brindley
我接受你的答案,因为你确实回答了这个问题。虽然我想知道是否有一个库实现,其中一个不需要实现np.genfromtxt具有的所有功能。很可能没有这样的库,所以你的答案是最合适的。 - Roman Shapovalov

0

尝试向OP演示注释。

def fread(name, cond):
    with open(name) as file:
        for line in file:
            if cond(line):
                yield line.split()

def a_genfromtxt_cond(fname, cond=(lambda str: True)):
    """Seems to work without need to convert to float."""
    return np.array(list(fread(fname, cond)), dtype=np.float64)

def b_genfromtxt_cond(fname, cond=(lambda str: True)):
    r = [[int(float(i)) for i in l] for l in fread(fname, cond)]
    return np.array(r, dtype=np.integer)


a = a_genfromtxt_cond("tar.data")
print a
aa = b_genfromtxt_cond("tar.data")
print aa

输出

[[ 1.   2.3  4.5]
 [ 4.7  9.2  6.7]
 [ 4.7  1.8  4.3]]
[[1 2 4]
 [4 9 6]
 [4 1 4]]

如果你担心内存使用(即没有中间列表),也可以查看numpy.fromiter。它适用于1D数组,但是你可以很容易地使其适用于2D。不是为了推销我的答案,但请参见这里的最后一个示例:https://dev59.com/nGox5IYBdhLWcg3wzXel#8964779 - Joe Kington

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接