Méthode paresseuse pour lire un gros fichier en Python?

290

J'ai un très gros fichier de 4 Go et lorsque j'essaie de le lire, mon ordinateur se bloque. Je veux donc le lire morceau par morceau et après le traitement de chaque morceau, stocker le morceau traité dans un autre fichier et lire le morceau suivant.

Y a-t-il une méthode pour yieldces pièces?

J'adorerais avoir une méthode paresseuse .

Pratik Deoghare
la source

Réponses:

424

Pour écrire une fonction paresseuse, utilisez simplement yield:

def read_in_chunks(file_object, chunk_size=1024):
    """Lazy function (generator) to read a file piece by piece.
    Default chunk size: 1k."""
    while True:
        data = file_object.read(chunk_size)
        if not data:
            break
        yield data


with open('really_big_file.dat') as f:
    for piece in read_in_chunks(f):
        process_data(piece)

Une autre option serait d'utiliser iteret une fonction d'aide:

f = open('really_big_file.dat')
def read1k():
    return f.read(1024)

for piece in iter(read1k, ''):
    process_data(piece)

Si le fichier est basé sur des lignes, l'objet fichier est déjà un générateur de lignes paresseux:

for line in open('really_big_file.dat'):
    process_data(line)
nosklo
la source
La ligne f = open('really_big_file.dat')n'est donc qu'un pointeur sans aucune consommation de mémoire? (Je veux dire que la mémoire consommée est la même quelle que soit la taille du fichier?) Comment cela affectera-t-il les performances si j'utilise urllib.readline () au lieu de f.readline ()?
2011
4
Bonne pratique pour utiliser open ('really_big_file.dat', 'rb') pour la compatibilité avec nos Windows avec Posix utilisant des collègues.
Tal Weiss
6
Manquant rbcomme @Tal Weiss l'a mentionné; et manque une file.close()déclaration (pourrait utiliser with open('really_big_file.dat', 'rb') as f:pour accomplir la même chose; voir ici pour une autre mise en œuvre concise
cod3monk3y
4
@ cod3monk3y: les fichiers texte et binaires sont des choses différentes. Les deux types sont utiles mais dans des cas différents. Le mode par défaut (texte) peut être utile ici, c'est-à-dire qu'il 'rb'n'est pas manquant.
jfs
2
@ jf-sebastian: vrai, l'OP n'a pas précisé s'il lisait des données textuelles ou binaires. Mais s'il utilise python 2.7 de Windows et est en train de lire des données binaires, il est certainement intéressant de noter que s'il oublie les 'b'ses données très probablement corrompus . De la documentation -Python on Windows makes a distinction between text and binary files; [...] it’ll corrupt binary data like that in JPEG or EXE files. Be very careful to use binary mode when reading and writing such files.
cod3monk3y
41

Si votre ordinateur, votre système d'exploitation et python sont en 64 bits , vous pouvez utiliser le module mmap pour mapper le contenu du fichier en mémoire et y accéder avec des index et des tranches. Voici un exemple tiré de la documentation:

import mmap
with open("hello.txt", "r+") as f:
    # memory-map the file, size 0 means whole file
    map = mmap.mmap(f.fileno(), 0)
    # read content via standard file methods
    print map.readline()  # prints "Hello Python!"
    # read content via slice notation
    print map[:5]  # prints "Hello"
    # update content using slice notation;
    # note that new content must have same size
    map[6:] = " world!\n"
    # ... and read again using standard file methods
    map.seek(0)
    print map.readline()  # prints "Hello  world!"
    # close the map
    map.close()

Si votre ordinateur, votre système d'exploitation ou python sont 32 bits , alors le mappage de gros fichiers peut réserver de grandes parties de votre espace d'adressage et affamer votre programme de mémoire.

Communauté
la source
7
Comment est-ce censé fonctionner? Et si j'ai un fichier de 32 Go? Que faire si je suis sur une machine virtuelle avec 256 Mo de RAM? Cartographier un fichier aussi volumineux n'est jamais une bonne chose.
Savino Sguera
4
Cette réponse mérite un vote de -12. Cela tuera quiconque l'utilisant pour de gros fichiers.
Phyo Arkar Lwin
23
Cela peut fonctionner sur un Python 64 bits même pour les gros fichiers. Même si le fichier est mappé en mémoire, il n'est pas lu en mémoire, donc la quantité de mémoire physique peut être beaucoup plus petite que la taille du fichier.
pts
1
@SavinoSguera la taille de la mémoire physique est-elle importante pour le mappage d'un fichier?
Nick T
17
@ V3ss0n: J'ai essayé de mapper un fichier de 32 Go sur Python 64 bits. Cela fonctionne (j'ai moins de 32 Go de RAM): je peux accéder au début, au milieu et à la fin du fichier à l'aide des interfaces de séquence et de fichier.
jfs
37

file.readlines() prend un argument de taille facultative qui se rapproche du nombre de lignes lues dans les lignes retournées.

bigfile = open('bigfilename','r')
tmp_lines = bigfile.readlines(BUF_SIZE)
while tmp_lines:
    process([line for line in tmp_lines])
    tmp_lines = bigfile.readlines(BUF_SIZE)
Anshul
la source
1
c'est une très bonne idée, surtout quand elle est combinée avec le defaultdict pour diviser les big data en plus petites.
Frank Wang
4
Je recommanderais de .read()ne pas utiliser .readlines(). Si le fichier est binaire, il ne comportera pas de sauts de ligne.
Myers Carpenter
1
Et si le fichier est une énorme chaîne?
MattSom
28

Il existe déjà de nombreuses bonnes réponses, mais si tout votre fichier se trouve sur une seule ligne et que vous souhaitez toujours traiter les "lignes" (par opposition aux blocs de taille fixe), ces réponses ne vous seront d'aucune aide.

99% du temps, il est possible de traiter les fichiers ligne par ligne. Ensuite, comme suggéré dans cette réponse , vous pouvez utiliser l'objet fichier lui-même comme générateur paresseux:

with open('big.csv') as f:
    for line in f:
        process(line)

Cependant, j'ai rencontré une fois un très gros fichier (presque) simple, où le séparateur de lignes n'était en fait pas '\n'mais '|'.

  • La lecture ligne par ligne n'était pas une option, mais j'avais encore besoin de la traiter ligne par ligne.
  • La conversion '|'en '\n'avant le traitement était également hors de question, car certains champs de ce csv contenaient '\n'(saisie utilisateur en texte libre).
  • L'utilisation de la bibliothèque csv a également été exclue car le fait que, au moins dans les premières versions de la bibliothèque, il est codé en dur pour lire l'entrée ligne par ligne .

Pour ce genre de situations, j'ai créé l'extrait de code suivant:

def rows(f, chunksize=1024, sep='|'):
    """
    Read a file where the row separator is '|' lazily.

    Usage:

    >>> with open('big.csv') as f:
    >>>     for r in rows(f):
    >>>         process(row)
    """
    curr_row = ''
    while True:
        chunk = f.read(chunksize)
        if chunk == '': # End of file
            yield curr_row
            break
        while True:
            i = chunk.find(sep)
            if i == -1:
                break
            yield curr_row + chunk[:i]
            curr_row = ''
            chunk = chunk[i+1:]
        curr_row += chunk

J'ai pu l'utiliser avec succès pour résoudre mon problème. Il a été largement testé, avec différentes tailles de morceaux.


Suite de tests, pour ceux qui veulent se convaincre.

test_file = 'test_file'

def cleanup(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        os.unlink(test_file)
    return wrapper

@cleanup
def test_empty(chunksize=1024):
    with open(test_file, 'w') as f:
        f.write('')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 1

@cleanup
def test_1_char_2_rows(chunksize=1024):
    with open(test_file, 'w') as f:
        f.write('|')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 2

@cleanup
def test_1_char(chunksize=1024):
    with open(test_file, 'w') as f:
        f.write('a')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 1

@cleanup
def test_1025_chars_1_row(chunksize=1024):
    with open(test_file, 'w') as f:
        for i in range(1025):
            f.write('a')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 1

@cleanup
def test_1024_chars_2_rows(chunksize=1024):
    with open(test_file, 'w') as f:
        for i in range(1023):
            f.write('a')
        f.write('|')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 2

@cleanup
def test_1025_chars_1026_rows(chunksize=1024):
    with open(test_file, 'w') as f:
        for i in range(1025):
            f.write('|')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 1026

@cleanup
def test_2048_chars_2_rows(chunksize=1024):
    with open(test_file, 'w') as f:
        for i in range(1022):
            f.write('a')
        f.write('|')
        f.write('a')
        # -- end of 1st chunk --
        for i in range(1024):
            f.write('a')
        # -- end of 2nd chunk
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 2

@cleanup
def test_2049_chars_2_rows(chunksize=1024):
    with open(test_file, 'w') as f:
        for i in range(1022):
            f.write('a')
        f.write('|')
        f.write('a')
        # -- end of 1st chunk --
        for i in range(1024):
            f.write('a')
        # -- end of 2nd chunk
        f.write('a')
    with open(test_file) as f:
        assert len(list(rows(f, chunksize=chunksize))) == 2

if __name__ == '__main__':
    for chunksize in [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]:
        test_empty(chunksize)
        test_1_char_2_rows(chunksize)
        test_1_char(chunksize)
        test_1025_chars_1_row(chunksize)
        test_1024_chars_2_rows(chunksize)
        test_1025_chars_1026_rows(chunksize)
        test_2048_chars_2_rows(chunksize)
        test_2049_chars_2_rows(chunksize)
user48678
la source
11
f = ... # file-like object, i.e. supporting read(size) function and 
        # returning empty string '' when there is nothing to read

def chunked(file, chunk_size):
    return iter(lambda: file.read(chunk_size), '')

for data in chunked(f, 65536):
    # process the data

MISE À JOUR: L'approche est mieux expliquée dans https://stackoverflow.com/a/4566523/38592

myroslav
la source
Cela fonctionne bien pour les blobs, mais peut ne pas être bon pour le contenu séparé par ligne (comme CSV, HTML, etc. où le traitement doit être géré ligne par ligne)
cgseller
7

Reportez-vous à la documentation officielle de python https://docs.python.org/zh-cn/3/library/functions.html?#iter

Peut-être que cette méthode est plus pythonique:

from functools import partial

"""A file object returned by open() is a iterator with
read method which could specify current read's block size"""
with open('mydata.db', 'r') as f_in:

    part_read = partial(f_in.read, 1024*1024)
    iterator = iter(part_read, b'')

    for index, block in enumerate(iterator, start=1):
        block = process_block(block)    # process block data
        with open(f'{index}.txt', 'w') as f_out:
            f_out.write(block)
Bruce
la source
3

Je pense que nous pouvons écrire comme ceci:

def read_file(path, block_size=1024): 
    with open(path, 'rb') as f: 
        while True: 
            piece = f.read(block_size) 
            if piece: 
                yield piece 
            else: 
                return

for piece in read_file(path):
    process_piece(piece)
TonyCoolZhu
la source
2

je ne suis pas autorisé à commenter en raison de ma faible réputation, mais la solution SilentGhosts devrait être beaucoup plus facile avec file.readlines ([sizehint])

méthodes de fichier python

edit: SilentGhost a raison, mais cela devrait être mieux que:

s = "" 
for i in xrange(100): 
   s += file.next()
sinzi
la source
ok, désolé, vous avez absolument raison. mais peut-être que cette solution vous rendra plus heureux;): s = "" pour i dans xrange (100): s + = file.next ()
sinzi
1
-1: Terrible solution, cela signifierait créer une nouvelle chaîne en mémoire à chaque ligne et copier toutes les données du fichier lu dans la nouvelle chaîne. Les pires performances et mémoire.
nosklo
pourquoi copierait-il toutes les données du fichier dans une nouvelle chaîne? à partir de la documentation de python: Afin de faire d'une boucle for le moyen le plus efficace de boucler sur les lignes d'un fichier (une opération très courante), la méthode next () utilise un tampon de lecture anticipée caché.
sinzi
3
@sinzi: "s + =" ou concaténation de chaînes crée une nouvelle copie de la chaîne à chaque fois, car la chaîne est immuable, vous créez donc une nouvelle chaîne.
nosklo
1
@nosklo: ce sont des détails de mise en œuvre, la compréhension de la liste peut être utilisée à sa place
SilentGhost
1

Je suis dans une situation quelque peu similaire. Il n'est pas clair si vous connaissez la taille des morceaux en octets; Je ne le fais généralement pas, mais le nombre d'enregistrements (lignes) requis est connu:

def get_line():
     with open('4gb_file') as file:
         for i in file:
             yield i

lines_required = 100
gen = get_line()
chunk = [i for i, j in zip(gen, range(lines_required))]

Mise à jour : Merci nosklo. Voici ce que je voulais dire. Cela fonctionne presque, sauf qu'il perd une ligne «entre» des morceaux.

chunk = [next(gen) for i in range(lines_required)]

Est-ce que l'astuce sans perdre de lignes, mais ça n'a pas l'air très agréable.

SilentGhost
la source
1
est ce pseudo code? ça ne marchera pas. Il est également inutile de confondre, vous devez faire du nombre de lignes un paramètre facultatif de la fonction get_line.
nosklo
0

Pour traiter ligne par ligne, c'est une solution élégante:

  def stream_lines(file_name):
    file = open(file_name)
    while True:
      line = file.readline()
      if not line:
        file.close()
        break
      yield line

Tant qu'il n'y a pas de lignes vides.

crizCraig
la source
6
C'est juste un équivalent trop compliqué, moins robuste et plus lent que ce openqui vous donne déjà. Un fichier est déjà un itérateur sur ses lignes.
abarnert
-2

vous pouvez utiliser le code suivant.

file_obj = open('big_file') 

open () retourne un objet fichier

puis utilisez os.stat pour obtenir la taille

file_size = os.stat('big_file').st_size

for i in range( file_size/1024):
    print file_obj.read(1024)
shrikant
la source
ne lirait pas le fichier entier si la taille n'était pas une multiplication de 1024
kmaork