Menghitung jumlah baris file teks secara efisien. (200mb +)

90

Saya baru saja mengetahui bahwa skrip saya memberikan kesalahan fatal:

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

Baris itu adalah ini:

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

Jadi saya pikir itu mengalami kesulitan memuat file ke dalam memeory dan menghitung jumlah baris, adakah cara yang lebih efisien untuk melakukan ini tanpa masalah memori?

File teks yang saya butuhkan untuk menghitung jumlah baris dari 2MB hingga 500MB. Mungkin terkadang Gig.

Terima kasih semua atas bantuannya.

Abs
sumber

Jawaban:

162

Ini akan menggunakan lebih sedikit memori, karena tidak memuat seluruh file ke dalam memori:

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

fclose($handle);

echo $linecount;

fgetsmemuat satu baris ke dalam memori (jika argumen kedua $lengthdihilangkan, argumen tersebut akan terus membaca dari aliran hingga mencapai akhir baris, yang kita inginkan). Ini masih tidak mungkin secepat menggunakan sesuatu selain PHP, jika Anda peduli dengan waktu dinding serta penggunaan memori.

Satu-satunya bahaya dengan ini adalah jika ada baris yang sangat panjang (bagaimana jika Anda menemukan file 2GB tanpa jeda baris?). Dalam hal ini Anda lebih baik melakukan menyeruputnya dalam potongan-potongan, dan menghitung karakter akhir baris:

$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;
Dominic Rodger
sumber
5
tidak sempurna: Anda dapat memiliki file bergaya unix ( \n) yang diurai pada mesin windows ( PHP_EOL == '\r\n')
nickf
1
Mengapa tidak meningkatkan sedikit dengan membatasi pembacaan baris menjadi 1? Karena kita hanya ingin menghitung jumlah baris, kenapa tidak dilakukan fgets($handle, 1);?
Cyril N.
1
@Tokopedia Ini tergantung pada pengaturan Anda. Jika Anda memiliki sebagian besar file yang hanya berisi beberapa karakter per baris, ini bisa lebih cepat karena Anda tidak perlu menggunakansubstr_count() , tetapi jika Anda memiliki antrean yang sangat panjang, Anda perlu memanggil while()dan fgets()banyak lagi yang menyebabkan kerugian. Jangan lupa: fgets() tidak dibaca baris demi baris. Ini hanya membaca jumlah karakter yang Anda tentukan $lengthdan jika berisi pemutusan baris ia berhenti apa pun yang $lengthtelah ditetapkan.
mgutt
3
Bukankah ini akan mengembalikan 1 lebih banyak dari jumlah baris? while(!feof())akan menyebabkan Anda membaca baris tambahan, karena indikator EOF tidak disetel sampai Anda mencoba membaca di akhir file.
Barmar
1
@DominicRodger di contoh pertama saya percaya $line = fgets($handle); bisa saja fgets($handle);karena $linetidak pernah digunakan.
Pockets dan
109

Menggunakan loop fgets()panggilan adalah solusi yang bagus dan paling mudah untuk ditulis, namun:

  1. meskipun secara internal file dibaca menggunakan buffer sebesar 8192 byte, kode Anda masih harus memanggil fungsi tersebut untuk setiap baris.

  2. secara teknis mungkin satu baris mungkin lebih besar dari memori yang tersedia jika Anda membaca file biner.

Kode ini membaca file dalam potongan masing-masing 8kB dan kemudian menghitung jumlah baris baru dalam potongan itu.

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

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

    fclose($f);

    return $lines;
}

Jika rata-rata panjang setiap baris paling banyak 4kB, Anda sudah mulai menghemat pemanggilan fungsi, dan itu dapat bertambah saat Anda memproses file besar.

Tolok ukur

Saya menjalankan tes dengan file 1GB; berikut hasilnya:

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

Waktu diukur dalam detik waktu nyata, lihat sini apa arti sebenarnya

Mendongkrak
sumber
Penasaran seberapa cepat (?) Jadinya jika Anda memperluas ukuran buffer ke sesuatu seperti 64k. PS: seandainya php memiliki cara mudah untuk membuat IO asynchronous dalam kasus ini
zerkms
@zerkms Untuk menjawab pertanyaan Anda, dengan buffer 64kB, ini menjadi 0,2 detik lebih cepat pada 1GB :)
Ja͢ck
3
Hati-hati dengan benchmark ini, mana yang lebih dulu dijalankan? Yang kedua akan mendapatkan keuntungan dari file yang sudah ada di cache disk, hasilnya sangat miring.
Oliver Charlesworth
7
@OliCharlesworth rata-rata mereka berlari lebih dari lima kali, melewatkan putaran pertama :)
Ja͢ck
1
Jawaban ini bagus! Namun, IMO, itu harus menguji ketika ada beberapa karakter di baris terakhir untuk menambahkan 1 dalam jumlah baris: pastebin.com/yLwZqPR2
caligari
50

Solusi Objek Berorientasi Sederhana

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

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

var_dump($file->key());

Memperbarui

Cara lain untuk membuatnya adalah dengan metode PHP_INT_MAXin SplFileObject::seek.

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

echo $file->key() + 1; 
Wallace Maxters
sumber
3
Solusi kedua bagus dan menggunakan Spl! Terima kasih.
Daniele Orlando
2
Terima kasih ! Ini memang bagus. Dan lebih cepat daripada menelepon wc -l(karena forking saya kira), terutama pada file kecil.
Drasill
1
Solusi luar biasa!
Dalibor Karlović
2
Ini adalah solusi terbaik sejauh ini
Valdrinium
1
Apakah "key () + 1" sudah benar? Saya mencobanya dan sepertinya salah. Untuk file tertentu dengan akhiran baris pada setiap baris termasuk yang terakhir, kode ini memberi saya 3998. Tetapi jika saya melakukan "wc" di atasnya, saya mendapatkan 3997. Jika saya menggunakan "vim", dikatakan 3997L (dan tidak menunjukkan hilang EOL). Jadi saya pikir jawaban "Perbarui" salah.
pengguna9645
37

Jika Anda menjalankan ini di host Linux / Unix, solusi termudah adalah menggunakan exec()atau serupa dengan menjalankan perintah wc -l $path. Pastikan Anda telah membersihkannya $pathterlebih dahulu untuk memastikan bahwa itu bukan sesuatu seperti "/ path / to / file; rm -rf /".

Dave Sherohman
sumber
Saya menggunakan mesin windows! Jika ya, saya pikir itu akan menjadi solusi terbaik!
Abs
25
@ ghostdog74: Ya, Anda benar. Ini tidak portabel. Itulah mengapa saya secara eksplisit mengakui saran saya yang tidak dapat dibawa-bawa dengan mengawali dengan klausa "Jika Anda menjalankan ini di host Linux / Unix ...".
Dave Sherohman
1
Non portabel (meskipun berguna dalam beberapa situasi), tetapi exec (atau shell_exec atau sistem) adalah panggilan sistem, yang jauh lebih lambat dibandingkan dengan fungsi built-in PHP.
Manz
11
@Manz: Ya, Anda benar. Ini tidak portabel. Itulah mengapa saya secara eksplisit mengakui saran saya yang tidak dapat dibawa-bawa dengan mengawali dengan klausa "Jika Anda menjalankan ini di host Linux / Unix ...".
Dave Sherohman
@DaveSherohman Ya, Anda benar, maaf. IMHO, menurut saya masalah yang paling penting adalah memakan waktu dalam panggilan sistem (terutama jika Anda perlu sering menggunakannya)
Manz
32

Ada cara yang lebih cepat yang saya temukan yang tidak memerlukan perulangan melalui seluruh file

hanya di sistem * nix , mungkin ada cara serupa di windows ...

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

//Get number of lines
$totalLines = intval(exec("wc -l '$file'"));
Andy Braham
sumber
tambahkan 2> / dev / null untuk menyembunyikan "Tidak ada file atau direktori seperti itu"
Tegan Snyder
$ total_lines = intval (exec ("wc -l '$ file'")); akan menangani nama file dengan spasi.
pgee70
Terima kasih pgee70 belum menemukan itu tetapi masuk akal, saya memperbarui jawaban saya
Andy Braham
6
exec('wc -l '.escapeshellarg($file).' 2>/dev/null')
Zheng Kai
Sepertinya jawaban @DaveSherohman di atas diposting 3 tahun sebelumnya
e2-e4
8

Jika Anda menggunakan PHP 5.5, Anda dapat menggunakan generator . Ini TIDAK akan berfungsi dalam versi PHP apa pun sebelum 5.5. Dari php.net:

"Generator menyediakan cara mudah untuk mengimplementasikan iterator sederhana tanpa overhead atau kerumitan implementasi kelas yang mengimplementasikan antarmuka 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
Ben Harold
sumber
5
The try/ finallytidak benar-benar diperlukan, PHP secara otomatis akan menutup file untuk Anda. Anda mungkin juga harus menyebutkan bahwa penghitungan sebenarnya dapat dilakukan dengan menggunakan iterator_count(getFiles($file)):)
NikiC
7

Ini adalah tambahan untuk solusi Wallace de Souza

Itu juga melewatkan baris kosong saat menghitung:

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; 
}
Jani
sumber
6

Jika Anda menggunakan linux, Anda dapat melakukan:

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

Anda hanya perlu menemukan perintah yang tepat jika Anda menggunakan OS lain

Salam

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

Saya ingin menambahkan sedikit perbaikan pada fungsi di atas ...

dalam contoh spesifik di mana saya memiliki file yang berisi kata 'pengujian' fungsi mengembalikan 2 sebagai hasilnya. jadi saya perlu menambahkan centang apakah widget dikembalikan salah atau tidak :)

Selamat bersenang-senang :)

ufk
sumber
1

Berdasarkan solusi dominic Rodger, inilah yang saya gunakan (menggunakan wc jika tersedia, jika tidak, mundur ke solusi 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

ling
sumber
1

Menghitung jumlah baris dapat dilakukan dengan kode-kode berikut:

<?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);
?>
Santosh Kumar
sumber
0

Anda punya beberapa pilihan. Yang pertama adalah meningkatkan memori yang tersedia, yang mungkin bukan cara terbaik untuk melakukan hal-hal mengingat Anda menyatakan bahwa file bisa menjadi sangat besar. Cara lain adalah dengan menggunakan widget untuk membaca file baris demi baris dan menambah penghitung, yang seharusnya tidak menyebabkan masalah memori sama sekali karena hanya baris saat ini yang ada di memori pada satu waktu.

Yacoby
sumber
0

Ada jawaban lain yang menurut saya bisa menjadi tambahan yang bagus untuk daftar ini.

Jika Anda telah perlmenginstal dan dapat menjalankan sesuatu dari shell di PHP:

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

Ini harus menangani sebagian besar jeda baris baik dari Unix atau file yang dibuat Windows.

DUA kelemahan (setidaknya):

1) Bukan ide bagus untuk memiliki skrip Anda sehingga bergantung pada sistem yang dijalankannya (mungkin tidak aman untuk menganggap Perl dan wc tersedia)

2) Hanya kesalahan kecil dalam melarikan diri dan Anda telah menyerahkan akses ke shell di komputer Anda.

Seperti kebanyakan hal yang saya ketahui (atau menurut saya) tentang pengkodean, saya mendapatkan info ini dari tempat lain:

Artikel John Reeve

Douglas.Sesar
sumber
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>";
}
Yogi Sadhwani
sumber
5
Harap pertimbangkan untuk menambahkan setidaknya beberapa kata yang menjelaskan OP dan kepada pembaca lebih lanjut dari Anda menjawab mengapa dan bagaimana itu menjawab pertanyaan asli.
β.εηοιτ.βε
0

Saya menggunakan metode ini untuk menghitung berapa banyak baris dalam sebuah file. Apa sisi negatif dari melakukan ayat-ayat ini dengan jawaban yang lain. Saya melihat banyak baris sebagai lawan dari solusi dua baris saya. Saya menduga ada alasan mengapa tidak ada yang melakukan ini.

$lines = count(file('your.file'));
echo $lines;
kaspirtk1
sumber
Solusi aslinya adalah ini. Tetapi karena file () memuat seluruh file dalam memori, ini juga merupakan masalah asli (kehabisan memori) jadi tidak, ini bukan solusi untuk pertanyaan tersebut.
Tuim
0

Solusi lintas platform paling ringkas yang hanya menyangga satu baris dalam satu waktu.

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

Sayangnya, kami harus mengatur READ_AHEADbendera jika tidak iterator_countmemblokir tanpa batas. Jika tidak, ini akan menjadi satu baris.

Pertanyaan Quolonel
sumber
-1

Untuk hanya menghitung garis gunakan:

$handle = fopen("file","r");
static $b = 0;
while($a = fgets($handle)) {
    $b++;
}
echo $b;
Adeel Ahmad
sumber