web-dev-qa-db-fra.com

Le moyen le plus rapide de traiter un gros fichier?

J'ai plusieurs fichiers séparés par des tabulations de 3 Go. Il y a 20 millions de lignes dans chaque fichier. Toutes les lignes doivent être traitées indépendamment, aucune relation entre deux lignes. Ma question est de savoir ce qui sera plus rapide A. Lire ligne par ligne en utilisant:

with open() as infile:
    for line in infile:

Ou B. Lire le fichier en mémoire par morceaux et le traiter, disons 250 Mo à la fois?

Le traitement n'est pas très compliqué, je saisis juste la valeur dans la colonne 1 pour List1, colonne2 à List2 etc. Peut-être besoin d'ajouter des valeurs de colonne ensemble.

J'utilise python 2.7 sur une boîte Linux qui a 30 Go de mémoire. ASCII Text.

Une façon d'accélérer les choses en parallèle? En ce moment, j'utilise l'ancienne méthode et le processus est très lent. L'utilisation d'un module CSVReader va-t-elle aider? Je n'ai pas à le faire en python, tout autre langage ou idée d'utilisation de base de données est le bienvenu.

27
Reise45

Il semble que votre code soit lié aux E/S. Cela signifie que le multitraitement ne va pas vous aider - si vous passez 90% de votre temps à lire à partir du disque, avoir 7 processus supplémentaires en attente à la prochaine lecture ne va rien aider.

Et, tout en utilisant un module de lecture CSV (que ce soit le csv de stdlib ou quelque chose comme NumPy ou Pandas) peut être une bonne idée pour la simplicité, il est peu probable de faire beaucoup de différence dans les performances.

Pourtant, cela vaut la peine de vérifier que vous êtes vraiment lié aux E/S, au lieu de simplement deviner. Exécutez votre programme et voyez si votre utilisation du processeur est proche de 0% ou proche de 100% ou d'un cœur. Faites ce que Amadan a suggéré dans un commentaire, et exécutez votre programme avec juste pass pour le traitement et voyez si cela coupe 5% du temps ou 70%. Vous pouvez même essayer de comparer avec une boucle sur os.open Et os.read(1024*1024) ou quelque chose et voir si c'est plus rapide.


Étant donné que vous utilisez Python 2.x, Python s'appuie sur la bibliothèque C stdio pour deviner la quantité de mémoire tampon à la fois, il peut donc être utile de forcer pour tamponner plus. La façon la plus simple de le faire est d'utiliser readlines(bufsize) pour certains gros bufsize. (Vous pouvez essayer différents nombres et les mesurer pour voir où se trouve le pic. D'après mon expérience , généralement, tout ce qui est de 64 Ko à 8 Mo est à peu près le même, mais en fonction de votre système, cela peut être différent, surtout si vous lisez, par exemple, un système de fichiers réseau avec un débit élevé mais une latence horrible qui submerge le débit contre la latence du disque physique réel et de la mise en cache du système d'exploitation.)

Ainsi, par exemple:

bufsize = 65536
with open(path) as infile: 
    while True:
        lines = infile.readlines(bufsize)
        if not lines:
            break
        for line in lines:
            process(line)

En attendant, en supposant que vous êtes sur un système 64 bits, vous pouvez essayer d'utiliser mmap au lieu de lire le fichier en premier lieu. Ce n'est certainement pas garanti pour être meilleur, mais il peut être mieux, selon votre système. Par exemple:

with open(path) as infile:
    m = mmap.mmap(infile, 0, access=mmap.ACCESS_READ)

A Python mmap est en quelque sorte un objet étrange - il agit comme un str et comme un file en même temps, donc vous peut, par exemple, itérer manuellement l'analyse des sauts de ligne, ou vous pouvez appeler readline dessus comme s'il s'agissait d'un fichier. Les deux prendront plus de traitement de Python que d'itérer la fichier sous forme de lignes ou faisant un batch readlines (car une boucle qui serait en C est maintenant en Python pur… bien que peut-être vous pouvez contourner cela avec re, ou avec une simple extension Cython?) … Mais l'avantage d'E/S du système d'exploitation sachant ce que vous faites avec le mappage peut inonder l'inconvénient du processeur.

Malheureusement, Python n'expose pas l'appel madvise que vous utiliseriez pour modifier les choses dans le but d'optimiser cela en C (par exemple, en définissant explicitement MADV_SEQUENTIAL au lieu de faire deviner le noyau ou de forcer des pages énormes transparentes) - mais vous pouvez réellement ctypes la fonction à partir de libc.

33
abarnert

Je sais que cette question est ancienne; mais je voulais faire une chose similaire, j'ai créé un cadre simple qui vous aide à lire et à traiter un gros fichier en parallèle. Laissant ce que j'ai essayé comme réponse.

Ceci est le code, je donne un exemple à la fin

def chunkify_file(fname, size=1024*1024*1000, skiplines=-1):
    """
    function to divide a large text file into chunks each having size ~= size so that the chunks are line aligned

    Params : 
        fname : path to the file to be chunked
        size : size of each chink is ~> this
        skiplines : number of lines in the begining to skip, -1 means don't skip any lines
    Returns : 
        start and end position of chunks in Bytes
    """
    chunks = []
    fileEnd = os.path.getsize(fname)
    with open(fname, "rb") as f:
        if(skiplines > 0):
            for i in range(skiplines):
                f.readline()

        chunkEnd = f.tell()
        count = 0
        while True:
            chunkStart = chunkEnd
            f.seek(f.tell() + size, os.SEEK_SET)
            f.readline()  # make this chunk line aligned
            chunkEnd = f.tell()
            chunks.append((chunkStart, chunkEnd - chunkStart, fname))
            count+=1

            if chunkEnd > fileEnd:
                break
    return chunks

def parallel_apply_line_by_line_chunk(chunk_data):
    """
    function to apply a function to each line in a chunk

    Params :
        chunk_data : the data for this chunk 
    Returns :
        list of the non-None results for this chunk
    """
    chunk_start, chunk_size, file_path, func_apply = chunk_data[:4]
    func_args = chunk_data[4:]

    t1 = time.time()
    chunk_res = []
    with open(file_path, "rb") as f:
        f.seek(chunk_start)
        cont = f.read(chunk_size).decode(encoding='utf-8')
        lines = cont.splitlines()

        for i,line in enumerate(lines):
            ret = func_apply(line, *func_args)
            if(ret != None):
                chunk_res.append(ret)
    return chunk_res

def parallel_apply_line_by_line(input_file_path, chunk_size_factor, num_procs, skiplines, func_apply, func_args, fout=None):
    """
    function to apply a supplied function line by line in parallel

    Params :
        input_file_path : path to input file
        chunk_size_factor : size of 1 chunk in MB
        num_procs : number of parallel processes to spawn, max used is num of available cores - 1
        skiplines : number of top lines to skip while processing
        func_apply : a function which expects a line and outputs None for lines we don't want processed
        func_args : arguments to function func_apply
        fout : do we want to output the processed lines to a file
    Returns :
        list of the non-None results obtained be processing each line
    """
    num_parallel = min(num_procs, psutil.cpu_count()) - 1

    jobs = chunkify_file(input_file_path, 1024 * 1024 * chunk_size_factor, skiplines)

    jobs = [list(x) + [func_apply] + func_args for x in jobs]

    print("Starting the parallel pool for {} jobs ".format(len(jobs)))

    lines_counter = 0

    pool = mp.Pool(num_parallel, maxtasksperchild=1000)  # maxtaskperchild - if not supplied some weird happend and memory blows as the processes keep on lingering

    outputs = []
    for i in range(0, len(jobs), num_parallel):
        print("Chunk start = ", i)
        t1 = time.time()
        chunk_outputs = pool.map(parallel_apply_line_by_line_chunk, jobs[i : i + num_parallel])

        for i, subl in enumerate(chunk_outputs):
            for x in subl:
                if(fout != None):
                    print(x, file=fout)
                else:
                    outputs.append(x)
                lines_counter += 1
        del(chunk_outputs)
        gc.collect()
        print("All Done in time ", time.time() - t1)

    print("Total lines we have = {}".format(lines_counter))

    pool.close()
    pool.terminate()
    return outputs

Disons par exemple, j'ai un fichier dans lequel je veux compter le nombre de mots dans chaque ligne, alors le traitement de chaque ligne ressemblerait à

def count_words_line(line):
    return len(line.strip().split())

puis appelez la fonction comme:

parallel_apply_line_by_line(input_file_path, 100, 8, 0, count_words_line, [], fout=None)

En utilisant cela, j'obtiens une vitesse de ~ 8 fois par rapport à Vanilla lecture ligne par ligne sur un fichier échantillon de taille ~ 20 Go dans laquelle je fais un traitement modérément compliqué sur chaque ligne.

0
Deepak Saini