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

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

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

「面試造火箭,入職擰螺絲」。然而,剛進來就擰螺絲的人若是可以對「PHP讀取一個10G的超大文件」有所看法的話,「造火箭」也是早晚的事兒。當前爲了可以來這裏「擰螺絲」,仍是得先搞定「讀取10G文件」這個問題。面試

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

... ...數組

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

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

首先,咱們嘗試用php的file函數來做一把死,大家感覺一下:spa

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

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

這句英文的大概意思就是「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

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

相關文章
相關標籤/搜索