【问题标题】:Python: How to read huge text file into memoryPython:如何将巨大的文本文件读入内存
【发布时间】:2010-12-26 04:24:05
【问题描述】:

我在具有 1GB RAM 的 Mac Mini 上使用 Python 2.6。我想读一个巨大的文本文件

$ ls -l links.csv; file links.csv; tail links.csv 
-rw-r--r--  1 user  user  469904280 30 Nov 22:42 links.csv
links.csv: ASCII text, with CRLF line terminators
4757187,59883
4757187,99822
4757187,66546
4757187,638452
4757187,4627959
4757187,312826
4757187,6143
4757187,6141
4757187,3081726
4757187,58197

所以文件中的每一行都包含一个由两个逗号分隔的整数值组成的元组。 我想读入整个文件并根据第二列对其进行排序。我知道,我可以在不将整个文件读入内存的情况下进行排序。但我认为对于 500MB 的文件,我应该仍然能够在内存中执行它,因为我有 1GB 可用。

但是,当我尝试读取文件时,Python 分配的内存似乎比磁盘上文件所需的内存要多得多。因此,即使有 1GB 的 RAM,我也无法将 500MB 的文件读入内存。 我用于读取文件并打印有关内存消耗的一些信息的 Python 代码是:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys

infile=open("links.csv", "r")

edges=[]
count=0
#count the total number of lines in the file
for line in infile:
 count=count+1

total=count
print "Total number of lines: ",total

infile.seek(0)
count=0
for line in infile:
 edge=tuple(map(int,line.strip().split(",")))
 edges.append(edge)
 count=count+1
 # for every million lines print memory consumption
 if count%1000000==0:
  print "Position: ", edge
  print "Read ",float(count)/float(total)*100,"%."
  mem=sys.getsizeof(edges)
  for edge in edges:
   mem=mem+sys.getsizeof(edge)
   for node in edge:
    mem=mem+sys.getsizeof(node) 

  print "Memory (Bytes): ", mem 

我得到的输出是:

Total number of lines:  30609720
Position:  (9745, 2994)
Read  3.26693612356 %.
Memory (Bytes):  64348736
Position:  (38857, 103574)
Read  6.53387224712 %.
Memory (Bytes):  128816320
Position:  (83609, 63498)
Read  9.80080837067 %.
Memory (Bytes):  192553000
Position:  (139692, 1078610)
Read  13.0677444942 %.
Memory (Bytes):  257873392
Position:  (205067, 153705)
Read  16.3346806178 %.
Memory (Bytes):  320107588
Position:  (283371, 253064)
Read  19.6016167413 %.
Memory (Bytes):  385448716
Position:  (354601, 377328)
Read  22.8685528649 %.
Memory (Bytes):  448629828
Position:  (441109, 3024112)
Read  26.1354889885 %.
Memory (Bytes):  512208580

在仅读取 500MB 文件的 25% 之后,Python 已经消耗了 500MB。因此,将文件内容存储为整数元组列表似乎不是很有效的内存。 有没有更好的方法可以将 500MB 的文件读入 1GB 的内存?

【问题讨论】:

  • 我猜使用解释器,比如 Python,你无法真正知道内存在哪里。但是,列表 [通常 - 我不知道确切的 python 实现)比数组需要更多的内存,例如用于 prev/next 指针。您可能需要使用 C/C++ 才能准确了解您使用了多少内存。
  • 您将内存估计基于原始数据,然后创建元组和整数。如您所见,与短字符串相比,Python 的实例开销在这里很明显。您甚至可以将这些数据排序为纯字符串,您尝试过吗?
  • 我的内存估计加上整数、元组和列表的内存消耗。没关系,它与我使用 top 看到的大致相同(减去 Python 解释器消耗的内存)。但我没有尝试将数据排序为纯字符串。我该怎么做?
  • 按字母顺序对字符串进行排序会产生错误的结果。您是否尝试过 jsut 获得 4GB RAM?它可能会起作用,如果您经常处理这种大小的数据,它很快就会收回成本。
  • 在内存中排序有什么意义?在不将整个文件加载到内存的情况下进行排序,你就完成了!当文件增长时,或者当你需要你的程序在更小的设备(电话,任何人?)上运行时,它就会起作用。如果您的目标只是试图了解 Python 本身的内存使用情况,那么请编写另一个更明确的问题!

标签: python memory sorting large-files


【解决方案1】:

我使用external merge sort为此用例创建了一个模块: https://bitbucket.org/richardpenman/csvsort

>>> from csvsort import csvsort
>>> csvsort('links.csv', columns=[1], has_header=False)

【讨论】:

    【解决方案2】:

    有一个用于对大于 RAM on this page 的文件进行排序的方法,但您必须根据涉及 CSV 格式数据的情况对其进行调整。那里还有其他资源的链接。

    编辑:的确,磁盘上的文件不是“大于 RAM”,但内存中的表示很容易变得比 可用 RAM 大得多。一方面,您自己的程序无法获得全部 1GB(操作系统开销等)。另一方面,即使您以最紧凑的纯 Python 形式存储它(两个整数列表,假设是 32 位机器等),对于这 30M 对整数,您将使用 934MB。

    使用 numpy 也可以完成这项工作,仅使用大约 250MB。以这种方式加载并不是特别快,因为您必须计算行数并预先分配数组,但考虑到它在内存中,它可能是最快的实际排序:

    import time
    import numpy as np
    import csv
    
    start = time.time()
    def elapsed():
        return time.time() - start
    
    # count data rows, to preallocate array
    f = open('links.csv', 'rb')
    def count(f):
        while 1:
            block = f.read(65536)
            if not block:
                 break
            yield block.count(',')
    
    linecount = sum(count(f))
    print '\n%.3fs: file has %s rows' % (elapsed(), linecount)
    
    # pre-allocate array and load data into array
    m = np.zeros(linecount, dtype=[('a', np.uint32), ('b', np.uint32)])
    f.seek(0)
    f = csv.reader(open('links.csv', 'rb'))
    for i, row in enumerate(f):
        m[i] = int(row[0]), int(row[1])
    
    print '%.3fs: loaded' % elapsed()
    # sort in-place
    m.sort(order='b')
    
    print '%.3fs: sorted' % elapsed()
    

    在我的机器上输出一个与您显示的类似的示例文件:

    6.139s: file has 33253213 lines
    238.130s: read into memory
    517.669s: sorted
    

    numpy 中的默认值为Quicksort。 ndarray.sort() 例程(就地排序)也可以采用关键字参数kind="mergesort"kind="heapsort",但似乎这些都不能对Record Array 进行排序,顺便说一下,我将其用作唯一的我可以看到将列排序 together 的方式,而不是默认对它们进行独立排序(完全弄乱你的数据)。

    【讨论】:

    • 但我的问题是对一个小于内存中可用 RAM 的文件进行排序。
    • @asmaier,查看编辑后的答案,说明内存使用情况,以及使用可能适合您的 numpy 的解决方案。
    • 您的解决方案有两个问题:为什么需要预先分配数组?不能简单地使用 numpy.fromfile() 来生成数组吗?
    • 如果您不预先分配,无论您使用什么方法加载数据都必须以增量方式“增长”数组。这总是比在一个步骤中分配你需要的所有东西的内存效率低得多,因为你永远不需要在加载时从一个数组复制到另一个更大的数组。至于 fromfile(),如果你能找到它的工作方式,请继续。它似乎需要一个“sep”参数,所以使用sep=",",你只会加载两个整数,而使用sep="\n",你只会得到一个......而且它仍然不知道要分配多少空间提前。
    • 所以我终于尝试了您的解决方案,它工作正常(输出:21.736s:文件有 30609720 行 | 213.738s:已加载 | 507.188s:已排序)非常感谢!但我还有几个问题:你为什么用f.seek(0) 而不是f.close()?为什么要使用生成器进行行数计算?我删除了生成器函数,改为使用 for line in f: linecount=linecount+1,它只比您的解决方案慢一点 (1%)。
    【解决方案3】:

    你可能想看看 mmap:

    http://docs.python.org/library/mmap.html

    它将让您将文件视为一个大数组/字符串,并让操作系统处理数据进出内存的混洗处理以使其适合。

    因此,您可以一次读取 csv 文件,然后将结果写入 mmap 文件(以合适的二进制格式),然后处理 mmap 文件。由于 mmap 文件只是临时文件,您当然可以为此创建一个 tmp 文件。

    这里有一些代码演示了使用带有临时文件的 mmap 来读取 csv 数据并将其存储为整数对:

    
    import sys
    import mmap
    import array
    from tempfile import TemporaryFile
    
    def write_int(buffer, i):
        # convert i to 4 bytes and write into buffer
        buffer.write(array.array('i', [i]).tostring())
    
    def read_int(buffer, pos):
        # get the 4 bytes at pos and convert to integer
        offset = 4*pos
        return array.array('i', buffer[offset:offset+4])[0]
    
    def get_edge(edges, lineno):
        pos = lineno*2
        i, j = read_int(edges, pos), read_int(edges, pos+1)
        return i, j
    
    infile=open("links.csv", "r")
    
    count=0
    #count the total number of lines in the file
    for line in infile:
        count=count+1
    
    total=count
    print "Total number of lines: ",total
    
    infile.seek(0)
    
    # make mmap'd file that's long enough to contain all data
    # assuming two integers (4 bytes) per line
    tmp = TemporaryFile()
    file_len = 2*4*count
    # increase tmp file size
    tmp.seek(file_len-1)
    tmp.write(' ')
    tmp.seek(0)
    edges = mmap.mmap(tmp.fileno(), file_len)
    
    for line in infile:
        i, j=tuple(map(int,line.strip().split(",")))
        write_int(edges, i)
        write_int(edges, j)
    
    # now confirm we can read the ints back out ok
    for i in xrange(count):
        print get_edge(edges, i)
    

    虽然有点粗糙。真的,你可能想用一个漂亮的类来包装所有这些,这样你的边缘就可以以一种使它们表现得像一个列表的方式被访问(带有索引、len 等)。希望它能给你一个起点。

    【讨论】:

    • (1) 它在哪里进行排序? (2) 考虑使用 struct.pack 和 struct.unpack 代替 array.array 方法——更少的开销(在一个函数调用中执行 2 个值,首先) (3) 不需要 tuple() (4) 应该剥离之后的两个部分
    【解决方案4】:

    所有 python 对象在它们实际存储的数据之上都有内存开销。根据我的 32 位 Ubuntu 系统上的 getsizeof ,一个元组的开销为 32 个字节,一个 int 需要 12 个字节,因此文件中的每一行需要一个 56 个字节 + 列表中的一个 4 字节指针 - 我想它会很多更多的 64 位系统。这与您提供的数据一致,意味着您的 3000 万行将占用 1.8 GB。

    我建议不要使用 python,而是使用 unix 排序实用程序。我不是 Mac 头,但我认为 OS X 排序选项与 linux 版本相同,所以应该可以:

    sort -n -t, -k2 links.csv
    

    -n 表示按数字排序

    -t,表示使用逗号作为字段分隔符

    -k2 表示在第二个字段上排序

    这将对文件进行排序并将结果写入标准输出。您可以将其重定向到另一个文件或通过管道将其传递给您的 python 程序以进行进一步处理。

    编辑: 如果您不想在运行 python 脚本之前对文件进行排序,您可以使用 subprocess 模块创建一个到 shell 排序实用程序的管道,然后从管道的输出中读取排序结果。

    【讨论】:

    • 为了 Windows 用户的利益:您可以从gnuwin32.sourceforge.net 的 GnuWin32 项目中获得兼容的 sort.exe
    • 仅用于排序您的解决方案绝对是最快的。在我的情况下,sort 需要 450 秒来对我的数据进行排序并将其输出到文件中,而 python 解决方案需要 1750 秒(并且大部分时间只用于编写文件)。但是sort 使用了 440MB 的 RAM,而 Peter Hansen 提出的 python 解决方案只需要 240MB。而且这两种方案都只用到了我双核机的一个核心,所以还有很大的提升空间……
    【解决方案5】:

    将输入行存储在内存中的最便宜的方法是使用 array.array('i') 元素——假设每个数字都适合有符号的 32 位整数。内存消耗为 8N 字节,其中 N 是行数。

    这里是如何进行排序并按排序顺序写入输出文件:

    from array import array
    import csv
    a = array('i')
    b = array('i')
    for anum, bnum in csv.reader(open('input.csv', 'rb')):
        a.append(int(anum))
        b.append(int(bnum))
    wtr = csv.writer(open('output.csv', 'wb'))
    for i in sorted(xrange(len(a)), key=lambda x: b[x]):
        wtr.writerow([a[i], b[i]])
    

    不幸的是sorted()返回一个列表,而不是一个迭代器,这个列表会相当大:4N字节的指针和12N字节的int对象,即16N字节的sorted()输出。注意:这是基于 32 位机器上的 CPython 2.X;对于 3.X 和 64 位机器来说,情况会变得更糟。总共有 24N 个字节。你有 3100 万行,所以你需要 31 * 24 = 744 MB ...看起来应该可以;请注意,此计算不允许排序分配的任何内存,但您有合理的安全余量。

    顺便说一句:按照你的工资标准,额外的 GB 或 3 内存的成本是多少?

    【讨论】:

      【解决方案6】:

      由于这些都只是数字,将它们加载到 Nx2 数组中会减少一些开销。将 NumPy 用于多维数组。或者,您可以使用两个普通的 python arrays 来表示每一列。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-11-21
        • 1970-01-01
        • 2013-09-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多