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?
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.
Une ligne, probablement assez rapide:
num_lines = sum(1 for line in open('myfile.txt'))
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))
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
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])
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 ) )
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.
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.
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.
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
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
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
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.
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.
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])
Ce code est plus court et plus clair. C'est probablement le meilleur moyen:
num_lines = open('yourfile.ext').read().count('\n')
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
print open('file.txt', 'r').read().count("\n") + 1
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
.
count = max(enumerate(open(filename)))[0]
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.
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.
Et ça
def file_len(fname):
counts = itertools.count()
with open(fname) as f:
for _ in f: counts.next()
return counts.next()
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
def line_count(path):
count = 0
with open(path) as lines:
for count, l in enumerate(lines, start=1):
pass
return count
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
Si le fichier peut tenir dans la mémoire, alors
with open(fname) as f:
count = len(f.read().split(b'\n')) - 1
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.
def count_text_file_lines(path):
with open(path, 'rt') as file:
line_count = sum(1 for _line in file)
return line_count
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])
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
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.