web-dev-qa-db-fra.com

Comment obtenir le nombre de lignes pas cher en Python?

Je dois obtenir le nombre de lignes d'un fichier volumineux (des centaines de milliers de lignes) en python. Quel est le moyen le plus efficace à la fois de mémoire et de temps?

En ce moment je fais:

def file_len(fname):
    with open(fname) as f:
        for i, l in enumerate(f):
            pass
    return i + 1

est-il possible de faire mieux?

822
SilentGhost

Vous ne pouvez pas faire mieux que ça.

Après tout, toute solution devra lire l'intégralité du fichier, déterminer combien vous avez \n et renvoyer le résultat.

Avez-vous un meilleur moyen de le faire sans lire le fichier en entier? Pas sûr ... La meilleure solution sera toujours I/O-bound, le mieux que vous puissiez faire est de vous assurer que vous n'utilisez pas de mémoire inutile, mais il semble que vous l'ayez couverte.

288
Yuval Adam

Une ligne, probablement assez rapide:

num_lines = sum(1 for line in open('myfile.txt'))
514
Kyle

Je crois qu'un fichier mappé en mémoire sera la solution la plus rapide. J'ai essayé quatre fonctions: la fonction affichée par l'OP (opcount); une simple itération sur les lignes du fichier (simplecount); readline avec un fichier mappé en mémoire (mmap) (mapcount); et la solution de lecture tampon proposée par Mykola Kharechko (bufcount).

J'ai exécuté chaque fonction cinq fois et calculé le temps d'exécution moyen d'un fichier texte de 1,2 million de lignes.

Windows XP, Python 2.5, 2 Go de RAM, processeur AMD à 2 GHz

Voici mes résultats:

mapcount : 0.465599966049
simplecount : 0.756399965286
bufcount : 0.546800041199
opcount : 0.718600034714

Éditer : nombres pour Python 2.6:

mapcount : 0.471799945831
simplecount : 0.634400033951
bufcount : 0.468800067902
opcount : 0.602999973297

La stratégie de lecture de la mémoire tampon semble donc être la plus rapide pour Windows/Python 2.6.

Voici le code:

from __future__ import with_statement
import time
import mmap
import random
from collections import defaultdict

def mapcount(filename):
    f = open(filename, "r+")
    buf = mmap.mmap(f.fileno(), 0)
    lines = 0
    readline = buf.readline
    while readline():
        lines += 1
    return lines

def simplecount(filename):
    lines = 0
    for line in open(filename):
        lines += 1
    return lines

def bufcount(filename):
    f = open(filename)                  
    lines = 0
    buf_size = 1024 * 1024
    read_f = f.read # loop optimization

    buf = read_f(buf_size)
    while buf:
        lines += buf.count('\n')
        buf = read_f(buf_size)

    return lines

def opcount(fname):
    with open(fname) as f:
        for i, l in enumerate(f):
            pass
    return i + 1


counts = defaultdict(list)

for i in range(5):
    for func in [mapcount, simplecount, bufcount, opcount]:
        start_time = time.time()
        assert func("big_file.txt") == 1209138
        counts[func].append(time.time() - start_time)

for key, vals in counts.items():
    print key.__name__, ":", sum(vals) / float(len(vals))
184
Ryan Ginstrom

J'ai dû poster ceci sur une question similaire jusqu'à ce que mon score de réputation augmente un peu (merci à celui qui m'a bosseé!). 

Toutes ces solutions ignorent un moyen d’accélérer considérablement l’exécution de cette opération, à savoir l’utilisation de l’interface non mise en mémoire tampon (brute), l’utilisation de bytearrays et la mise en mémoire tampon de votre choix. (Ceci s'applique uniquement à Python 3. Dans Python 2, l'interface brute peut ou non être utilisée par défaut, mais dans Python 3, vous utiliserez par défaut Unicode.)

En utilisant une version modifiée de l'outil de chronométrage, je pense que le code suivant est plus rapide (et légèrement plus pythonique) que les solutions proposées:

def rawcount(filename):
    f = open(filename, 'rb')
    lines = 0
    buf_size = 1024 * 1024
    read_f = f.raw.read

    buf = read_f(buf_size)
    while buf:
        lines += buf.count(b'\n')
        buf = read_f(buf_size)

    return lines

En utilisant une fonction de générateur séparée, cela exécute un smidge plus rapidement:

def _make_gen(reader):
    b = reader(1024 * 1024)
    while b:
        yield b
        b = reader(1024*1024)

def rawgencount(filename):
    f = open(filename, 'rb')
    f_gen = _make_gen(f.raw.read)
    return sum( buf.count(b'\n') for buf in f_gen )

Cela peut être fait complètement avec les expressions de générateurs en ligne en utilisant itertools, mais cela devient plutôt étrange:

from itertools import (takewhile,repeat)

def rawincount(filename):
    f = open(filename, 'rb')
    bufgen = takewhile(lambda x: x, (f.raw.read(1024*1024) for _ in repeat(None)))
    return sum( buf.count(b'\n') for buf in bufgen )

Voici mes timings:

function      average, s  min, s   ratio
rawincount        0.0043  0.0041   1.00
rawgencount       0.0044  0.0042   1.01
rawcount          0.0048  0.0045   1.09
bufcount          0.008   0.0068   1.64
wccount           0.01    0.0097   2.35
itercount         0.014   0.014    3.41
opcount           0.02    0.02     4.83
kylecount         0.021   0.021    5.05
simplecount       0.022   0.022    5.25
mapcount          0.037   0.031    7.46
89
Michael Bacon

Vous pouvez exécuter un sous-processus et exécuter wc -l filename

import subprocess

def file_len(fname):
    p = subprocess.Popen(['wc', '-l', fname], stdout=subprocess.PIPE, 
                                              stderr=subprocess.PIPE)
    result, err = p.communicate()
    if p.returncode != 0:
        raise IOError(err)
    return int(result.strip().split()[0])
76
Ólafur Waage

Voici un programme python qui utilise la bibliothèque de multitraitement pour distribuer le comptage de lignes sur des machines/cœurs. Mon test améliore le comptage d'un fichier de 20 millions de lignes de 26 secondes à 7 secondes à l'aide d'un serveur Windows 64 à 8 coeurs. Remarque: ne pas utiliser le mappage de mémoire rend les choses beaucoup plus lentes.

import multiprocessing, sys, time, os, mmap
import logging, logging.handlers

def init_logger(pid):
    console_format = 'P{0} %(levelname)s %(message)s'.format(pid)
    logger = logging.getLogger()  # New logger at root level
    logger.setLevel( logging.INFO )
    logger.handlers.append( logging.StreamHandler() )
    logger.handlers[0].setFormatter( logging.Formatter( console_format, '%d/%m/%y %H:%M:%S' ) )

def getFileLineCount( queues, pid, processes, file1 ):
    init_logger(pid)
    logging.info( 'start' )

    physical_file = open(file1, "r")
    #  mmap.mmap(fileno, length[, tagname[, access[, offset]]]

    m1 = mmap.mmap( physical_file.fileno(), 0, access=mmap.ACCESS_READ )

    #work out file size to divide up line counting

    fSize = os.stat(file1).st_size
    chunk = (fSize / processes) + 1

    lines = 0

    #get where I start and stop
    _seedStart = chunk * (pid)
    _seekEnd = chunk * (pid+1)
    seekStart = int(_seedStart)
    seekEnd = int(_seekEnd)

    if seekEnd < int(_seekEnd + 1):
        seekEnd += 1

    if _seedStart < int(seekStart + 1):
        seekStart += 1

    if seekEnd > fSize:
        seekEnd = fSize

    #find where to start
    if pid > 0:
        m1.seek( seekStart )
        #read next line
        l1 = m1.readline()  # need to use readline with memory mapped files
        seekStart = m1.tell()

    #tell previous rank my seek start to make their seek end

    if pid > 0:
        queues[pid-1].put( seekStart )
    if pid < processes-1:
        seekEnd = queues[pid].get()

    m1.seek( seekStart )
    l1 = m1.readline()

    while len(l1) > 0:
        lines += 1
        l1 = m1.readline()
        if m1.tell() > seekEnd or len(l1) == 0:
            break

    logging.info( 'done' )
    # add up the results
    if pid == 0:
        for p in range(1,processes):
            lines += queues[0].get()
        queues[0].put(lines) # the total lines counted
    else:
        queues[0].put(lines)

    m1.close()
    physical_file.close()

if __== '__main__':
    init_logger( 'main' )
    if len(sys.argv) > 1:
        file_name = sys.argv[1]
    else:
        logging.fatal( 'parameters required: file-name [processes]' )
        exit()

    t = time.time()
    processes = multiprocessing.cpu_count()
    if len(sys.argv) > 2:
        processes = int(sys.argv[2])
    queues=[] # a queue for each process
    for pid in range(processes):
        queues.append( multiprocessing.Queue() )
    jobs=[]
    prev_pipe = 0
    for pid in range(processes):
        p = multiprocessing.Process( target = getFileLineCount, args=(queues, pid, processes, file_name,) )
        p.start()
        jobs.append(p)

    jobs[0].join() #wait for counting to finish
    lines = queues[0].get()

    logging.info( 'finished {} Lines:{}'.format( time.time() - t, lines ) )
37
Martlark

J'utiliserais la méthode d'objet de fichier Python readlines, comme suit:

with open(input_file) as foo:
    lines = len(foo.readlines())

Cela ouvre le fichier, crée une liste de lignes dans le fichier, compte la longueur de la liste, l'enregistre dans une variable et ferme à nouveau le fichier.

10
Daniel Lee

Voici ce que j'utilise, semble assez propre:

import subprocess

def count_file_lines(file_path):
    """
    Counts the number of lines in a file using wc utility.
    :param file_path: path to file
    :return: int, no of lines
    """
    num = subprocess.check_output(['wc', '-l', file_path])
    num = num.split(' ')
    return int(num[0])

UPDATE: Cela est légèrement plus rapide que d’utiliser du python pur, mais au prix de l’utilisation de la mémoire. Le sous-processus crée un nouveau processus avec la même empreinte mémoire que le processus parent pendant l'exécution de la commande.

8
radtek

J'ai eu une petite amélioration (4-8%) avec cette version qui réutilise un tampon constant afin d'éviter toute surcharge de mémoire ou de GC:

lines = 0
buffer = bytearray(2048)
with open(filename) as f:
  while f.readinto(buffer) > 0:
      lines += buffer.count('\n')

Vous pouvez jouer avec la taille du tampon et peut-être voir une petite amélioration.

8
Scott Persinger
def file_len(full_path):
  """ Count number of lines in a file."""
  f = open(full_path)
  nr_of_lines = sum(1 for line in f)
  f.close()
  return nr_of_lines
7
pkit

solution en une ligne

import os
os.system("wc -l  filename")  

mon bout

os.system ('wc -l * .txt')

0 bar.txt
1000 command.txt
3 test_file.txt
1003 total
7
TheExorcist

La réponse de Kyle

num_lines = sum(1 for line in open('my_file.txt'))

est probablement le meilleur, une alternative pour cela est

num_lines =  len(open('my_file.txt').read().splitlines())

Voici la comparaison des performances des deux 

In [20]: timeit sum(1 for line in open('Charts.ipynb'))
100000 loops, best of 3: 9.79 µs per loop

In [21]: timeit len(open('Charts.ipynb').read().splitlines())
100000 loops, best of 3: 12 µs per loop
7
ChillarAnand

C’est la solution la plus rapide que j’ai trouvée en utilisant du python pur . Vous pouvez utiliser n’importe quelle quantité de mémoire que vous voulez en configurant le tampon, bien que 2 ** 16 apparaisse comme un point d’équilibre sur mon ordinateur. 

from functools import partial

buffer=2**16
with open(myfile) as f:
        print sum(x.count('\n') for x in iter(partial(f.read,buffer), ''))

J'ai trouvé la réponse ici Pourquoi la lecture des lignes de stdin est-elle beaucoup plus lente en C++ qu'en Python? et l'a tordu juste un tout petit peu. C’est une très bonne lecture pour comprendre comment compter les lignes rapidement, bien que wc -l soit toujours environ 75% plus rapide qu’autre chose.

6
jeffpkamp

Juste pour compléter les méthodes ci-dessus, j'ai essayé une variante avec le module fileinput:

import fileinput as fi   
def filecount(fname):
        for line in fi.input(fname):
            pass
        return fi.lineno()

Et passé un fichier de lignes de 60 millilitres à toutes les méthodes mentionnées ci-dessus:

mapcount : 6.1331050396
simplecount : 4.588793993
opcount : 4.42918205261
filecount : 43.2780818939
bufcount : 0.170812129974

C'est une petite surprise pour moi que l'entrée de fichier soit si mauvaise et qu'elle soit bien pire que toutes les autres méthodes.

5
BandGap

Une solution bash sur une ligne similaire à this answer , en utilisant la fonction moderne subprocess.check_output:

def line_count(file):
    return int(subprocess.check_output('wc -l {}'.format(file), Shell=True).split()[0])
5
1''

Ce code est plus court et plus clair. C'est probablement le meilleur moyen:

num_lines = open('yourfile.ext').read().count('\n')
5
Texom512

Pour moi cette variante sera la plus rapide:

#!/usr/bin/env python

def main():
    f = open('filename')                  
    lines = 0
    buf_size = 1024 * 1024
    read_f = f.read # loop optimization

    buf = read_f(buf_size)
    while buf:
        lines += buf.count('\n')
        buf = read_f(buf_size)

    print lines

if __== '__main__':
    main()

raisons: la mise en mémoire tampon plus rapide que la lecture ligne par ligne et string.count est également très rapide

4
Mykola Kharechko
print open('file.txt', 'r').read().count("\n") + 1
4
Andrés Torres

le résultat de l'ouverture d'un fichier est un itérateur qui peut être converti en une séquence de longueur:

with open(filename) as f:
   return len(list(f))

ceci est plus concis que votre boucle explicite et évite la variable enumerate.

3
Andrew Jaffe

count = max(enumerate(open(filename)))[0]

3
pyanon

J'ai modifié le cas de tampon comme ceci:

def CountLines(filename):
    f = open(filename)
    try:
        lines = 1
        buf_size = 1024 * 1024
        read_f = f.read # loop optimization
        buf = read_f(buf_size)

        # Empty file
        if not buf:
            return 0

        while buf:
            lines += buf.count('\n')
            buf = read_f(buf_size)

        return lines
    finally:
        f.close()

Désormais, les fichiers vides et la dernière ligne (sans\n) sont également comptés.

3
Dummy

Si on veut obtenir le nombre de lignes à moindre coût en Python sous Linux, je recommande cette méthode:

import os
print os.popen("wc -l file_path").readline().split()[0]

chemin_fichier peut être un chemin de fichier abstrait ou un chemin relatif. J'espère que cela peut aider.

2
lerner

Et ça

def file_len(fname):
  counts = itertools.count()
  with open(fname) as f: 
    for _ in f: counts.next()
  return counts.next()
2
odwl

Que diriez-vous de ce one-liner:

file_length = len(open('myfile.txt','r').read().split('\n'))

Prend 0,003 seconde en utilisant cette méthode pour chronométrer sur un fichier de ligne 3900

def c():
  import time
  s = time.time()
  file_length = len(open('myfile.txt','r').read().split('\n'))
  print time.time() - s
1
onetwopunch
def line_count(path):
    count = 0
    with open(path) as lines:
        for count, l in enumerate(lines, start=1):
            pass
    return count
1
mdwhatcott

Que dis-tu de ça?

import fileinput
import sys

counter=0
for line in fileinput.input([sys.argv[1]]):
    counter+=1

fileinput.close()
print counter
1
leba-lev

Si le fichier peut tenir dans la mémoire, alors

with open(fname) as f:
    count = len(f.read().split(b'\n')) - 1
0
Karthik

Si toutes les lignes de votre fichier ont la même longueur (et ne contiennent que des caractères ASCII) *, vous pouvez effectuer les tâches suivantes à moindre coût:

fileSize     = os.path.getsize( pathToFile )  # file size in bytes
bytesPerLine = someInteger                    # don't forget to account for the newline character
numLines     = fileSize // bytesPerLine

* Je suppose que davantage d’efforts seraient nécessaires pour déterminer le nombre d’octets dans une ligne si des caractères unicode tels que é sont utilisés.

0
Jet Blue
def count_text_file_lines(path):
    with open(path, 'rt') as file:
        line_count = sum(1 for _line in file)
    return line_count
0
jciloa

Une autre possibilité: 

import subprocess

def num_lines_in_file(fpath):
    return int(subprocess.check_output('wc -l %s' % fpath, Shell=True).strip().split()[0])
0
Jenny Yue Jin

Créez un fichier de script exécutable nommé count.py: 

#!/usr/bin/python

import sys
count = 0
for line in sys.stdin:
    count+=1

Ensuite, dirigez le contenu du fichier dans le script python: cat huge.txt | ./count.py. Pipe fonctionne également sur Powershell , vous aurez donc fini par compter le nombre de lignes.

Pour moi, sous Linux, il était 30% plus rapide que:

count=1
with open('huge.txt') as f:
    count+=1
0
0x90

Vous pouvez utiliser le module os.path de la manière suivante:

import os
import subprocess
Number_lines = int( (subprocess.Popen( 'wc -l {0}'.format( Filename ), Shell=True, stdout=subprocess.PIPE).stdout).readlines()[0].split()[0] )

, où Filename est le chemin absolu du fichier.

0
Victor