Đếm số dòng của tệp văn bản một cách hiệu quả. (200mb +)


88

Tôi vừa phát hiện ra rằng tập lệnh của tôi gây ra lỗi nghiêm trọng:

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

Dòng đó là:

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

Vì vậy, tôi nghĩ rằng nó đang gặp khó khăn khi tải tệp vào memeory và đếm số dòng, có cách nào hiệu quả hơn để tôi có thể làm điều này mà không gặp vấn đề về bộ nhớ không?

Các tệp văn bản mà tôi cần đếm số dòng cho phạm vi từ 2MB đến 500MB. Có thể là một Gig đôi khi.

Cảm ơn tất cả cho bất kỳ sự giúp đỡ.

Câu trả lời:


161

Điều này sẽ sử dụng ít bộ nhớ hơn, vì nó không tải toàn bộ tệp vào bộ nhớ:

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

fclose($handle);

echo $linecount;

fgetstải một dòng đơn vào bộ nhớ (nếu đối số thứ hai $lengthbị bỏ qua, nó sẽ tiếp tục đọc từ luồng cho đến khi nó đến cuối dòng, đó là những gì chúng ta muốn). Điều này vẫn chưa chắc sẽ nhanh bằng việc sử dụng thứ gì đó khác ngoài PHP, nếu bạn quan tâm đến thời gian tường cũng như mức sử dụng bộ nhớ.

Nguy hiểm duy nhất với điều này là nếu bất kỳ dòng nào đặc biệt dài (nếu bạn gặp phải tệp 2GB không có ngắt dòng thì sao?). Trong trường hợp đó, tốt hơn là bạn nên chia nhỏ nó thành nhiều phần và đếm các ký tự cuối dòng:

$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
không hoàn hảo: bạn có thể có một tập tin unix-style ( \n) đang được phân tích trên một máy cửa sổ ( PHP_EOL == '\r\n')
nickf

1
Tại sao không cải thiện một chút bằng cách giới hạn đọc dòng xuống 1? Vì chúng ta chỉ muốn đếm số dòng, tại sao không thực hiện a fgets($handle, 1);?
Cyril N.

1
@CyrilN. Điều này phụ thuộc vào thiết lập của bạn. Nếu bạn đang có hầu hết các tệp chỉ chứa một số ký tự trên mỗi dòng thì có thể nhanh hơn vì bạn không cần sử dụng substr_count(), nhưng nếu bạn đang có các dòng rất dài, bạn cần gọi while()fgets()nhiều hơn nữa gây ra bất lợi. Đừng quên: fgets() không đọc từng dòng. Nó chỉ đọc số lượng ký tự mà bạn đã xác định $lengthnếu nó chứa dấu ngắt dòng, nó sẽ dừng bất cứ thứ gì $lengthđã được đặt.
mgutt,

3
Điều này sẽ không trả về 1 nhiều hơn số dòng phải không? while(!feof())sẽ khiến bạn đọc thêm một dòng, vì chỉ báo EOF không được đặt cho đến khi bạn cố gắng đọc ở cuối tệp.
Barmar

1
@DominicRodger trong ví dụ đầu tiên mà tôi tin rằng $line = fgets($handle);có thể là fgets($handle);do $linekhông bao giờ được sử dụng.
Pocketsand

107

fgets()Tuy nhiên, sử dụng một vòng lặp các cuộc gọi là giải pháp tốt và dễ viết nhất:

  1. mặc dù bên trong tệp được đọc bằng cách sử dụng bộ đệm 8192 byte, mã của bạn vẫn phải gọi hàm đó cho mỗi dòng.

  2. Về mặt kỹ thuật, có thể một dòng đơn có thể lớn hơn bộ nhớ khả dụng nếu bạn đang đọc tệp nhị phân.

Mã này đọc một tệp theo từng đoạn 8kB và sau đó đếm số dòng mới trong đoạn đó.

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

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

    fclose($f);

    return $lines;
}

Nếu độ dài trung bình của mỗi dòng tối đa là 4kB, bạn sẽ bắt đầu lưu các lệnh gọi hàm và chúng có thể cộng dồn khi bạn xử lý các tệp lớn.

Điểm chuẩn

Tôi đã chạy thử nghiệm với tệp 1GB; đây là kết quả:

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

Thời gian được đo bằng giây thời gian thực, hãy xem ở đây nghĩa là gì


Tò mò rằng nó sẽ nhanh hơn (?) Như thế nào nếu bạn mở rộng kích thước bộ đệm lên một cái gì đó như 64k. PS: nếu chỉ php đã có một số dễ dàng cách để làm cho IO không đồng bộ trong trường hợp này
zerkms

@zerkms Để trả lời câu hỏi của bạn, với bộ đệm 64kB, nó sẽ nhanh hơn 0,2 giây trên 1GB :)
Ja͢ck 13/1213

3
Hãy cẩn thận với điểm chuẩn này, bạn đã chạy cái nào trước? Cái thứ hai sẽ có lợi ích là tệp đã có trong bộ nhớ cache của đĩa, làm sai lệch kết quả hàng loạt.
Oliver Charlesworth

6
@OliCharlesworth họ trung bình hơn năm lần chạy, bỏ qua lần chạy đầu tiên :)
Ja͢ck

1
Câu trả lời này là tuyệt vời! Tuy nhiên, IMO, nó phải kiểm tra khi có một số nhân vật trong dòng cuối cùng để thêm 1 trong số dòng: pastebin.com/yLwZqPR2
Caligari

48

Giải pháp Đối tượng Định hướng Đơn giản

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

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

var_dump($file->key());

Cập nhật

Một cách khác để thực hiện điều này là với PHP_INT_MAXtrong SplFileObject::seekphương pháp.

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

echo $file->key() + 1; 

3
Giải pháp thứ hai là tuyệt vời và sử dụng Spl! Cảm ơn.
Daniele Orlando

2
Cảm ơn bạn ! Đây quả thực là tuyệt vời. Và nhanh hơn so với việc gọi điện wc -l(vì tôi cho là có sự phân tách), đặc biệt là trên các tệp nhỏ.
Drasill

Tôi không nghĩ rằng giải pháp sẽ hữu ích như vậy!
Wallace Maxters

2
Đây là giải pháp tốt nhất cho đến nay
Valdrinium

1
"Key () + 1" có đúng không? Tôi đã thử nó và có vẻ sai. Đối với một tệp nhất định có kết thúc dòng trên mọi dòng bao gồm cả dòng cuối cùng, mã này cho tôi 3998. Nhưng nếu tôi nhập "wc" trên đó, tôi nhận được 3997. Nếu tôi sử dụng "vim", nó cho biết 3997L (và không cho biết bị thiếu EOL). Vì vậy, tôi nghĩ rằng câu trả lời "Cập nhật" là sai.
user9645 ngày

37

Nếu bạn đang chạy điều này trên máy chủ Linux / Unix, giải pháp dễ nhất là sử dụng exec()hoặc tương tự để chạy lệnh wc -l $path. Chỉ cần đảm bảo rằng bạn đã làm sạch $pathtrước để chắc chắn rằng nó không phải là một cái gì đó giống như "/ path / to / file; rm -rf /".


Tôi đang sử dụng máy tính windows! Nếu là tôi, tôi nghĩ đó sẽ là giải pháp tốt nhất!
Abs

24
@ ghostdog74: Tại sao, vâng, bạn nói đúng. Nó là không di động. Đó là lý do tại sao tôi xác nhận rõ ràng tính không di động của đề xuất của mình bằng cách đặt trước nó bằng mệnh đề "Nếu bạn đang chạy điều này trên máy chủ Linux / Unix ...".
Dave Sherohman

1
Không di động (mặc dù hữu ích trong một số trường hợp), nhưng thực thi (hoặc shell_exec hoặc hệ thống) là một lệnh gọi hệ thống, chậm hơn đáng kể so với các hàm tích hợp sẵn trong PHP.
Manz

11
@Manz: Tại sao, vâng, bạn nói đúng. Nó là không di động. Đó là lý do tại sao tôi xác nhận rõ ràng tính không di động của đề xuất của mình bằng cách đặt trước nó bằng mệnh đề "Nếu bạn đang chạy điều này trên máy chủ Linux / Unix ...".
Dave Sherohman

@DaveSherohman Vâng, bạn nói đúng, xin lỗi. IMHO, tôi nghĩ rằng vấn đề quan trọng nhất là tiêu thụ trong một cuộc gọi hệ thống (đặc biệt nếu bạn cần phải sử dụng thường xuyên) thời gian
Manz

32

Có một cách nhanh hơn mà tôi đã tìm thấy mà không yêu cầu lặp lại toàn bộ tệp

chỉ trên hệ thống * nix , có thể có cách tương tự trên windows ...

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

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

thêm 2> / dev / null để chặn thông báo "Không có tệp hoặc thư mục như vậy"
Tegan Snyder

$ total_lines = intval (thi hành ("wc -l '$ file'")); sẽ xử lý các tên tệp có khoảng trắng.
pgee70

Cảm ơn pgee70 không đi qua mà chưa nhưng có ý nghĩa, tôi cập nhật câu trả lời của tôi
Andy Braham

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

Có vẻ như câu trả lời của @DaveSherohman ở trên được đăng 3 năm trước câu trả lời này
e2-e4

8

Nếu bạn đang sử dụng PHP 5.5, bạn có thể sử dụng trình tạo . Điều này sẽ KHÔNG hoạt động trong bất kỳ phiên bản PHP nào trước 5.5. Từ php.net:

"Trình tạo cung cấp một cách dễ dàng để triển khai các trình vòng lặp đơn giản mà không cần chi phí cao hoặc phức tạp khi triển khai một lớp thực hiện giao diện 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
Dấu try/ finallykhông hoàn toàn cần thiết, PHP sẽ tự động đóng tệp cho bạn. Bạn nên có lẽ cũng đề cập rằng đếm thực tế có thể được thực hiện bằng iterator_count(getFiles($file)):)
NikiC

7

Đây là một bổ sung cho giải pháp của Wallace de Souza

Nó cũng bỏ qua các dòng trống trong khi đếm:

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

Nếu bạn đang sử dụng Linux, bạn có thể chỉ cần làm:

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

Bạn chỉ cần tìm đúng lệnh nếu đang sử dụng hệ điều hành khác

Trân trọng


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

Tôi muốn thêm một chút sửa chữa cho hàm trên ...

trong một ví dụ cụ thể, nơi tôi có một tệp chứa từ 'testing', kết quả là hàm trả về 2. vì vậy tôi cần phải thêm một kiểm tra xem fgets có trả về false hay không :)

chúc vui vẻ :)


1

Việc đếm số dòng có thể được thực hiện bằng các mã sau:

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

Bạn có một số lựa chọn. Đầu tiên là tăng bộ nhớ khả dụng cho phép, đây có lẽ không phải là cách tốt nhất để thực hiện những việc mà bạn cho rằng tệp có thể rất lớn. Một cách khác là sử dụng fgets để đọc các dòng tập tin bằng cách dòng và tăng số đếm, mà không nên gây ra bất kỳ vấn đề bộ nhớ tại tất cả như chỉ dòng hiện tại là trong bộ nhớ cùng một lúc.


0

Có một câu trả lời khác mà tôi nghĩ có thể là một bổ sung tốt cho danh sách này.

Nếu bạn đã perlcài đặt và có thể chạy mọi thứ từ shell trong PHP:

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

Điều này sẽ xử lý hầu hết các ngắt dòng cho dù từ tệp Unix hay Windows được tạo.

HAI nhược điểm (ít nhất):

1) Không phải là một ý tưởng tuyệt vời nếu tập lệnh của bạn quá phụ thuộc vào hệ thống đang chạy (có thể không an toàn nếu cho rằng có Perl và wc)

2) Chỉ cần một sai sót nhỏ trong việc thoát và bạn đã trao quyền truy cập vào một shell trên máy của mình.

Như với hầu hết những điều tôi biết (hoặc nghĩ rằng tôi biết) về mã hóa, tôi lấy thông tin này từ một nơi khác:

John Reeve Bài báo


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
Vui lòng xem xét thêm ít nhất một số từ giải thích cho OP và để các độc giả khác của bạn trả lời tại sao và cách nó trả lời câu hỏi ban đầu.
β.εηοιτ.βε

0

Dựa trên giải pháp của Rodger dominic, đây là những gì tôi sử dụng (nó sử dụng wc nếu có, nếu không sẽ là dự phòng cho giải pháp của Rodger dominic).

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

Tôi sử dụng phương pháp này để đếm hoàn toàn có bao nhiêu dòng trong một tệp. Nhược điểm của việc làm câu này là gì câu kia trả lời. Tôi thấy nhiều dòng trái ngược với giải pháp hai dòng của tôi. Tôi đoán có một lý do không ai làm điều này.

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

Giải pháp ban đầu là này. Nhưng vì tệp () tải toàn bộ tệp trong bộ nhớ, đây cũng là vấn đề ban đầu (cạn kiệt bộ nhớ) nên không, đây không phải là giải pháp cho câu hỏi.
Tuim

0

Giải pháp đa nền tảng ngắn gọn nhất chỉ đệm một dòng tại một thời điểm.

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

Thật không may, chúng tôi phải đặt READ_AHEADcờ nếu không sẽ iterator_countchặn vô thời hạn. Nếu không, đây sẽ là một lớp lót.


-1

Để chỉ đếm các dòng, hãy sử dụng:

$handle = fopen("file","r");
static $b = 0;
while($a = fgets($handle)) {
    $b++;
}
echo $b;
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.