28

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

したがって、ファイルの各行は、コンマで区切られた 2 つの整数値のタプルで構成されます。ファイル全体を読み込んで、2 番目の列に従って並べ替えたいと思います。ファイル全体をメモリに読み込まずに並べ替えを実行できることはわかっています。しかし、500MB のファイルの場合、1GB が利用可能であるため、メモリ内で実行できるはずだと考えました。

ただし、ファイルを読み込もうとすると、Python はディスク上のファイルが必要とするよりも多くのメモリを割り当てているようです。そのため、1 GB の RAM を使用しても、500 MB のファイルをメモリに読み込むことができません。ファイルを読み取り、メモリ消費に関する情報を出力するための私の 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 を消費します。そのため、ファイルの内容を int のタプルのリストとして格納することは、メモリ効率があまり良くないようです。500MB のファイルを 1GB のメモリに読み込むことができるようにするためのより良い方法はありますか?

4

6 に答える 6

22

RAM より大きいファイルを並べ替えるためのレシピがこのページにありますが、CSV 形式のデータを含むケースに合わせて調整する必要があります。追加のリソースへのリンクもあります。

編集:確かに、ディスク上のファイルは「RAM よりも大きい」わけではありませんが、メモリ内の表現は利用可能な RAMよりもはるかに大きくなる可能性があります。1 つには、独自のプログラムが 1GB 全体を取得するわけではありません (OS のオーバーヘッドなど)。別の理由として、これを純粋な Python 用の最もコンパクトな形式 (32 ビット マシンなどを想定した整数の 2 つのリスト) で格納したとしても、これらの 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"レコード配列でソートすることができないようです。列を個別に並べ替えるデフォルトとは対照的に、列をまとめます(データを完全に台無しにします)。

于 2009-12-13T14:38:05.727 に答える
8

すべての python オブジェクトには、実際に格納しているデータの上にメモリ オーバーヘッドがあります。私の 32 ビット Ubuntu システムの getsizeof によると、タプルには 32 バイトのオーバーヘッドがあり、int には 12 バイトかかるため、ファイルの各行は 56 バイト + リスト内の 4 バイトのポインターを取ります。 64ビットシステムの場合はさらに。これはあなたが与えた数字と一致しており、3000万行が1.8 GBかかることを意味します.

Python を使用する代わりに、UNIX の sort ユーティリティを使用することをお勧めします。私は Mac ヘッドではありませんが、OS X の並べ替えオプションは Linux バージョンと同じであると推測しているため、これは機能するはずです。

sort -n -t, -k2 links.csv

-n は数値でソートすることを意味します

-t は、フィールド区切り文字としてコンマを使用することを意味します

-k2 は、2 番目のフィールドでの並べ替えを意味します

これにより、ファイルがソートされ、結果が stdout に書き込まれます。別のファイルにリダイレクトするか、Python プログラムにパイプして、さらに処理を行うことができます。

編集: Python スクリプトを実行する前にファイルを並べ替えたくない場合は、subprocess モジュールを使用してシェルの並べ替えユーティリティへのパイプを作成し、パイプの出力から並べ替えられた結果を読み取ることができます。

于 2009-12-13T16:32:22.667 に答える
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 バイト、int オブジェクトの場合は 12N バイト、つまりsorted()出力の場合は 16N バイトです。注: これは 32 ビット マシン上の CPython 2.X に基づいています。3.X マシンと 64 ビット マシンのそれぞれで悪化します。全部で 24N バイトです。3,100 万行あるので、31 * 24 = 744 MB が必要です ... 動作するはずです。この計算では、並べ替えによって割り当てられたメモリは考慮されていませんが、妥当な安全マージンがあることに注意してください。

余談ですが、追加の GB または 3 メモリのコストは、あなたの給与レートでの時間数で表されますか?

于 2009-12-13T16:14:40.570 に答える
4

これらはすべて単なる数値であるため、それらを Nx2 配列にロードするとオーバーヘッドがいくらか除去されます。多次元配列には NumPy を使用します。または、2 つの通常の Python配列を使用して各列を表すこともできます。

于 2009-12-13T14:59:00.090 に答える
2

mmap を見たいと思うかもしれません:

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

ファイルを大きな配列/文字列のように扱うことができ、OSがデータをメモリに出し入れするシャッフルを処理して収まるようにします。

そのため、csv ファイルを一度に 1 行ずつ読み込み、その結果を (適切なバイナリ形式で) 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 などを使用)。うまくいけば、それがあなたの出発点になると思いました。

于 2009-12-13T19:32:17.313 に答える