Python: 如何将大型文本文件读入内存

30

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

$ 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++来准确地知道你使用了多少内存。 - Drakosha
你可以基于原始数据估计内存,然后创建元组和整数。与短字符串相比,Python的实例开销在这里是明显的,正如您所见。您甚至可以将这些数据作为纯字符串进行排序,您试过了吗? - u0b34a0f6ae
1
在内存中进行排序有什么意义呢?不要把整个文件加载到内存中,只需进行排序就可以完成!而且当文件变得更大时,或者当你需要程序在较小的设备上运行(比如手机)时,它也能正常工作。如果你的目标只是想了解Python内存使用情况本身,那就写另一个更明确的问题吧! - Davide
Python中的列表是使用数组实现的。 - Alexander
不要使用Python。使用Scala,加入Spark和HeyPresto,这很容易。 - samthebest
显示剩余4条评论
6个回答

22
此页面上有一种排序大于RAM的文件的方法,但您需要为涉及CSV格式数据的情况进行适应。那里还提供了其他资源的链接。
编辑:确实,磁盘上的文件不是“大于RAM”,但是内存中的表示可以很容易地变得比可用RAM大得多。首先,您自己的程序没有获得整个1GB(操作系统开销等)。另外,即使您以纯Python的最紧凑形式存储它(两个整数列表,假设32位机器等),您也将使用934MB来存储这30M对整数。
使用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的默认排序算法是快速排序。ndarray.sort()函数(原地排序)也可使用关键字参数kind="mergesort"kind="heapsort",但似乎这两种算法都无法在记录数组上进行排序,顺便说一下,我使用记录数组作为唯一的排序方法,以便将列同时排序,而不是像默认情况下一样将它们独立排序(从而完全搞乱你的数据)。

但我的问题是关于在内存中可用RAM比文件小的情况下进行排序。 - asmaier
@asmaier,请查看已编辑的答案,其中澄清了内存使用情况,并提供了可能适合您的使用numpy的解决方案。 - Peter Hansen
你的解决方案有两个问题:为什么需要预先分配数组?难道不能简单地使用numpy.fromfile()来生成数组吗? - asmaier
1
如果您不进行预分配,无论您使用什么方法来加载数据,都必须逐步“增加”数组。这始终比在单个步骤中分配所需的所有内容要少得多,因为您永远不需要从一个数组复制到另一个更大的数组,而是在加载时。至于fromfile(),如果您能想到一种方法让它工作,请继续。它似乎只需要一个“sep”参数,因此使用sep =“,”将仅加载两个整数,而使用sep =“\n”将仅加载一个...并且它仍然不知道预先分配多少空间。 - Peter Hansen
所以我最终尝试了你的解决方案,它运行良好(输出:21.736秒:文件有30609720行|213.738秒:已加载|507.188秒:已排序),非常感谢!但我仍有几个问题:为什么要使用f.seek(0)而不是f.close()?为什么要使用生成器进行行计数?我删除了生成器函数,改用for line in f: linecount=linecount+1,只比你的解决方案慢了1%。 - asmaier
如果我执行了f.close(),那么我只需要重新打开文件即可。f.seek(0)只是为了避免这种情况(不过无论哪种方式都没有问题)。我采用生成器的方法是因为我认为读取块并使用.count()会更快(过早优化!我做得不好...),但显然这并不是必需的。你的方法也完全可以。很高兴能够帮助。 - Peter Hansen

9
所有的Python对象在存储实际数据的基础上都有一定的内存开销。在我的32位Ubuntu系统上,使用getsizeof函数可以得知一个元组(tuple)的开销为32字节,一个整数(int)的开销为12字节,因此文件中的每一行需要56字节加上列表中的4字节指针——我认为在64位系统上会更多。这与你提供的数字相符,意味着你的3000万行将占用1.8GB的空间。
我建议你不要使用Python,而是使用Unix的sort工具。我不是Mac-head,但我认为OS X的sort选项与Linux版本相同,所以以下命令应该可行:
sort -n -t, -k2 links.csv

-n 表示按照数字排序
-t 表示使用逗号作为字段分隔符
-k2 表示按照第二个字段排序
这会对文件进行排序并将结果写入标准输出。您可以将其重定向到另一个文件或将其管道传输到Python程序以进行进一步处理。
编辑: 如果您不想在运行Python脚本之前对文件进行排序,可以使用subprocess模块创建到shell sort实用程序的管道,然后从管道输出中读取排序结果。

为了Windows用户的方便起见:您可以从GnuWin32项目的http://gnuwin32.sourceforge.net/获取一个兼容的sort.exe。 - John Machin
1
仅就排序而言,您的解决方案绝对是最快的。在我的情况下,sort 需要 450 秒来将我的数据排序并输出到文件中,而 Python 解决方案需要 1750 秒(大部分时间都花在写文件上)。但是 sort 使用了 440MB 的 RAM,而 Peter Hansen 提出的 Python 解决方案只需要 240MB。而且这两种解决方案都只使用了我双核机器的一个核心,因此还有很大的改进空间... - asmaier

4

将输入行存储在内存中最便宜的方法是将其作为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对象,即sorted()输出需要16N个字节。注意:这是基于32位机器上的CPython 2.X;对于3.X和64位机器,情况会更糟。总共需要24N个字节。你有3100万行数据,所以你需要31 * 24 = 744 MB ... 看起来应该可以工作;请注意,这个计算没有考虑排序分配的任何内存,但你还有一个合理的安全边际。
另外:如果以你的薪资水平计算,额外1GB或3GB内存的成本是多少小时?

4

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


2
你可能想看一下mmap:

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

它让您将文件视为一个大的数组/字符串,并让操作系统处理数据进出内存以使其适合。因此,您可以逐行读取csv文件,然后将结果写入到mmap'd文件(使用适当的二进制格式),然后对mmap'd文件进行操作。由于mmap'd文件仅是临时的,因此您当然可以为此目的创建一个临时文件。以下是一些示例代码,演示如何将csv数据作为整数对读入并存储到mmap'd文件中:

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)

“虽然有点粗糙。但是,你可能想用一个好的类来封装所有这些内容,这样可以通过索引、长度等方式访问边缘,使它们表现得像列表一样。希望这会给你提供一个起点。”

1
(1) 它进行排序的部分在哪里? (2) 考虑使用struct.pack和struct.unpack代替array.array方法——开销更小(首先,在一个函数调用中处理两个值)。 (3) 不需要使用tuple() (4) 应该在split之后同时去除两个部分。 - John Machin

0

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