[python] 파이썬에서 큰 파일의 줄 수를 싸게 얻는 방법?

파이썬에서 큰 파일 (수만 줄)의 줄 수를 가져와야합니다. 메모리와 시간 단위로 가장 효율적인 방법은 무엇입니까?

현재 내가하는 일 :

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

더 잘 할 수 있습니까?



답변

당신은 그것보다 더 나아질 수 없습니다.

결국 모든 솔루션은 전체 파일을 읽어야합니다. \n 결과를 반환해야합니다.

전체 파일을 읽지 않고 더 좋은 방법이 있습니까? 확실하지 않습니다 … 최선의 솔루션은 항상 I / O 바인딩 일 것입니다. 최선의 방법은 불필요한 메모리를 사용하지 않는 것입니다.


답변

한 줄, 아마도 꽤 빠릅니다.

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


답변

메모리 매핑 파일이 가장 빠른 솔루션이라고 생각합니다. 나는 네 가지 기능을 시도했다 : OP ( opcount) 에 의해 게시 된 기능 ; 파일의 라인들에 대한 간단한 반복 ( simplecount); 메모리 매핑 된 파일 (mmap) ( mapcount) 이있는 판독 선 ; Mykola Kharechko에서 제공하는 버퍼 읽기 솔루션 (bufcount )에서 .

각 기능을 5 번 실행하고 120 만 줄 텍스트 파일의 평균 런타임을 계산했습니다.

Windows XP, Python 2.5, 2GB RAM, 2GHz AMD 프로세서

내 결과는 다음과 같습니다.

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

편집 : Python 2.6의 숫자 :

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

따라서 버퍼 읽기 전략은 Windows / Python 2.6에서 가장 빠른 것으로 보입니다.

코드는 다음과 같습니다.

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


답변

나는 나의 명성 점수가 약간 올라갈 때까지 비슷한 질문에 이것을 게시해야했다.

이러한 모든 솔루션은 버퍼링되지 않은 (raw) 인터페이스를 사용하고, 바이트 배열을 사용하고, 자체 버퍼링을 수행하여이 실행을 상당히 빠르게하는 한 가지 방법을 무시합니다. (이것은 Python 3에만 적용됩니다. Python 2에서는 기본 인터페이스가 기본적으로 사용되거나 사용되지 않을 수 있지만 Python 3에서는 기본적으로 유니 코드로 설정됩니다.)

수정 된 버전의 타이밍 도구를 사용하면 다음 코드가 제공되는 솔루션보다 빠릅니다 (그리고 조금 더 많은 파이썬).

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

별도의 생성기 기능을 사용하면 smidge가 더 빠르게 실행됩니다.

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 )

itertools를 사용하여 인라인으로 생성기 표현식을 사용하여 완전히 수행 할 수 있지만 꽤 이상하게 보입니다.

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 )

내 타이밍은 다음과 같습니다.

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


답변

하위 프로세스를 실행하고 실행할 수 있습니다. 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])


답변

다음은 멀티 프로세싱 라이브러리를 사용하여 머신 / 코어에 라인 수를 분배하는 파이썬 프로그램입니다. 내 테스트는 8 코어 Windows 64 서버를 사용하여 2 천만 줄 파일을 26 초에서 7 초로 세는 것을 향상시킵니다. 참고 : 메모리 매핑을 사용하지 않으면 작업 속도가 훨씬 느려집니다.

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


답변

현대적인 기능을 사용 하여이 답변 과 비슷한 한 줄 bash 솔루션 subprocess.check_output:

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