텍스트 파일의 줄 수를 효율적으로 계산합니다. (200MB 이상)


88

내 스크립트에서 치명적인 오류가 발생한다는 사실을 방금 발견했습니다.

Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 440 bytes) in C:\process_txt.php on line 109

그 라인은 다음과 같습니다.

$lines = count(file($path)) - 1;

그래서 파일을 메모리에로드하고 줄 수를 세는 데 어려움이 있다고 생각하는데, 메모리 문제없이이 작업을 수행 할 수있는 더 효율적인 방법이 있습니까?

2MB에서 500MB까지의 줄 수를 계산하는 데 필요한 텍스트 파일입니다. 가끔 공연 일 수도 있습니다.

도움을 주셔서 감사합니다.

답변:


161

전체 파일을 메모리에로드하지 않기 때문에 메모리를 덜 사용합니다.

$file="largefile.txt";
$linecount = 0;
$handle = fopen($file, "r");
while(!feof($handle)){
  $line = fgets($handle);
  $linecount++;
}

fclose($handle);

echo $linecount;

fgets한 줄을 메모리에로드합니다 (두 번째 인수 $length가 생략되면 우리가 원하는 줄의 끝에 도달 할 때까지 스트림에서 계속 읽습니다). 벽 시간과 메모리 사용량에 관심이 있다면 PHP가 아닌 다른 것을 사용하는 것만 큼 빠르지 않을 것입니다.

이것의 유일한 위험은 어떤 줄이 특히 긴 경우입니다 (줄 바꿈이없는 2GB 파일을 발견하면 어떻게 될까요?). 어떤 경우에는 덩어리로 슬러 핑하고 줄 끝 문자를 세는 것이 좋습니다.

$file="largefile.txt";
$linecount = 0;
$handle = fopen($file, "r");
while(!feof($handle)){
  $line = fgets($handle, 4096);
  $linecount = $linecount + substr_count($line, PHP_EOL);
}

fclose($handle);

echo $linecount;

5
완벽하지 않음 : \nWindows 시스템 ( PHP_EOL == '\r\n') 에서 유닉스 스타일 파일 ( )을 구문 분석 할 수 있습니다.
nickf

1
줄 읽기를 1로 제한하여 조금 개선하지 않겠습니까? 줄 수만 계산하고 싶기 때문에 fgets($handle, 1);?
Cyril N.

1
@CyrilN. 이것은 설정에 따라 다릅니다. 한 줄에 몇 개의 문자 만 포함 된 파일이 대부분이라면을 사용할 필요가 없기 때문에 더 빠를 수 substr_count()있지만, 줄이 너무 길면 호출해야 while()하며 fgets()훨씬 더 많은 단점이 있습니다. 잊지 마세요 : fgets() 한 줄씩 읽지 않습니다. 그것은 단지 당신을 통해 정의 된 문자의 양을 읽어 $length하고 있다면 그것은 LINEBREAK가 포함되어 무엇이든 정지 $length된 세트가 있습니다.
mgutt

3
이것은 줄 수보다 1을 더 많이 반환하지 않습니까? while(!feof())EOF 표시기는 파일 끝에서 읽으려고 할 때까지 설정되지 않기 때문에 추가 행을 읽게됩니다.
Barmar 2015-04-29

1
첫 번째 예에서 @DominicRodger는 사용되지 않기 때문에 $line = fgets($handle);될 수 있다고 생각 합니다. fgets($handle);$line
Pocketsand 2010 년

107

fgets()호출 루프를 사용하는 것이 좋은 솔루션이며 작성하기 가장 간단합니다.

  1. 내부적으로 8192 바이트의 버퍼를 사용하여 파일을 읽더라도 코드는 여전히 각 행에 대해 해당 함수를 호출해야합니다.

  2. 바이너리 파일을 읽는 경우 기술적으로 한 줄이 사용 가능한 메모리보다 클 수 있습니다.

이 코드는 각각 8kB의 청크 단위로 파일을 읽은 다음 해당 청크 내의 줄 바꿈 수를 계산합니다.

function getLines($file)
{
    $f = fopen($file, 'rb');
    $lines = 0;

    while (!feof($f)) {
        $lines += substr_count(fread($f, 8192), "\n");
    }

    fclose($f);

    return $lines;
}

각 라인의 평균 길이가 최대 4kB이면 이미 함수 호출에 대한 저장을 시작하고 큰 파일을 처리 할 때 추가 될 수 있습니다.

기준

1GB 파일로 테스트를 실행했습니다. 결과는 다음과 같습니다.

             +-------------+------------------+---------+
             | This answer | Dominic's answer | wc -l   |
+------------+-------------+------------------+---------+
| Lines      | 3550388     | 3550389          | 3550388 |
+------------+-------------+------------------+---------+
| Runtime    | 1.055       | 4.297            | 0.587   |
+------------+-------------+------------------+---------+

시간은 실시간으로 초 단위로 측정됩니다. 여기에서 실제 의미를 확인 하세요.


버퍼 크기를 64k와 같이 확장하면 얼마나 빨라질 지 궁금합니다 (?). 추신 : 만약 php 만이 경우에 IO를 비동기로 만드는 쉬운 방법 이 있다면
zerkms

@zerkms 귀하의 질문에 답하기 위해 64kB 버퍼를 사용하면 1GB에서 0.2 초 더 빨라집니다. :)
Ja͢ck

3
이 벤치 마크에주의하세요. 어떤 것을 먼저 실행 했나요? 두 번째 파일은 이미 디스크 캐시에있는 파일의 이점을 가지므로 결과가 크게 왜곡됩니다.
Oliver Charlesworth 2014-08-28

6
@OliCharlesworth 그들은 첫 번째 실행을 건너 뛰고 5 회 이상 평균입니다. :)
Ja͢ck

1
이 대답은 훌륭합니다! 그러나 IMO는 마지막 줄에 문자가있을 때 줄 수에 1을 더하기 위해 테스트해야합니다. pastebin.com/yLwZqPR2
caligari

48

단순 지향 객체 솔루션

$file = new \SplFileObject('file.extension');

while($file->valid()) $file->fgets();

var_dump($file->key());

최신 정보

이것을 만드는 또 다른 방법은 PHP_INT_MAXin SplFileObject::seekmethod입니다.

$file = new \SplFileObject('file.extension', 'r');
$file->seek(PHP_INT_MAX);

echo $file->key() + 1; 

3
두 번째 솔루션은 훌륭하고 Spl을 사용합니다! 감사.
Daniele Orlando

2
감사합니다 ! 이것은 참으로 훌륭합니다. wc -l특히 작은 파일에서 호출하는 것보다 빠릅니다 (내가 생각하는 포크 ​​때문에).
Drasill

솔루션이 그렇게 도움이 될 것이라고 생각하지 않았습니다!
Wallace Maxters

2
이것은 지금까지 최고의 솔루션입니다
Valdrinium

1
"key () + 1"이 맞습니까? 나는 그것을 시도하고 잘못된 것 같습니다. 마지막 줄을 포함하여 모든 줄에 줄 끝이있는 주어진 파일에 대해이 코드는 3998을 제공합니다.하지만 "wc"를 입력하면 3997이됩니다. "vim"을 사용하면 3997L이라고 표시됩니다 (누락을 나타내지 않음). EOL). 그래서 "업데이트"대답이 틀렸다고 생각합니다.
user9645

37

Linux / Unix 호스트에서 exec()이를 실행하는 경우 가장 쉬운 솔루션은 명령 을 사용 하거나 이와 유사한 명령을 실행하는 것 wc -l $path입니다. $path"/ path / to / file; rm -rf /"와 같은 것이 아닌지 확인하기 위해 먼저 삭제 했는지 확인하십시오.


나는 윈도우 머신에있다! 만약 그렇다면 그게 최선의 해결책이라고 생각합니다!
Abs

23
@ ghostdog74 : 네, 맞아요. 휴대가 불가능합니다. 그래서 나는 "만약 당신이 이것을 리눅스 / 유닉스 호스트에서 실행하고 있다면 ..."이라는 절을 앞에 붙여서 내 제안의 이식성이 없다는 것을 명시 적으로 인정했다.
Dave Sherohman

1
이식 할 수는 없지만 (일부 상황에서는 유용하지만) exec (또는 shell_exec 또는 시스템)는 시스템 호출로 PHP 내장 함수에 비해 상당히 느립니다.
Manz 2012

10
@Manz : 왜, 네, 맞아요. 휴대가 불가능합니다. 그래서 나는 "만약 당신이 이것을 리눅스 / 유닉스 호스트에서 실행하고 있다면 ..."이라는 절을 앞에 붙여서 내 제안의 이식성이 없다는 것을 명시 적으로 인정했다.
Dave Sherohman

@DaveSherohman 네, 맞아요, 죄송합니다. IMHO, 가장 중요한 문제는 시스템 호출에 시간이 많이 걸린다고 생각합니다 (특히 자주 사용해야하는 경우)
Manz

32

전체 파일을 반복 할 필요가없는 더 빠른 방법이 있습니다.

* nix 시스템에서만 Windows에서도 비슷한 방법이있을 수 있습니다.

$file = '/path/to/your.file';

//Get number of lines
$totalLines = intval(exec("wc -l '$file'"));

"No such file or directory"를 억제하려면 2> / dev / null 추가
Tegan Snyder 2013 년

$ total_lines = intval (exec ( "wc -l '$ file'")); 공백이있는 파일 이름을 처리합니다.
pgee70

감사 pgee70이 아직 건너하지만 의미가하지 않았다, 나는 내 대답 업데이트
앤디 브라만

6
exec('wc -l '.escapeshellarg($file).' 2>/dev/null')
Zheng Kai

위의 @DaveSherohman의 답변이 3 년 전에 게시 된 것 같습니다
e2-e4

8

PHP 5.5를 사용하는 경우 생성기를 사용할 수 있습니다 . 이것은 5.5 이전의 PHP 버전에서는 작동 하지 않습니다 . php.net에서 :

"Generator는 Iterator 인터페이스를 구현하는 클래스를 구현하는 오버 헤드 나 복잡성없이 간단한 반복기를 구현하는 쉬운 방법을 제공합니다."

// This function implements a generator to load individual lines of a large file
function getLines($file) {
    $f = fopen($file, 'r');

    // read each line of the file without loading the whole file to memory
    while ($line = fgets($f)) {
        yield $line;
    }
}

// Since generators implement simple iterators, I can quickly count the number
// of lines using the iterator_count() function.
$file = '/path/to/file.txt';
$lineCount = iterator_count(getLines($file)); // the number of lines in the file

5
try/는 finally당신을 위해 PHP가 자동으로 닫힙니다 파일, 엄격하게 필요하지 않습니다. 실제 계산은 다음을 사용하여 수행 할 수 있음을 언급해야합니다. iterator_count(getFiles($file)):)
NikiC

7

이것은 Wallace de Souza의 솔루션에 추가되었습니다.

또한 계산하는 동안 빈 줄을 건너 뜁니다.

function getLines($file)
{
    $file = new \SplFileObject($file, 'r');
    $file->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY | 
SplFileObject::DROP_NEW_LINE);
    $file->seek(PHP_INT_MAX);

    return $file->key() + 1; 
}

6

Linux를 사용하는 경우 다음을 수행 할 수 있습니다.

number_of_lines = intval(trim(shell_exec("wc -l ".$file_name." | awk '{print $1}'")));

다른 OS를 사용하는 경우 올바른 명령을 찾아야합니다.

문안 인사


1
private static function lineCount($file) {
    $linecount = 0;
    $handle = fopen($file, "r");
    while(!feof($handle)){
        if (fgets($handle) !== false) {
                $linecount++;
        }
    }
    fclose($handle);
    return  $linecount;     
}

위의 기능에 약간의 수정을 추가하고 싶었습니다.

'테스트'라는 단어가 포함 된 파일이있는 특정 예에서 함수가 결과로 2를 반환했습니다. 그래서 fgets가 false를 반환했는지 여부를 확인해야했습니다. :)

재미있다 :)


1

줄 수는 다음 코드로 계산할 수 있습니다.

<?php
$fp= fopen("myfile.txt", "r");
$count=0;
while($line = fgetss($fp)) // fgetss() is used to get a line from a file ignoring html tags
$count++;
echo "Total number of lines  are ".$count;
fclose($fp);
?>

0

몇 가지 옵션이 있습니다. 첫 번째는 허용되는 사용 가능한 메모리를 늘리는 것입니다. 이는 파일이 매우 커질 수 있다는 점을 고려할 때 작업을 수행하는 가장 좋은 방법이 아닐 수 있습니다. 다른 방법은 fgets 를 사용 하여 한 줄씩 파일을 읽고 카운터를 증가시키는 것입니다. 이는 한 번에 현재 줄만 메모리에 있으므로 메모리 문제를 전혀 일으키지 않아야합니다.


0

이 목록에 좋은 추가가 될 것이라고 생각한 또 다른 답변이 있습니다.

perlPHP의 쉘을 설치하고 실행할 수 있는 경우 :

$lines = exec('perl -pe \'s/\r\n|\n|\r/\n/g\' ' . escapeshellarg('largetextfile.txt') . ' | wc -l');

이것은 Unix 또는 Windows 생성 파일에서 대부분의 줄 바꿈을 처리해야합니다.

두 가지 단점 (적어도) :

1) 스크립트가 실행중인 시스템에 따라 달라지는 것은 좋은 생각이 아닙니다 (Perl과 wc를 사용할 수 있다고 가정하는 것은 안전하지 않을 수 있습니다).

2) 이스케이프에서 작은 실수로 컴퓨터의 셸에 대한 액세스 권한을 넘겼습니다.

코딩에 대해 알고있는 (또는 알고 있다고 생각하는) 대부분의 것과 마찬가지로 다른 곳에서이 정보를 얻었습니다.

존 리브 Article


0
public function quickAndDirtyLineCounter()
{
    echo "<table>";
    $folders = ['C:\wamp\www\qa\abcfolder\',
    ];
    foreach ($folders as $folder) {
        $files = scandir($folder);
        foreach ($files as $file) {
            if($file == '.' || $file == '..' || !file_exists($folder.'\\'.$file)){
                continue;
            }
                $handle = fopen($folder.'/'.$file, "r");
                $linecount = 0;
                while(!feof($handle)){
                    if(is_bool($handle)){break;}
                    $line = fgets($handle);
                    $linecount++;
                  }
                fclose($handle);
                echo "<tr><td>" . $folder . "</td><td>" . $file . "</td><td>" . $linecount . "</td></tr>";
            }
        }
        echo "</table>";
}

5
OP에 대해 설명하는 단어를 최소한 몇 개 추가하고 더 많은 독자가 원래 질문에 대한 이유와 방법에 대해 답해주세요.
β.εηοιτ.βε

0

dominic Rodger의 솔루션을 기반으로 여기에 내가 사용하는 것이 있습니다 (사용 가능한 경우 wc를 사용하고 그렇지 않으면 dominic Rodger의 솔루션으로 대체합니다).

class FileTool
{

    public static function getNbLines($file)
    {
        $linecount = 0;

        $m = exec('which wc');
        if ('' !== $m) {
            $cmd = 'wc -l < "' . str_replace('"', '\\"', $file) . '"';
            $n = exec($cmd);
            return (int)$n + 1;
        }


        $handle = fopen($file, "r");
        while (!feof($handle)) {
            $line = fgets($handle);
            $linecount++;
        }
        fclose($handle);
        return $linecount;
    }
}

https://github.com/lingtalfi/Bat/blob/master/FileTool.php


0

이 방법을 사용하여 순전히 파일의 줄 수를 계산합니다. 이 구절의 단점은 다른 답변입니다. 두 줄 솔루션과 반대로 많은 줄이 표시됩니다. 아무도 이것을하지 않는 이유가 있다고 생각합니다.

$lines = count(file('your.file'));
echo $lines;

원래 해결책은 이것입니다. 그러나 file ()은 전체 파일을 메모리에로드하기 때문에 이것은 원래 문제 (메모리 고갈)이기도하므로 문제에 대한 해결책이 아닙니다.
Tuim

0

한 번에 한 줄만 버퍼링하는 가장 간결한 크로스 플랫폼 솔루션입니다.

$file = new \SplFileObject(__FILE__);
$file->setFlags($file::READ_AHEAD);
$lines = iterator_count($file);

불행히도 READ_AHEAD플래그 를 설정해야 iterator_count합니다. 그렇지 않으면 무기한 차단됩니다. 그렇지 않으면 한 줄짜리가됩니다.


-1

선을 계산하려면 다음을 사용하십시오.

$handle = fopen("file","r");
static $b = 0;
while($a = fgets($handle)) {
    $b++;
}
echo $b;
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.