web-dev-qa-db-fra.com

Python: Comment lire un gros fichier texte en mémoire

J'utilise Python 2.6 sur un Mac Mini avec 1 Go de RAM. Je veux lire dans un énorme fichier texte

$ 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

Ainsi, chaque ligne du fichier consiste en un tuple de deux valeurs entières séparées par des virgules. Je souhaite lire le fichier entier et le trier en fonction de la deuxième colonne. Je sais que je pourrais faire le tri sans lire le fichier entier en mémoire. Mais je pensais que pour un fichier de 500 Mo, je devrais pouvoir le faire en mémoire car j'ai 1 Go disponible.

Cependant, lorsque j'essaie de lire le fichier, Python semble allouer beaucoup plus de mémoire que ce dont le fichier a besoin. Donc, même avec 1 Go de RAM, je ne parviens pas à lire le fichier de 500 Mo dans la mémoire. Mon code Python pour lire le fichier et imprimer des informations sur la consommation de mémoire est:

#!/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 

La sortie que j'ai eu était:

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

Déjà après avoir lu seulement 25% du fichier de 500 Mo, Python consomme 500 Mo. Il semble donc que stocker le contenu du fichier sous la forme d’une liste de tuples d’entités n’est pas très efficace en termes de mémoire. Mémoire?

22
asmaier

Il existe une recette pour trier des fichiers plus volumineux que RAM sur cette page , bien que vous deviez l'adapter à votre cas impliquant des données au format CSV. Vous y trouverez également des liens vers des ressources supplémentaires.

Edit: True, le fichier sur le disque n’est pas "plus grand que la RAM", mais la représentation en mémoire peut facilement devenir beaucoup plus grande que disponible RAM. D'une part, votre propre programme ne reçoit pas l'intégralité de 1 Go (surcharge du système d'exploitation, etc.). D'autre part, même si vous stockiez cela sous la forme la plus compacte pour Python pur (deux listes d'entiers, en supposant une machine 32 bits, etc.), vous utiliseriez 934 Mo pour ces 30 millions de paires d'entiers.

En utilisant numpy, vous pouvez également faire le travail, en utilisant seulement environ 250 Mo. Ce n'est pas particulièrement rapide de charger de cette façon, car vous devez compter les lignes et pré-allouer le tableau, mais il peut s'agir du tri le plus rapide, vu qu'il est en mémoire:

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()

Sortie sur ma machine avec un exemple de fichier similaire à ce que vous avez montré:

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

La valeur par défaut dans numpy est Quicksort . La routine ndarray.sort () (qui trie sur place) peut également prendre l'argument clé kind="mergesort" ou kind="heapsort", mais il semble qu'aucune de celles-ci ne soit capable de trier sur un Record Array que j'ai utilisé comme seul moyen Je pouvais voir trier les colonnes ensemble par opposition à la valeur par défaut qui les trierait indépendamment (foirer totalement vos données).

18
Peter Hansen

Tous les objets python ont une surcharge de mémoire en plus des données qu’ils sont en train de stocker. Selon getsizeof de mon système Ubuntu 32 bits, un tuple représente 32 octets et un int prend 12 octets. Ainsi, chaque ligne de votre fichier utilise un pointeur de 56 octets + un pointeur de 4 octets dans la liste - je présume que ce sera beaucoup plus pour un système 64 bits. Cela correspond aux chiffres que vous avez donnés et signifie que vos 30 millions de lignes prendront 1,8 Go.

Je suggère qu'au lieu d'utiliser python, vous utilisiez l'utilitaire de tri Unix. Je ne suis pas un Mac-Head, mais je présume que les options de tri d'OS X sont identiques à celles de la version Linux. Cela devrait donc fonctionner:

sort -n -t, -k2 links.csv

-n signifie trier numériquement

-t, signifie utiliser une virgule comme séparateur de champ

-k2 signifie trier sur le deuxième champ

Cela va trier le fichier et écrire le résultat sur stdout. Vous pouvez le rediriger vers un autre fichier ou le diriger vers votre programme python pour effectuer un traitement ultérieur.

edit: Si vous ne souhaitez pas trier le fichier avant d'exécuter votre script python, vous pouvez utiliser le module de sous-processus pour créer un canal vers l'utilitaire de tri Shell, puis lire les résultats triés à partir de la sortie du canal. .

7
Dave Kirby

Comme ce ne sont que des nombres, les charger dans un tableau Nx2 enlèverait une surcharge. Utilisez NumPy pour les tableaux multidimensionnels. Alternativement, vous pouvez utiliser deux python arrays normaux pour représenter chaque colonne. 

4
kwatford

Le moyen le moins coûteux de stocker les lignes d’entrée en mémoire consiste à utiliser des éléments array.array ('i') - en supposant que chaque nombre tiendra dans un entier signé de 32 bits. Le coût de la mémoire sera de 8N octets, N étant le nombre de lignes.

Voici comment faire le tri et écrire le fichier de sortie dans l'ordre trié:

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]])

Malheureusement, sorted() renvoie une liste, pas un itérateur, et cette liste sera plutôt longue: 4N octets pour les pointeurs et 12N octets pour les objets int, c’est-à-dire 16N octets pour la sortie sorted(). Remarque: ceci est basé sur CPython 2.X sur une machine 32 bits; la situation empire pour les machines 3.X et 64 bits. Tout cela fait 24N octets. Vous avez 31 millions de lignes, vous avez donc besoin de 31 * 24 = 744 Mo ... semble devoir fonctionner; Notez que ce calcul ne permet aucune mémoire allouée par le tri, mais vous disposez d'une marge de sécurité raisonnable.

De côté: Quel est le coût d'un gigaoctet supplémentaire ou de 3 de mémoire exprimé en heures à votre taux de salaire?

4
John Machin

Vous voudrez peut-être regarder mmap:

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

Cela vous permettra de traiter le fichier comme un grand tableau/chaîne et obligera le système d'exploitation à gérer le brassage des données dans et hors de la mémoire pour le laisser entrer.

Vous pouvez donc lire le fichier csv, ligne par ligne, puis écrire les résultats dans un fichier mmap'd (dans un format binaire approprié), puis travailler sur le fichier mmap'd. Comme le fichier mmap'd n’est que temporaire, vous pouvez bien sûr créer un fichier tmp à cet effet.

Voici quelques exemples de code utilisant mmap avec un fichier temporaire pour lire les données csv et les stocker sous forme de paires d'entiers:


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)

C'est un peu dur cependant. Vraiment, vous voudrez probablement terminer tout cela avec une classe Nice, pour pouvoir accéder à votre Edge de manière à ce qu’ils se comportent comme une liste (avec indexation, len, etc.). Espérons que cela vous donnerait un point de départ.

2
John Montgomery

J'ai créé un module pour ce cas d'utilisation en utilisant type de fusion externe : https://bitbucket.org/richardpenman/csvsort

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