摘要:這篇筆記主要解決這么幾個問題如何使用超低內(nèi)存快速遍歷數(shù)以萬計的目錄文件如何使用超低內(nèi)存快速讀取幾百甚至是級文件順便解決哪天我忘了可以通過搜索引擎搜到我自己寫的筆記來看看。
這不是一篇教程,這是一篇筆記,所以我不會很系統(tǒng)地論述原理和實現(xiàn),只簡單說明和舉例。
前言我寫這篇筆記的原因是現(xiàn)在網(wǎng)絡上關于 PHP 遍歷目錄文件和 PHP 讀取文本文件的教程和示例代碼都是極其低效的,低效就算了,有的甚至好意思說是高效,實在辣眼睛。
這篇筆記主要解決這么幾個問題:
PHP 如何使用超低內(nèi)存快速遍歷數(shù)以萬計的目錄文件?
PHP 如何使用超低內(nèi)存快速讀取幾百MB甚至是GB級文件?
順便解決哪天我忘了可以通過搜索引擎搜到我自己寫的筆記來看看。(因為需要 PHP 寫這兩個功能的情況真的很少,我記性不好,免得忘了又重走一遍彎路)
遍歷目錄文件網(wǎng)上關于這個方法的實現(xiàn)大多示例代碼是 glob 或者 opendir + readdir 組合,在目錄文件不多的情況下是沒問題的,但文件一多就有問題了(這里是指封裝成函數(shù)統(tǒng)一返回一個數(shù)組的時候),過大的數(shù)組會要求使用超大內(nèi)存,不僅導致速度慢,而且內(nèi)存不足的時候直接就崩潰了。
這時候正確的實現(xiàn)方法是使用 yield 關鍵字返回,下面是我最近使用的代碼:
valid()) { yield $sub->current(); $sub->next(); } if ($include_dirs) yield $rfile; } else { yield $rfile; } } closedir($dh); } } // 使用 $glob = glob2foreach("/var/www"); while ($glob->valid()) { // 當前文件 $filename = $glob->current(); // 這個就是包括路徑在內(nèi)的完整文件名了 // echo $filename; // 指向下一個,不能少 $glob->next(); }
yield 返回的是生成器對象(不了解的可以先去了解一下 PHP 生成器),并沒有立即生成數(shù)組,所以目錄下文件再多也不會出現(xiàn)巨無霸數(shù)組的情況,內(nèi)存消耗是低到可以忽略不計的幾十 kb 級別,時間消耗也幾乎只有循環(huán)消耗。
讀取文本文件讀取文本文件的情況跟遍歷目錄文件其實類似,網(wǎng)上教程基本上都是使用 file_get_contents 讀到內(nèi)存里或者 fopen + feof + fgetc 組合即讀即用,處理小文件的時候沒問題,但是處理大文件就有內(nèi)存不足等問題了,用 file_get_contents 去讀幾百MB的文件幾乎就是自殺。
這個問題的正確處理方法同樣和 yield 關鍵字有關,通過 yield 逐行處理,或者 SplFileObject 從指定位置讀取。
逐行讀取整個文件:
valid()) { // 當前行文本 $line = $glob->current(); // 逐行處理數(shù)據(jù) // $line // 指向下一個,不能少 $glob->next(); }
通過 yield 逐行讀取文件,具體使用多少內(nèi)存取決于每一行的數(shù)據(jù)量有多大,如果是每行只有幾百字節(jié)的日志文件,即使這個文件超過100M,占用內(nèi)存也只是KB級別。
但很多時候我們并不需要一次性讀完整個文件,比如當我們想分頁讀取一個1G大小的日志文件的時候,可能想第一頁讀取前面1000行,第二頁讀取第1000行到2000行,這時候就不能用上面的方法了,因為那方法雖然占用內(nèi)存低,但是數(shù)以萬計的循環(huán)是需要消耗時間的。
這時候,就改用 SplFileObject 處理,SplFileObject 可以從指定行數(shù)開始讀取。下面例子是寫入數(shù)組返回,可以根據(jù)自己業(yè)務決定要不要寫入數(shù)組,我懶得改了。
seek($offset); $i = 0; while (! $fp->eof()) { // 必須放在開頭 $i++; // 只讀 $count 這么多行 if ($i > $count) break; $line = $fp->current(); $line = trim($line); $arr[] = $line; // 指向下一個,不能少 $fp->next(); } return $arr; }
以上所說的都是文件巨大但是每一行數(shù)據(jù)量都很小的情況,有時候情況不是這樣,有時候是一行數(shù)據(jù)也有上百MB,那這該怎么處理呢?
如果是這種情況,那就要看具體業(yè)務了,SplFileObject 是可以通過 fseek 定位到字符位置(注意,跟 seek 定位到行數(shù)不一樣),然后通過 fread 讀取指定長度的字符。
也就是說通過 fseek 和 fread 是可以實現(xiàn)分段讀取一個超長字符串的,也就是可以實現(xiàn)超低內(nèi)存處理,但是具體要怎么做還是得看具體業(yè)務要求允許你怎么做。
復制大文件順便說下 PHP 復制文件,復制小文件用 copy 函數(shù)是沒問題的,復制大文件的話還是用數(shù)據(jù)流好,例子如下:
最后我這只說結論,沒有展示測試數(shù)據(jù),可能難以服眾,如果你持懷疑態(tài)度想求證,可以用 memory_get_peak_usage 和 microtime 去測一下代碼的占用內(nèi)存和運行時間。
補充:踩坑和修改大文件這篇筆記是我昨晚睡不著無聊突然想起來就隨手寫的,今天起來又看了一下,發(fā)現(xiàn)有一個巨坑沒提到,雖說不計劃寫成教程,但是這個巨坑必須提一下。
前面生成器對象循環(huán)代碼塊里最后都有一個 $glob->next(); 代碼,意思是指向下一項,這個至關重要,因為如果沒有了它,下一次循環(huán)獲取到的還是這次的結果。
舉個例子:
有個文本文件里面有三行文本,分別是 111111、222222、333333 ,當我們用以下代碼讀取的時候,while 會循環(huán)三次,每次 $line 分別對應 111111、222222、333333 。
valid()) { // 當前行文本 $line = $glob->current(); // 逐行處理數(shù)據(jù) // $line // 指向下一個,不能少 $glob->next(); }但是,如果沒有 $glob->next(); 這一行,就會導致 $line 始終是讀到第一行 111111 ,會導致死循環(huán)或者讀取到的不是預期的數(shù)據(jù)。
看到這里你可能會覺得這是廢話,不,不是,理論上不容易出現(xiàn)這個錯誤,但是在實際的編程中我們可能會使用 continue 跳到下次循環(huán),如果你寫著寫著不記得了,在 $glob->next(); 前面使用 continue 跳到下次循環(huán),就會導致下次循環(huán)的 $line 依然是這次的值,導致異常甚至死循環(huán)。
要解決這個問題,除了保持編碼警惕性,也可以修改下 $glob->next(); 的位置。
valid()) { // 當前項 $line = $glob->current(); // 指向下一個,不能少 $glob->next(); // 注意,這時已經(jīng)指向下一項 // 再使用 $glob->current() 獲取到的就不是 $line 的值了,而是下一項的值了 // 在這后面你就可以放心使用 continue 了 // 但是別忘了讀取當前項只能通過 $line 了 // 逐行處理數(shù)據(jù) }這個坑我是踩過的,無意間使用 continue 導致讀取數(shù)據(jù)不對。其實出現(xiàn)這種錯誤導致死循環(huán)程序崩潰是好事,立即排查能排查出結果,最可怕的是只讀錯數(shù)據(jù),讓人一時半會兒察覺不到。
另外,補充一下修改大文件的要點。
要讀大文件往往會涉及到修改它,如果是從中摘取數(shù)據(jù)或者大幅度修改,我們可以使用 fopen + fwrite 組合配合生成器對象逐行處理數(shù)據(jù)之后逐行寫入,這樣效率也是高的,盡量避免存到變量里再集中寫入以免占用內(nèi)存爆炸。
valid()) { // 當前行文本 $line = $glob->current(); // 逐行處理數(shù)據(jù) // 將處理過的寫入新文件 fwrite($handle, $line . " "); // 指向下一個,不能少 $glob->next(); } fclose($handle);如果是修改大文件里的小細節(jié),這個我還沒做過,不過據(jù)我了解好像是通過 Stream Functions 的 filter 實現(xiàn)效率比較高。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/31361.html
摘要:今天分享一個特別好用的東西,里面的生成器才引入的功能,可以避免數(shù)組過大導致內(nèi)存溢出的問題理解生成器關鍵字不是返回值,他的專業(yè)術語叫產(chǎn)出值,只是生成一個值,并不會立即生成所有結果集,所以內(nèi)存始終是一條循環(huán)的值應用場景遍歷文件目錄讀取超大文件日 今天分享一個特別好用的東西,php里面的生成器(PHP 5.5.0才引入的功能),可以避免數(shù)組過大導致內(nèi)存溢出的問題 理解:生成器yield關鍵字...
摘要:然而,剛進來就擰螺絲的人如果能夠對讀取一個的超大文件有所見解的話,造火箭也是遲早的事兒。其中,用于告知當前文件讀取指針所在位置,可以手動設定文件讀取指針的位置。 作為一名常年深耕curd的PHPer,關注內(nèi)存那是不可能的,反正apache或者fpm都幫我們做了,況且運行一次就銷毀,根本就不存在什么內(nèi)存問題。 然而偏偏就有些個不開眼的人把這些個東西當面試題,比如總有刁民用php讀取一個1...
摘要:場景和痛點說明今天因為一個老同學找我,說自己公司的物流業(yè)務都是現(xiàn)在用處理,按月因為數(shù)據(jù)量大,一個差不多有百萬數(shù)據(jù),文件有接近,打開和搜索就相當?shù)穆?lián)想到場景要導入數(shù)據(jù),可能數(shù)據(jù)量很大,這里利用常用的一些方法比如會常有時間和內(nèi)存限制問題下面我 場景和痛點 說明 今天因為一個老同學找我,說自己公司的物流業(yè)務都是現(xiàn)在用excel處理,按月因為數(shù)據(jù)量大,一個excel差不多有百萬數(shù)據(jù),文件有接...
摘要:執(zhí)行測試代碼兩種不同遍歷方法的代碼分別位于和。參考如何使用對一個萬的的表進行遍歷操作關于專注于微信小程序微信小游戲支付寶小程序和線上應用實時監(jiān)控。自從年雙十一正式上線,累計處理了億錯誤事件,付費客戶有金山軟件百姓網(wǎng)等眾多品牌企業(yè)。 GitHub 倉庫:Fundebug/loop-mongodb-big-collection showImg(https://segmentfault.c...
摘要:執(zhí)行測試代碼兩種不同遍歷方法的代碼分別位于和。參考如何使用對一個萬的的表進行遍歷操作關于專注于微信小程序微信小游戲支付寶小程序和線上應用實時監(jiān)控。自從年雙十一正式上線,累計處理了億錯誤事件,付費客戶有金山軟件百姓網(wǎng)等眾多品牌企業(yè)。 GitHub 倉庫:Fundebug/loop-mongodb-big-collection showImg(https://segmentfault.c...
閱讀 3595·2021-11-24 10:19
閱讀 3733·2021-09-30 09:47
閱讀 1300·2019-08-30 15:56
閱讀 798·2019-08-29 15:11
閱讀 909·2019-08-29 13:43
閱讀 3573·2019-08-28 18:25
閱讀 2166·2019-08-26 13:27
閱讀 1441·2019-08-26 11:44