IT이야기

Python에서 대용량 파일의 라인 수를 저렴하게 얻는 방법

cyworld 2022. 4. 9. 09:09
반응형

Python에서 대용량 파일의 라인 수를 저렴하게 얻는 방법

나는 파이톤으로 큰 파일(수십만 줄)의 라인 카운트를 구해야 한다.기억력과 시간 면에서 가장 효율적인 방법은 무엇인가?

그 순간 나는 다음과 같이 한다.

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

더 잘 할 수 있을까?

한 줄, 아마 꽤 빠를 것이다.

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

이보다 더 좋을 수는 없다.

결국, 어떤 솔루션이든 전체 파일을 읽고 얼마나 많은 파일을 읽어야 하는지를 알아내야 한다.\n가지고 있고, 그 결과를 돌려준다.

파일 전체를 읽지 않고 그렇게 하는 더 좋은 방법이 있니?잘 모르겠는데...가장 좋은 해결책은 항상 I/O-bound일 것이며, 불필요한 메모리를 사용하지 않도록 하는 것이 최선일 것이다. 그러나 그것은 당신이 가지고 있는 것처럼 보인다.

나는 메모리 매핑 파일이 가장 빠른 해결책이 될 것이라고 믿는다.OP가 게시한 기능(OP가 게시한 기능)의 네 가지 기능을 시도해 보았다.opcount 반복 (; ); 파의 에에 반복 (.simplecount);메모리가 저장되는 파일(mmap)로 된 리드라인(readline)mapcount (;;; 미콜라 카레코 가 (Mykola Karechkoga)가 솔루션은 solution는버퍼퍼는는는는(((는는는(()이다.bufcount).

각 함수를 다섯 번 실행했고, 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))

평판점수가 조금 뛸 때까지 비슷한 질문에 이 글을 올려야 했다.

이러한 모든 솔루션은 이 실행 속도를 상당히 빠르게 하기 위한 한 가지 방법, 즉 버퍼링되지 않은 (원시) 인터페이스 사용, 바이테레이 사용 및 자체 버퍼링 수행 방법을 무시한다. (이것은 파이썬 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 )

이 작업은 iertools를 사용하여 인라인에 있는 생성자 표현으로 완전히 수행될 수 있지만, 매우 이상하게 보인다.

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개의 코어 윈도우 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 ) )

이 답변과 유사한 한 줄 바시 솔루션으로, 현대적인 것을 사용한다.subprocess.check_output함수:

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

Perflot 분석 후 버퍼링된 읽기 솔루션을 추천해야 함

def buf_count_newlines_gen(fname):
    def _make_gen(reader):
        while True:
            b = reader(2 ** 16)
            if not b: break
            yield b

    with open(fname, "rb") as f:
        count = sum(buf.count(b"\n") for buf in _make_gen(f.raw.read))
    return count

빠르고 기억력이 좋다.대부분의 다른 해결책들은 약 20배 느리다.

여기에 이미지 설명을 입력하십시오.


플롯을 재현하는 코드:

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

Python의 파일 오브젝트 메서드를 사용할 경우readlines, 다음과 같다.

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

이렇게 하면 파일이 열리고 파일에 줄 목록이 작성되며 목록의 길이를 세어 변수에 저장하고 파일을 다시 닫는다.

이것은 내가 순수한 비단뱀을 사용한 것 중에서 가장 빠른 것이다.내 컴퓨터에서는 2***16이 좋은 것 같지만, 버퍼 설정을 통해 원하는 메모리 양을 얼마든지 사용할 수 있다.

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

나는 여기서 답을 찾았다. 왜 Python보다 C++에서 stdin의 대사를 읽는 것이 더 느릴까? 그리고 그것을 아주 조금 수정했다.하지만 줄을 빨리 세는 방법을 이해하는 것은 매우 좋은 읽을거리다.wc -l다른 어떤 것보다 75% 더 빠를 겁니다

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

여기 내가 사용하는 것이 있는데, 꽤 깨끗해 보인다.

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: 이것은 순수한 비단뱀을 사용하는 것보다 약간 더 빠르지만 메모리 사용량에 대한 비용이다.하위 프로세스는 명령을 실행하는 동안 상위 프로세스와 동일한 메모리 풋프린트로 새 프로세스를 분기한다.

단일 라인 솔루션:

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

내 새끼:

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

아마도 가장 좋은 방법일 것이다, 이것을 위한 대안은

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

다음은 두 가지 모두의 성과에 대한 비교다.

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

이 버전에서는 메모리나 GC 오버헤드를 피하기 위해 일정한 버퍼를 다시 사용하는(4-8%)이 약간 개선되었다.

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

당신은 버퍼 크기를 가지고 놀 수 있고 약간의 향상을 볼 수 있다.

위의 방법을 완료하기 위해 파일 입력 모듈로 변형된 모델을 사용해 보십시오.

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

그리고 위에 언급된 모든 방법에 6천만 줄의 파일을 전달하였다.

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

파일입력이 그렇게 나쁘고 다른 모든 방법보다 훨씬 더 나쁜 규모라는 것은 내게는 좀 놀라운 일이다...

나로서는 이 변종이 가장 빠를 것이다.

#!/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()

이유: 한 줄씩 읽는 것보다 빠른 버퍼링string.count또한 매우 빠르다.

나는 버퍼 케이스를 다음과 같이 수정했다.

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

이제 빈 파일과 마지막 줄(\n 제외)도 계산된다.

이 코드는 더 짧고 선명하다.그게 아마 가장 좋은 방법일 거야

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

Linux의 Python에서 라인 카운트를 저렴하게 받고 싶다면 다음 방법을 추천한다.

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

file_path는 추상 파일 경로 또는 상대 경로일 수 있다.이것이 도움이 되기를 바란다.

이미 많은 해답이 있지만 불행히도 대부분은 거의 최적화할 수 없는 문제에 있는 작은 경제일 뿐이다...

라인 카운트가 소프트웨어의 핵심 기능인 몇 가지 프로젝트를 했고, 엄청난 수의 파일을 가지고 가능한 한 빨리 작업하는 것이 무엇보다 중요했다.

라인 개수가 있는 주된 병목현상은 I/O 액세스인데, 라인 리턴 문자를 감지하려면 각 라인을 읽어야 하기 때문에, 도저히 방법이 없다.두 번째 잠재적 병목현상은 메모리 관리다. 즉, 한 번에 로딩할수록 처리 속도가 빨라질 수 있지만, 이 병목현상은 첫 번째 병목현상에 비해 무시할 수 있다.

따라서 gc 수집 비활성화 및 기타 마이크로 매니징 트릭과 같은 작은 최적화 외에도 라인 카운트 기능의 처리 시간을 줄이는 3가지 주요 방법이 있다.

  1. 하드웨어 솔루션: 프로그래밍을 하지 않는 가장 확실한 방법은 매우 빠른 SSD/플래시 하드 드라이브를 구입하는 것이다.지금까지, 이것이 당신이 가장 큰 속도 상승을 얻을 수 있는 방법이다.

  2. 데이터 준비 솔루션: 처리 중인 파일이 생성되는 방식을 생성하거나 수정할 수 있거나 사전 처리할 수 있는 경우 먼저 라인 리턴을 유닉스 스타일로 변환하십시오(\n이렇게 하면 Windows 또는 MacOS 스타일에 비해 1자가 절약되며(큰 절약은 아니지만 쉽게 얻을 수 있음), 두 번째로 중요한 것은 고정된 길이의 줄을 쓸 수 있다는 것이다.가변 길이가 필요하면 항상 더 작은 선을 패딩할 수 있다.이렇게 하면 전체 파일 크기에서 즉시 줄 수를 계산할 수 있으며, 이 숫자는 훨씬 더 빨리 액세스할 수 있다.종종, 문제에 대한 최선의 해결책은 그것이 당신의 최종 목적에 더 잘 맞도록 그것을 미리 처리하는 것이다.

  3. 병렬화 + 하드웨어 솔루션: 여러 개의 하드 디스크(그리고 가능한 경우 SSD 플래시 디스크)를 구입할 수 있다면 병렬화를 활용함으로써 한 디스크의 속도를 뛰어넘어 디스크 간에 파일을 균형 있게 저장(전체 크기에 따라 균형을 맞추는 것이 가장 쉽다)한 다음 모든 디스크에서 병렬로 읽을 수 있다.그러면, 당신은 당신이 가지고 있는 디스크의 수에 비례하여 더 큰 증가를 기대할 수 있다.디스크를 여러 개 구입하는 것이 선택사항이 아니라면 병렬화는 도움이 되지 않을 것이다(일부 전문가급 디스크와 같은 판독 헤더가 여러 개 있지만, 그렇다고 하더라도 디스크의 내부 캐시 메모리와 PCB 회로는 병목 현상이 되어 모든 헤드를 완전히 병렬로 사용하지 못하게 할 뿐 아니라, 또한 고안해야 한다.정확한 클러스터 매핑을 알아야 다른 헤드의 클러스터에 파일을 저장할 수 있고, 이후 다른 헤드로 파일을 읽을 수 있기 때문에 이 하드 드라이브에 사용할 특정 코드).실제로 순차적 판독은 거의 항상 랜덤 읽기보다 빠르며, 단일 디스크에서 병렬화하면 순차적 판독보다 랜덤 읽기와 유사한 성능을 갖는다는 것은 널리 알려져 있다(예를 들어 CrystalDiskMark를 사용하여 두 가지 측면에서 모두 하드 드라이브 속도를 테스트할 수 있다).

만약 그것들 중 어느 것도 선택사항이 아니라면, 당신은 당신의 라인 카운트 기능의 몇 퍼센트 속도만큼 향상시키기 위해 마이크로 매니징 기술에 의존할 수 있지만, 정말로 중요한 것을 기대하지는 마십시오.오히려, 여러분은 여러분이 보게 될 속도 개선의 수익률에 비해 트위킹을 하는 데 걸리는 시간이 불균형적일 것이라고 예상할 수 있다.

간단한 방법:

1)

>>> f = len(open("myfile.txt").readlines())
>>> f

430
>>> f = open("myfile.txt").read().count('\n')
>>> f
430
>>>
num_lines = len(list(open('myfile.txt')))

파일을 여는 결과는 다음과 같은 길이를 가진 시퀀스로 변환할 수 있는 반복기입니다.

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

이것은 당신의 명시적인 루프보다 더 간결하고, 그리고 그것을 피한다enumerate.

이건 어때?

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

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

이건 어때?

import fileinput
import sys

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

fileinput.close()
print counter

이 원라이너 어떠세요?

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

이 방법을 사용하여 3900라인 파일에 시간을 설정하는 데 0.003초가 소요됨

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

참조URL: https://stackoverflow.com/questions/845058/how-to-get-line-count-of-a-large-file-cheaply-in-python

반응형