PHP分段讀取大文件並統計

轉載自:https://www.yduba.com/biancheng-2442221832.html

有時候,我們經常對大文件進行操作和分析,比如:去統計日誌裏某個IP最近訪問網站的情況。nginx 的 assess.log 的文件就記錄了訪問日誌,但是這個文件一般的情況下都是特別大的。用PHP的話,怎麼去統計裏面的信息呢?這裏自己做一個學習總結。

理論方法:

1、把文件一次性讀到內存中,然後一行一行分析,如:函數 file_get_contents、fread、file 這些都可以操作

2、分段讀取內容到內存裏,一段一段的分析,如:函數 fgets

這兩種方法都可以現實我們的操作。首先說一說第一種,一次性讀取內容。這種方法比較簡單,就是把日誌讀到內存之後,轉換成數組,然後分析分析數組的每一列。代碼如下:

<?php
defined('READ_FILE')  or define('READ_FILE', './assess.log');
defined('WRITE_FILE_A') or define('WRITE_FILE_A', './temp1');

// 方法 file_get_contents
function writeLogA()
{
    $st = microtime( true );
    $sm = memory_get_usage();

    $content 		= file_get_contents(READ_FILE);
    $content_arr 	= explode("\n", $content);
    $writeres 		= fopen(WRITE_FILE_A, 'a+');

    // 鎖定 WRITE_FILE
    flock($writeres, LOCK_EX);

    foreach($content_arr as $row)
    {
	if( strpos($row, "192.168.10.10") !== false )
	{
            fwrite($writeres, $row  . "\n");
	}
    }

    $em = memory_get_usage();
    flock($writeres, LOCK_UN);
    fclose($writeres);

    $et = microtime( true );
    echo "使用 file_get_contents: 用時間:" . ($et - $st) . "\n";
    echo "使用 file_get_contents: 用內存:" . ($em - $sm) . "\n";
}

writeLogA();

以上代碼運行之後,就可以把IP爲192.168.10.10的用戶訪問日誌給寫到一個臨時文件中,但是這時候,有一個問題是,代碼運行的時間和消耗的內存特別大。特別說明:我的 assess.log 並不大,10萬行左右,這裏只爲做演示 運行結果如圖

A.png

現在這個access.log文件並不大,已經用了這麼長時間和消耗這麼大的內存了,如果更大的文件呢,所以,這種方法並不實用。

再看看另一個函數 fread, fread是讀取一個文件的內容到一個字符串中。

string fread ( resource $handle , int $length )

第一個參數是文件系統指針,由 fopen 創建並返回的值,第二個參數是要讀取的大小。返回值爲正確讀取的內容。

我們現在把上面的代碼中的 file_get_contents 替換成 fread。代碼如下:

<?php
defined('READ_FILE')  or define('READ_FILE', './error.log');
defined('WRITE_FILE_B') or define('WRITE_FILE_B', './temp1');

// 方法 fread
function writeLogB()
{
    $st = microtime( true );
    $sm = memory_get_usage();

    $fopenres = fopen(READ_FILE, "r");
    $content  = fread($fopenres, filesize(READ_FILE));
    $content_arr 	= explode("\n", $content);

    $writeres 		= fopen(WRITE_FILE_B, 'a+');

    // 鎖定 WRITE_FILE
    flock($writeres, LOCK_EX);

    foreach($content_arr as $row)
    {
        if( strpos($row, "[error]") !== false )
	{
	    fwrite($writeres, $row  . "\n");
	}
    }

    $em = memory_get_usage();
    flock($writeres, LOCK_UN);
    fclose($writeres);
    fclose($fopenres);

    $et = microtime( true );
    echo "使用 fread: 用時間:" . ($et - $st) . "\n";
    echo "使用 fread: 用內存:" . ($em - $sm) . "\n";
}
writeLogB();

如果不出什麼特別的情況下,內存消耗會比上一個代碼更大。結果圖如下:

b.png

這一點,在PHP的官方網站也有說明:如果只是想將一個文件的內容讀入到一個字符串中,用 file_get_contents(),它的性能比 fread 好得多, 具體查看https://www.php.net/manual/zh/function.fread.php

file 函數也可以做到,想要的結果,這裏就不做演示了。file 函數是把一個文件讀到一個數組中。其實上面三個函數的原理都一樣,就是把一個大文件一次性讀到內存裏,顯然這樣的方式很消耗內存,在處理大文件的時候,這個方法並不可取。

我們再看看理論的第二種方法,分段讀取。這個方法是把一個大文件分成若干小段,每次讀到一段分析,最後整合在一起再分析統計。fgets 是每次讀到一行,好像正是我們想要的。

string fgets ( resource $handle [, int $length ] )

第一個參數是文件系統指針,由 fopen 創建並返回的值,第二個參數是要讀取的大小。如果沒有指定 length,則默認爲 1K,或者說 1024 字節,返回值爲正確讀取的內容。

現在把上面的代碼再做一次修改。如下:

<?php
defined('READ_FILE')  or define('READ_FILE', './error.log');
defined('WRITE_FILE_C') or define('WRITE_FILE_C', './temp1');

// 方法 fgets
function writeLogC()
{
    $st = microtime( true );
    $sm = memory_get_usage();

    $fileres  = fopen(READ_FILE, 'r');
    $writeres = fopen(WRITE_FILE_C, 'a+');

    // 鎖定 WRITE_FILE
    flock($writeres, LOCK_EX);

    while( $row = fgets($fileres) )
    {
	if( strpos($row, "[error]") !== false )
	{
    	    fwrite($writeres, $row);
	}
    }

    $em = memory_get_usage();
    flock($writeres, LOCK_UN);
    fclose($writeres);
    fclose($fileres);

    $et = microtime( true );
    echo "使用 fgets: 用時間:" . ($et - $st) . "\n";
    echo "使用 fgets: 用內存:" . ($em - $sm) . "\n";
}
writeLogC();

運行之後,發現內存一下降了好多,但是,時間好像還是一樣的。運行結果如下:

C.png

爲什麼爲這樣的呢,其實很簡單,因爲現在是每次讀取一行到內存,所以,內存並不會太高。但是,不管怎麼樣,以上三種方法,都是要循環一遍整個文件(10萬行),所以時間的話,三種方法並不會相差太多。那有沒有更好的方法呢,有。就是採用多線程,把大文件分成小文件,每一個線程處理一個小文件,這樣的話,時間肯定會小很多。

說明一下,PHP默認情況下,並沒有安裝多線程模塊,這裏要自己安裝。沒有安裝的,請查看 https://www.yduba.com/biancheng-6852262344.html

現在就按上面的方法把代碼換成如下的方法:

<?php
defined('READ_FILE')  or define('READ_FILE', './error.log');
defined('WRITE_FILE_D') or define('WRITE_FILE_D', './temp1');

// 使用多線程
class Mythread extends Thread
{
    private $i = null;

    public function __construct( $i )
    {
	$this->i = $i;
    }

    public function run()
    {
	$filename = "temp_log_" . Thread::getCurrentThreadId();
	$cutline = ($this->i - 1) * 40000 + 1 . ", " . ($this->i * 40000);
	exec("sed -n '" . $cutline . "p' " . READ_FILE . " > " . $filename);

	$this->_writeTemp( $filename );
    }

    private function _writeTemp( $readfile = '' )
    {
	if( !$readfile || !file_exists($readfile) ) return;

    	$fileres  = fopen($readfile, 'r');
	$writeres = fopen(WRITE_FILE_D, 'a+');

	// 鎖定 WRITE_FILE
	flock($writeres, LOCK_EX);

	while( $row = fgets($fileres) )
	{
	    if( strpos($row, "[error]") !== false )
	    {
	        fwrite($writeres, $row);
	    }
	}

	flock($writeres, LOCK_UN);
	fclose($fileres);
	fclose($writeres);
	unlink( $readfile );
    }
}

function writeLogd()
{
    $st = microtime( true );
    $sm = memory_get_usage();

    $count_line = 0;

    //獲取整個文件的行數
    $content 		= exec('wc -l ' . READ_FILE);
    $content_arr 	= explode(" ", $content);
    $count_line 	= $content_arr[0];

    //線程數
    $count_thread 	= ceil($count_line / 40000);

    $worker = array();
    for($i=1; $i<=$count_thread; $i++)
    {
    	$worker[$i] = new Mythread( $i );
    	$worker[$i]->start();
    }
    $em = memory_get_usage();
    $et = microtime( true );
    echo "使用 多線程: 用時間:" . ($et - $st) . "\n";
    echo "使用 多線程: 用內存:" . ($em - $sm) . "\n";
}
writeLogd();

運行一下,發現時間直線下降。內存也有所減少。運行結果圖如:

d.png

 

特別說明一點,線程數並不是越多超好。線程數量多的話,在每一個線程處理的時間少了,但是在創建線程的時候,也是浪費時間的,我這裏每個線程處理4萬條記錄,你可以把這個數減少或增加,測試一下結果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章