Hoe krijg ik het aantal regels van een groot bestand goedkoop in Python?

Ik heb een aantal regels nodig van een groot bestand (honderdduizenden regels) in python. Wat is de meest efficiënte manier, zowel qua geheugen als qua tijd?

Op dit moment doe ik:

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

kan het nog beter?


Antwoord 1, autoriteit 100%

Je kunt niet beter worden dan dat.

Per slot van rekening moet elke oplossing het hele bestand lezen, uitzoeken hoeveel \nje hebt, en dat resultaat teruggeven.

Kunt u dat beter doen zonder het hele bestand te lezen? Ik weet het niet zeker… De beste oplossing zal altijd I/O-gebonden zijn, het beste wat je kunt doen is ervoor zorgen dat je geen onnodig geheugen gebruikt, maar het lijkt erop dat je dat hebt gedekt.


Antwoord 2, autoriteit 93%

Eén regel, waarschijnlijk behoorlijk snel:

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

Antwoord 3, autoriteit 54%

Ik geloof dat een geheugen toegewezen bestand de snelste oplossing zal zijn. Ik heb vier functies geprobeerd: de functie gepost door de OP (opcount); een eenvoudige iteratie over de regels in het bestand (simplecount); leesregel met een in het geheugen toegewezen bestand (mmap) (mapcount); en de bufferleesoplossing aangeboden door Mykola Kharechko (bufcount).

Ik heb elke functie vijf keer uitgevoerd en de gemiddelde looptijd berekend voor een tekstbestand van 1,2 miljoen regels.

Windows XP, Python 2.5, 2 GB RAM, 2 GHz AMD-processor

Dit zijn mijn resultaten:

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

Bewerken: getallen voor Python 2.6:

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

Dus de bufferleesstrategie lijkt de snelste te zijn voor Windows/Python 2.6

Hier is de 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))

Antwoord 4, autoriteit 42%

Ik moest dit op een vergelijkbare vraag posten totdat mijn reputatiescore een beetje steeg (dankzij degene die me tegen het lijf liep!).

Al deze oplossingen negeren een manier om deze functie aanzienlijk sneller te maken, namelijk met behulp van de onbevalde (RAW) -interface, met behulp van bytearrays en het doen van uw eigen buffering. (Dit geldt alleen in Python 3. In Python 2 kan de RAW-interface al dan niet standaard worden gebruikt, maar in Python 3 wordt u standaard in Unicode.)

Een gewijzigde versie van de timingtool gebruiken, ik ben van mening dat de volgende code sneller is (en marginaal meer pythonic) dan een van de aangeboden oplossingen:

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

Met behulp van een afzonderlijke generatorfunctie wordt dit een SMISTE sneller uitgevoerd:

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 )

Dit kan volledig worden gedaan met generatorenuitdrukkingen in-line met behulp van itertools, maar het wordt behoorlijk raar op zoek:

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 )

Hier zijn mijn 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

5, Autoriteit 11%

Hier is een Python-programma om de multiprocessing-bibliotheek te gebruiken om de lijntelling te verspreiden over machines / kernen. Mijn test verbetert het tellen van een 20 miljoen lijnbestand van 26 seconden tot 7 seconden met behulp van een 8-kern Windows 64-server. Opmerking: niet met behulp van geheugenmapping maakt dingen veel langzamer.

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 __name__ == '__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 ) )

6, Autoriteit 7%

een one-line bash oplossing vergelijkbaar met dit antwoord , met behulp van de moderne subprocess.check_outputFunctie:

def line_count(filename):
    return int(subprocess.check_output(['wc', '-l', filename]).split()[0])

7, Autoriteit 4%

Ik zou Python’s File Object-methode gebruiken readlines, als volgt:

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

Hiermee wordt het bestand geopend, zorgt voor een lijst met regels in het bestand, telt de lengte van de lijst, slaat dat op een variabele op en sluit het bestand opnieuw.


8, Autoriteit 3%

Dit is het snelste wat ik heb gevonden met pure python.
U kunt elke hoeveelheid geheugen gebruiken die u wilt door buffer in te stellen, hoewel 2 ** 16 een zoete plek op mijn computer lijkt.

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), ''))

Ik vond het antwoord hier Waarom is leeslijnen van Stdin veel langzamer in C++ dan Python? en tweaked het slechts een klein beetje. Het is een zeer goed gelezen om te begrijpen hoe de lijnen snel kunnen tellen, hoewel wc -lis nog steeds ongeveer 75% sneller dan iets anders.


9, Autoriteit 3%

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

10, Autoriteit 3%

Hier is wat ik gebruik, lijkt vrij schoon:

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: dit is marginaal sneller dan het gebruik van pure python, maar ten koste van het geheugengebruik. Subprocessen vult een nieuw proces met dezelfde geheugenvoetafdruk als ouderproces terwijl het uw opdracht uitvoert.


11, Autoriteit 2%

One Line-oplossing:

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

Mijn fragment:

>>> os.system('wc -l *.txt')
0 bar.txt
1000 command.txt
3 test_file.txt
1003 total

12, Autoriteit 2%

Kyle’s antwoord

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

is waarschijnlijk het beste, een alternatief hiervoor is

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

Hier is de vergelijking van de prestaties van zowel

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

13, Autoriteit 2%

Ik heb een kleine verbetering (4-8%) met deze versie die een constante buffer opnieuw gebruikt, dus het zou elk geheugen of GC overhead moeten vermijden:

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

Je kunt rond spelen met de buffergrootte en misschien een beetje verbetering zien.


14

Om de bovenstaande methoden te voltooien, probeerde ik een variant met de FILEInput-module:

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

en slaagde een 60 MIL-lijnenbestand aan aan alle hierboven vermelde methoden:

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

Het is een beetje verrassing voor mij dat FileGress zo slecht is en verer erger is dan alle andere methoden …


15

Voor mij is deze variant de snelste:

#!/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 __name__ == '__main__':
    main()

redenen: sneller bufferen dan regel voor regel lezen en string.countis ook erg snel


Antwoord 16

Ik heb de buffercase als volgt aangepast:

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

Nu worden ook lege bestanden en de laatste regel (zonder \n) geteld.


Antwoord 17

Deze code is korter en duidelijker. Het is waarschijnlijk de beste manier:

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

Antwoord 18

Als je het aantal regels goedkoop wilt krijgen in Python in Linux, raad ik deze methode aan:

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

bestandspad kan zowel een abstract bestandspad als een relatief pad zijn. Ik hoop dat dit kan helpen.


Antwoord 19

Na een perfplot analyse, moet men de gebufferde leesoplossing aanbevelen

def buf_count_newlines_gen(fname):
    def _make_gen(reader):
        b = reader(2 ** 16)
        while b:
            yield b
            b = reader(2 ** 16)
    with open(fname, "rb") as f:
        count = sum(buf.count(b"\n") for buf in _make_gen(f.raw.read))
    return count

Het is snel en geheugen-efficiënt. De meeste andere oplossingen zijn ongeveer 20 keer langzamer. Het UNIX-opdrachtregelgereedschap wcdoet ook een goede baan, maar het voordeel is zeer gering en de oproep overhead rechtvaardigt het alleen voor enkele miljoenen lijnen.


code om het perceel te reproduceren:

import mmap
import subprocess
from functools import partial
import perfplot
def setup(n):
    fname = "t.txt"
    with open(fname, "w") as f:
        for i in range(n):
            f.write(str(i) + "\n")
    return fname
def for_enumerate(fname):
    i = 0
    with open(fname) as f:
        for i, _ in enumerate(f):
            pass
    return i + 1
def sum1(fname):
    return sum(1 for _ in open(fname))
def mmap_count(fname):
    with open(fname, "r+") as f:
        buf = mmap.mmap(f.fileno(), 0)
    lines = 0
    while buf.readline():
        lines += 1
    return lines
def for_open(fname):
    lines = 0
    for _ in open(fname):
        lines += 1
    return lines
def buf_count_newlines(fname):
    lines = 0
    buf_size = 2 ** 16
    with open(fname) as f:
        buf = f.read(buf_size)
        while buf:
            lines += buf.count("\n")
            buf = f.read(buf_size)
    return lines
def buf_count_newlines_gen(fname):
    def _make_gen(reader):
        b = reader(2 ** 16)
        while b:
            yield b
            b = reader(2 ** 16)
    with open(fname, "rb") as f:
        count = sum(buf.count(b"\n") for buf in _make_gen(f.raw.read))
    return count
def wc_l(fname):
    return int(subprocess.check_output(["wc", "-l", fname]).split()[0])
def sum_partial(fname):
    with open(fname) as f:
        count = sum(x.count("\n") for x in iter(partial(f.read, 2 ** 16), ""))
    return count
def read_count(fname):
    return open(fname).read().count("\n")
b = perfplot.bench(
    setup=setup,
    kernels=[
        for_enumerate,
        sum1,
        mmap_count,
        for_open,
        wc_l,
        buf_count_newlines,
        buf_count_newlines_gen,
        sum_partial,
        read_count,
    ],
    n_range=[2 ** k for k in range(27)],
    xlabel="num lines",
)
b.save("out.png")
b.show()

Antwoord 20

het resultaat van het openen van een bestand is een iterator, die kan worden geconverteerd naar een reeks met een lengte:

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

dit is beknopter dan uw expliciete lus en vermijdt de enumerate.


Antwoord 21

En dit

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

Antwoord 22

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


Antwoord 23

Wat dacht je hiervan?

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

Antwoord 24

Wat dacht je van deze oneliner:

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

Het duurt 0,003 sec met deze methode om het te timen op een bestand van 3900 regels

def c():
  import time
  s = time.time()
  file_length = len(open('myfile.txt','r').read().split('\n'))
  print time.time() - s

Antwoord 25

print open('file.txt', 'r').read().count("\n") + 1

Antwoord 26

def line_count(path):
    count = 0
    with open(path) as lines:
        for count, l in enumerate(lines, start=1):
            pass
    return count

Antwoord 27

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

Antwoord 28

Dit is een meta-opmerking bij enkele van de andere antwoorden.

  • De regels voor het lezen en gebufferde \n-teltechnieken geven niet voor elk bestand hetzelfde antwoord, omdat sommige tekstbestanden geen nieuwe regel hebben aan het einde van de laatste regel. U kunt dit omzeilen door de laatste byte van de laatste niet-lege buffer te controleren en 1 toe te voegen als deze niet b'\n'is.

  • in Python 3, het openen van het bestand in de tekstmodus en in binaire modus kan verschillende resultaten opleveren, omdat de tekstmodus standaard CR-, LF, en CRLF als lijnuiteinden herkent (die ze allemaal converteren naar '\n'), terwijl alleen LF en CRLF in binaire modus worden meegeteld als u het b'\n'telt. Dit geldt of u leest door lijnen of in een buffer met vaste grootte. Het klassieke Mac OS gebruikte CR als een lijn die eindigt; Ik weet niet hoe vaak die bestanden deze dagen zijn.

  • De buffer-lees-aanpak gebruikt een begrensde hoeveelheid RAM-onafhankelijk van de bestandsgrootte, terwijl de line-lees-aanpak het volledige bestand in één keer in het ergste geval in RAM zou kunnen lezen (vooral als het bestand CR-lijn-eindes gebruikt ). In het ergste geval kan het aanzienlijk meer RAM gebruiken dan de bestandsgrootte, vanwege overhead van dynamische grootte van de lijnbuffer en (als u in de tekstmodus geopend) Unicode-decodering en opslag.

  • U kunt het geheugengebruik verbeteren, en waarschijnlijk de snelheid, van de gebufferde aanpak door het vooraf toe te wijzen aan een bytearray en het gebruik van readintoin plaats van read. Een van de bestaande antwoorden (met weinig stemmen) doet dit, maar het is buggy (het dubbele-telt enkele bytes).

  • Het bovenste antwoord van de bufferlezing gebruikt een grote buffer (1 MIB). Het gebruik van een kleinere buffer kan eigenlijk sneller zijn vanwege OS Readahead. Als u 32K of 64K tegelijk leest, begint het OS waarschijnlijk de volgende 32K / 64K in de cache te lezen voordat u erom vraagt, en elke reis naar de kernel zal bijna onmiddellijk terugkeren. Als u 1 MIB per keer leest, is het OS onwaarschijnlijk dat het een hele megabyte speculatief heeft gelezen. Het kan een kleiner bedrag voorgaan, maar u zult nog steeds een aanzienlijke hoeveelheid tijd doorbrengen in de kernel die op de schijf wacht om de rest van de gegevens te retourneren.


29

Veel antwoorden al, maar helaas zijn de meeste slechts kleine economieën op een nauwelijks te optimaliseren probleem…

Ik heb aan verschillende projecten gewerkt waarbij het aantal regels de kernfunctie van de software was, en het zo snel mogelijk werken met een groot aantal bestanden van het allergrootste belang was.

Het belangrijkste knelpunt bij het aantal regels is I/O-toegang, aangezien u elke regel moet lezen om het regelretourteken te detecteren, er is gewoon geen manier om hier omheen te komen. Het tweede potentiële knelpunt is geheugenbeheer: hoe meer u tegelijk laadt, hoe sneller u kunt verwerken, maar dit knelpunt is verwaarloosbaar in vergelijking met het eerste.

Daarom zijn er drie belangrijke manieren om de verwerkingstijd van een functie voor het tellen van regels te verminderen, afgezien van kleine optimalisaties zoals het uitschakelen van gc-verzameling en andere trucs voor microbeheer:

  1. Hardware-oplossing:de belangrijkste en meest voor de hand liggende manier is niet-programmatisch: koop een zeer snelle SSD/flash-harde schijf. Dit is hoe je verreweg de grootste snelheidsboosts kunt krijgen.

  2. Oplossing voor gegevensvoorbereiding:als u de bestanden die u verwerkt genereert of kunt wijzigen, of als het acceptabel is dat u ze vooraf kunt verwerken, converteert u eerst de regel return naar unix stijl (\n) omdat dit 1 teken bespaart in vergelijking met Windows- of MacOS-stijlen (geen grote besparing, maar het is een gemakkelijke winst), en ten tweede en het belangrijkste is dat u mogelijk regels met een vaste lengte kunt schrijven . Als u een variabele lengte nodig heeft, kunt u altijd kleinere lijnen opvullen. Op deze manier kunt u direct het aantal regels berekenen uit de totale bestandsgrootte, wat veel sneller toegankelijk is. Vaak is de beste oplossing voor een probleem om het vooraf te verwerken, zodat het beter past bij uw einddoel.

  3. Parallisatie + hardware-oplossing:als u meerdere harde schijven (en indien mogelijk SSD-flashschijven) kunt kopen, kunt u zelfs de snelheid van één schijf overschrijden door gebruik te maken van parallellisatie, door uw bestanden op een gebalanceerde manier opslaan (het gemakkelijkst is om te balanceren op totale grootte) tussen schijven, en dan parallel lezen van al die schijven. Dan kun je een vermenigvuldigingsboost verwachten in verhouding tot het aantal schijven dat je hebt. Als het kopen van meerdere schijven geen optie voor je is, zal parallellisatie waarschijnlijk niet helpen (behalve als je schijf meerdere leeskoppen heeft zoals sommige professionele schijven, maar zelfs dan zullen het interne cachegeheugen van de schijf en het PCB-circuit waarschijnlijk een knelpunt zijn en voorkomen dat u alle koppen parallel volledig gebruikt, plus dat u een specifieke code moet bedenken voor deze harde schijf die u zult gebruiken, omdat u de exacte clustertoewijzing moet weten, zodat u uw bestanden op clusters onder verschillende koppen opslaat, en zo dat je ze daarna met verschillende koppen kunt lezen). Het is inderdaad algemeen bekend dat sequentiële lezing bijna altijd sneller is dan willekeurige lezing, en parallellisatie op een enkele schijf zal een prestatie hebben die meer lijkt op willekeurig lezen dan sequentiële lezing (u kunt de snelheid van uw harde schijf in beide aspecten testen met CrystalDiskMark bijvoorbeeld) .

Als geen van deze opties een optie is, kun je alleen vertrouwen op micro-managing-trucs om de snelheid van je lijntelfunctie met een paar procent te verbeteren, maar verwacht niet echt iets significants. U kunt eerder verwachten dat de tijd die u besteedt aan tweaken niet in verhouding staat tot het rendement dat u zult zien in snelheidsverbetering.

Other episodes