นับจำนวนบรรทัดของไฟล์ข้อความได้อย่างมีประสิทธิภาพ (200mb +)


90

ฉันเพิ่งพบว่าสคริปต์ของฉันทำให้ฉันมีข้อผิดพลาดร้ายแรง:

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;

ฉันคิดว่ามันมีปัญหาในการโหลดไฟล์ลงใน memeory และการนับจำนวนบรรทัดมีวิธีที่มีประสิทธิภาพมากกว่านี้ไหมที่ฉันสามารถทำได้โดยไม่ต้องมีปัญหาเรื่องหน่วยความจำ

ไฟล์ข้อความที่ฉันต้องนับจำนวนบรรทัดสำหรับช่วงตั้งแต่ 2MB ถึง 500MB บางทีกิ๊ก

ขอบคุณสำหรับความช่วยเหลือใด ๆ

คำตอบ:


162

สิ่งนี้จะใช้หน่วยความจำน้อยลงเนื่องจากไม่ได้โหลดทั้งไฟล์ลงในหน่วยความจำ:

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

fclose($handle);

echo $linecount;

fgetsโหลดบรรทัดเดียวลงในหน่วยความจำ (ถ้าอาร์กิวเมนต์ที่สอง$lengthถูกละไว้มันจะอ่านต่อจากสตรีมจนกว่าจะถึงจุดสิ้นสุดของบรรทัดซึ่งเป็นสิ่งที่เราต้องการ) การดำเนินการนี้ยังไม่น่าจะรวดเร็วเท่ากับการใช้งานอย่างอื่นนอกเหนือจาก PHP หากคุณสนใจเรื่องเวลาติดผนังและการใช้หน่วยความจำ

อันตรายเพียงอย่างเดียวคือหากบรรทัดใด ๆ ยาวเป็นพิเศษ (จะเกิดอะไรขึ้นถ้าคุณพบไฟล์ 2GB โดยไม่มีการแบ่งบรรทัด?) ในกรณีนี้คุณจะดีกว่าในการทำ slurping เป็นชิ้น ๆ และการนับอักขระท้ายบรรทัด:

$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
ไม่สมบูรณ์แบบ: คุณสามารถมีไฟล์สไตล์ยูนิกซ์ ( \n) ถูกแยกวิเคราะห์บนเครื่อง windows ( PHP_EOL == '\r\n')
nickf

1
ทำไมไม่ปรับปรุงเล็กน้อยโดย จำกัด การอ่านบรรทัดไว้ที่ 1? เนื่องจากเราต้องการนับจำนวนบรรทัดเท่านั้นทำไมไม่ทำ a fgets($handle, 1);?
Cyril N.

1
@CyrilN. ขึ้นอยู่กับการตั้งค่าของคุณ หากคุณมีไฟล์ส่วนใหญ่ที่มีเพียงบางตัวอักษรต่อบรรทัดอาจเร็วกว่าเพราะคุณไม่จำเป็นต้องใช้substr_count()แต่ถ้าคุณมีสายที่ยาวมากคุณต้องโทรwhile()และfgets()อื่น ๆ อีกมากมายที่ทำให้เสียเปรียบ อย่าลืม: fgets()อย่าอ่านทีละบรรทัด มันอ่านเฉพาะจำนวนอักขระที่คุณกำหนดไว้$lengthและหากมีการแบ่งบรรทัดมันจะหยุดสิ่งที่$lengthตั้งไว้
mgutt

3
จะไม่ส่งคืน 1 มากกว่าจำนวนบรรทัดหรือไม่? while(!feof())จะทำให้คุณต้องอ่านบรรทัดพิเศษเนื่องจากไม่ได้ตั้งค่าตัวบ่งชี้ EOF จนกว่าคุณจะพยายามอ่านในตอนท้ายของไฟล์
Barmar

1
@DominicRodger ในตัวอย่างแรกฉันเชื่อว่า$line = fgets($handle);อาจเป็นfgets($handle);เพราะ$lineไม่เคยใช้
Pocketsand

109

การใช้การ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 PS: ถ้า php มีวิธีง่ายๆในการทำให้ IO asynchronous ในกรณีนี้
zerkms

@zerkms เพื่อตอบคำถามของคุณด้วยบัฟเฟอร์ 64kB มันจะเร็วขึ้น 0.2 วินาทีบน 1GB :)
Ja͢ck

3
โปรดใช้ความระมัดระวังกับเกณฑ์มาตรฐานนี้ซึ่งคุณวิ่งก่อน อันที่สองจะได้รับประโยชน์จากไฟล์ที่อยู่ในดิสก์แคชอยู่แล้วซึ่งจะทำให้ผลลัพธ์บิดเบี้ยวอย่างมาก
Oliver Charlesworth

7
@OliCharlesworth พวกเขาเฉลี่ยมากกว่าห้าวิ่งข้ามการวิ่งครั้งแรก :)
Ja͢ck

1
คำตอบนี้ดีมาก! อย่างไรก็ตาม IMO จะต้องทดสอบเมื่อมีอักขระบางตัวในบรรทัดสุดท้ายเพื่อเพิ่ม 1 ในจำนวนบรรทัด: pastebin.com/yLwZqPR2
caligari

50

โซลูชันเชิงวัตถุอย่างง่าย

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

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

var_dump($file->key());

อัปเดต

วิธีที่จะทำให้นี้ก็คือด้วยPHP_INT_MAXในSplFileObject::seekวิธีการ

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

echo $file->key() + 1; 

3
วิธีที่สองนั้นยอดเยี่ยมและใช้ Spl! ขอบคุณ.
Daniele Orlando

2
ขอขอบคุณ ! นี่เป็นเรื่องที่ยอดเยี่ยมจริงๆ และเร็วกว่าการโทรwc -l(เพราะฉันคิดว่าฟอร์ก) โดยเฉพาะไฟล์ขนาดเล็ก
Drasill

1
ทางออกที่ยอดเยี่ยม!
Dalibor Karlović

2
นี่เป็นทางออกที่ดีที่สุด
Valdrinium

1
"คีย์ () + 1" ถูกต้องหรือไม่? ฉันลองแล้วดูเหมือนจะผิด สำหรับไฟล์หนึ่ง ๆ ที่มีการลงท้ายบรรทัดทุกบรรทัดรวมถึงไฟล์สุดท้ายรหัสนี้จะให้ 3998 แต่ถ้าฉันทำ "wc" กับมันฉันจะได้ 3997 ถ้าฉันใช้ "เป็นกลุ่ม" มันจะระบุว่า 3997L (และไม่ได้ระบุว่าหายไป EOL) ดังนั้นฉันคิดว่าคำตอบ "อัปเดต" ไม่ถูกต้อง
user9645

37

หากคุณกำลังเรียกใช้สิ่งนี้บนโฮสต์ Linux / Unix วิธีแก้ปัญหาที่ง่ายที่สุดคือใช้exec()หรือคล้ายกับการรันคำสั่งwc -l $pathหรือคล้ายกันที่จะเรียกใช้คำสั่งเพียงตรวจสอบให้แน่ใจว่าคุณได้รับการฆ่าเชื้อ$pathก่อนเพื่อให้แน่ใจว่าไม่ใช่ "/ path / to / file; rm -rf /"


ฉันอยู่บนเครื่อง windows! ถ้าเป็นฉันคิดว่านั่นจะเป็นทางออกที่ดีที่สุด!
Abs

25
@ ghostdog74: ทำไมใช่คุณพูดถูก ไม่ใช่แบบพกพา นั่นเป็นเหตุผลที่ฉันยอมรับข้อเสนอแนะอย่างชัดเจนว่าไม่สามารถพกพาได้โดยนำหน้าด้วยประโยค "หากคุณกำลังเรียกใช้สิ่งนี้บนโฮสต์ Linux / Unix ... "
Dave Sherohman

1
ไม่พกพา (แม้ว่าจะมีประโยชน์ในบางสถานการณ์) แต่ exec (หรือ shell_exec หรือ system) เป็นการเรียกระบบซึ่งช้ากว่ามากเมื่อเทียบกับฟังก์ชันในตัวของ PHP
Manz

11
@Manz: ทำไมใช่คุณพูดถูก ไม่ใช่แบบพกพา นั่นเป็นเหตุผลที่ฉันยอมรับข้อเสนอแนะอย่างชัดเจนว่าไม่สามารถพกพาได้โดยนำหน้าด้วยประโยค "หากคุณกำลังเรียกใช้สิ่งนี้บนโฮสต์ Linux / Unix ... "
Dave Sherohman

@DaveSherohman ใช่คุณพูดถูกขอโทษ IMHO ฉันคิดว่าปัญหาที่สำคัญที่สุดคือการใช้เวลานานในการโทรระบบ (โดยเฉพาะอย่างยิ่งถ้าคุณจำเป็นต้องใช้บ่อยๆ)
Manz

32

มีวิธีที่เร็วกว่าที่ฉันพบโดยไม่ต้องวนซ้ำทั้งไฟล์

เฉพาะในระบบ * nixอาจมีวิธีที่คล้ายกันบน windows ...

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

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

เพิ่ม 2> / dev / null เพื่อระงับ "ไม่มีไฟล์หรือไดเรกทอรีดังกล่าว"
Tegan Snyder

$ total_lines = intval (exec ("wc -l '$ file'")); จะจัดการชื่อไฟล์ที่มีช่องว่าง
pgee70

ขอบคุณ pgee70 ยังไม่เจอ แต่ก็สมเหตุสมผลฉันอัปเดตคำตอบของฉัน
Andy Braham

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

ดูเหมือนคำตอบโดย @DaveSherohman ด้านบนโพสต์เมื่อ 3 ปีก่อนหน้านี้
e2-e4

8

หากคุณกำลังใช้ PHP 5.5 คุณสามารถใช้เครื่องกำเนิดไฟฟ้า สิ่งนี้จะใช้ไม่ได้กับ PHP เวอร์ชันใด ๆ ก่อนหน้า 5.5 จาก php.net:

"Generators ให้วิธีง่ายๆในการใช้งานตัววนซ้ำแบบง่ายโดยไม่ต้องใช้ค่าใช้จ่ายหรือความซับซ้อนในการใช้คลาสที่ใช้อินเทอร์เฟซ 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}'")));

คุณต้องหาคำสั่งที่ถูกต้องหากคุณใช้ระบบปฏิบัติการอื่น

ความนับถือ


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 ส่งคืนเป็นเท็จหรือไม่ :)

มีความสุข :)


1

จากการแก้ปัญหาของโดมินิกร็อดเจอร์นี่คือสิ่งที่ฉันใช้ (ใช้ wc ถ้ามีมิฉะนั้นจะเป็นทางเลือกในการแก้ปัญหาของโดมินิกร็อดเจอร์)

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


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

มีคำตอบอื่นที่ฉันคิดว่าอาจเป็นส่วนเสริมที่ดีในรายการนี้

หากคุณperlติดตั้งและสามารถเรียกใช้สิ่งต่างๆจากเชลล์ใน PHP:

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

สิ่งนี้ควรจัดการกับการแบ่งบรรทัดส่วนใหญ่ไม่ว่าจะเป็นไฟล์ที่สร้างจาก Unix หรือ Windows

ข้อเสียสองประการ (อย่างน้อย):

1) ไม่ใช่ความคิดที่ดีที่จะให้สคริปต์ของคุณขึ้นอยู่กับระบบที่ทำงานอยู่ (อาจไม่ปลอดภัยที่จะถือว่า Perl และ wc พร้อมใช้งาน)

2) ความผิดพลาดเพียงเล็กน้อยในการหลบหนีและคุณได้ส่งมอบการเข้าถึงเชลล์บนเครื่องของคุณ

เช่นเดียวกับสิ่งต่างๆส่วนใหญ่ที่ฉันรู้ (หรือคิดว่าฉันรู้) เกี่ยวกับการเข้ารหัสฉันได้รับข้อมูลนี้จากที่อื่น:

บทความของ John Reeve


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

ฉันใช้วิธีนี้เพื่อนับจำนวนบรรทัดในไฟล์เท่านั้น ข้อเสียของการทำข้อนี้คืออะไรกับคำตอบอื่น ๆ ฉันเห็นหลายบรรทัดตรงข้ามกับโซลูชันสองบรรทัดของฉัน ฉันเดาว่ามีเหตุผลที่ไม่มีใครทำแบบนี้

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

วิธีแก้ปัญหาเดิมคือสิ่งนี้ แต่เนื่องจากไฟล์ () โหลดไฟล์ทั้งหมดในหน่วยความจำนี่ก็เป็นปัญหาดั้งเดิมเช่นกัน (หน่วยความจำหมด) จึงไม่ใช่นี่ไม่ใช่วิธีแก้ปัญหาสำหรับคำถาม
Tuim

0

โซลูชันข้ามแพลตฟอร์มที่รวบรัดที่สุดที่บัฟเฟอร์ทีละบรรทัดเท่านั้น

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

น่าเสียดายที่เราต้องตั้งค่าREAD_AHEADสถานะมิฉะนั้นจะiterator_countบล็อกโดยไม่มีกำหนด มิฉะนั้นนี่จะเป็นซับเดียว


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.