轉載自: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萬行左右,這裏只爲做演示 運行結果如圖
現在這個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();
如果不出什麼特別的情況下,內存消耗會比上一個代碼更大。結果圖如下:
這一點,在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();
運行之後,發現內存一下降了好多,但是,時間好像還是一樣的。運行結果如下:
爲什麼爲這樣的呢,其實很簡單,因爲現在是每次讀取一行到內存,所以,內存並不會太高。但是,不管怎麼樣,以上三種方法,都是要循環一遍整個文件(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();
運行一下,發現時間直線下降。內存也有所減少。運行結果圖如:
特別說明一點,線程數並不是越多超好。線程數量多的話,在每一個線程處理的時間少了,但是在創建線程的時候,也是浪費時間的,我這裏每個線程處理4萬條記錄,你可以把這個數減少或增加,測試一下結果