老舊話題:PHP讀取超大文件

作爲一名常年深耕curd的PHPer,關注內存那是不可能的,反正apache或者fpm都幫我們做了,況且運行一次就銷燬,根本就不存在什麼內存問題。

然而偏偏就有些個不開眼的人把這些個東西當面試題,比如總有刁民用“php讀取一個10G的超大文件”當面試題來問你。當然了,作爲一個和我一樣的普普通通的蠢貨,你聽到這個問題的第一瞬間是懵逼,第二瞬間是臥槽,第三瞬間是保持結巴狀態。

“面試造火箭,入職擰螺絲”。然而,剛進來就擰螺絲的人如果能夠對“PHP讀取一個10G的超大文件”有所見解的話,“造火箭”也是遲早的事兒。當前爲了能夠來這裏“擰螺絲”,還是得先搞定“讀取10G文件”這個問題。

要想讀取10G的文件,首先,你得有個10G的文件

... ...

其實,相對來說也是比較簡單的事情,我們隨便找一個nginx的日誌文件,哪怕只有10KB,假設文件名是test.log,然後呢執行" cat test.log >> test.log ",聽我說少年,30秒左右你就該按下ctrl + C了,比如我這裏,你們感受一下:

202MB,作爲實驗演示,夠意思了。難不成真要造10G的文件?

首先,我們嘗試用php的file函數來作一把死,你們感受一下:

<?php
$begin = microtime( true );
file( './test.log' );
$end = microtime( true );
echo "cost : ".( $end - $begin ).PHP_EOL;

保存爲test.php,然後命令行下執行一把,結果如下圖所示:

這句英文的大概意思就是“PHP最大隻給每個進程分配了128MB內存,然而你特麼張口要202MB?”所以,我們修改一下php配置文件... ...

千萬不要手軟,把這個參數改成1024MB,然後再次執行上面的php腳本:

然後,我們再試試最愛的file_get_contents()函數,結果如下圖:

文件已經一次性全部被載入到內存中並將文件的每一行保存到了一個php數組中,我的機器是10G內存+256G固態硬盤,一次性載入這個202MB的文件file函數用了0.67秒鐘、file_get_contents函數用了0.25秒鐘(看起來file_get_content要比file靠譜的多),不過,敲重點的我們調整了配置文件纔可以讀取202MB的文件,如果擺在我們面前的是一個100G的文件呢?或者說,系統提供的php配置最多之給20MB內存而你又無法修改呢?

我們重點是如何在內存有限的機器上讀取體積幾百倍於內存的文件。下面,我們把memory_limit調整成16M,開啓困難模式。

202MB的文件,允許被分配的內存爲16MB,所以,總體思路其實也很簡單,就是一點兒一點兒地讀,只要每次讀取的內容小於16MB,那就一定不會有問題,首先我們感受一下一個字符一個字符讀,出場嘉賓是fgetc函數:

<?php
$begin = microtime( true );
$fp = fopen( './test.log' );
while( false !== ( $ch = fgetc( $fp ) ) ){
  // ⚠️⚠️⚠️ 作爲測試代碼是否正確,你可以打開註釋 ⚠️⚠️⚠️
  // 但是,打開註釋後屏顯字符會嚴重拖慢程序速度!也就是說程序運行速度可能遠遠超出屏幕顯示速度
  //echo $char.PHP_EOL;
}
fclose( $fp );
$end = microtime( true );
echo "cost : ".( $end - $begin ).PHP_EOL;

運行結果如下圖:

雖然只有給了16M內存,但我們還是成功將202M文件全部讀出來了,只不過這個運行速度是差了那麼點兒意思,不大行。不能一個字母一個字母地讀,這次我們一行一行地讀:

<?php
$begin = microtime( true );
$fp = fopen( './test.log', 'r' );
while( false !== ( $buffer = fgets( $fp, 4096 ) ) ){
  //echo $buffer.PHP_EOL;
}
if( !feof( $fp ) ){
  throw new Exception('... ...');
}
fclose( $fp );
$end = microtime( true );
echo "cost : ".( $end - $begin ).' sec'.PHP_EOL;

運行結果如下圖:

一行一行果然比一個一個字符要快很多,轉念一想吧,系統分配給我們的內存上限是16MB,那我們索性一次讀取一定量容量數據看看,會不會更快:

<?php
$begin = microtime( true );
$fp = fopen( './test.log', 'r' );
while( !feof( $fp ) ){
  // 如果你要使用echo,那麼,你會很慘烈...
  fread( $fp, 10240 );
}
fclose( $fp );
$end = microtime( true );
echo "cost : ".( $end - $begin ).' sec'.PHP_EOL;
exit;

保存代碼,運行一把,屌了屌了!!!在內存有限的情況下,我們還把時間縮短到了0.1秒!

然後我們考慮將問題升級一下,依然是上述這個202M的文件,這次我們要求讀取倒數後5行的內容,這個問題看起來屌了些許,用原來的fread啥的雖然奏效但總感覺比較愚蠢。所以,現在又得引入全新的函數來解決這個問題:ftell和fseek。其中,ftell用於告知當前文件讀取指針所在位置,fseek可以手動設定文件讀取指針的位置。我建議大家去手冊上重點觀摩一下fseek函數:點擊這裏

<?php
$fp = fopen( './test1.log', 'r' );
$line = 5;
$pos = -2;
$ch = '';
$content = '';
while( $line > 0 ){
  while( $ch != "\n" ){
    fseek( $fp, $pos, SEEK_END );
    $ch = fgetc( $fp );
    $pos--;
  }
  $ch = '';
  $content .= fgets( $fp );
  $line--;
}
echo $content;
exit;

其中test1.log文件的內容如下:

aa
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
1111111111
2222222222

保存文件並運行,結果如下圖所示:

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