日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
如何在不會(huì)導(dǎo)致服務(wù)器宕機(jī)的情況下,用PHP讀取大文件

作為PHP開發(fā)人員,我們并不經(jīng)常需要擔(dān)心內(nèi)存管理。PHP 引擎在我們背后做了很好的清理工作,短期執(zhí)行上下文的 Web 服務(wù)器模型意味著即使是最潦草的代碼也不會(huì)造成持久的影響。

很少情況下我們可能需要走出這個(gè)舒適的地方 —— 比如當(dāng)我們試圖在一個(gè)大型項(xiàng)目上運(yùn)行 Composer 來創(chuàng)建我們可以創(chuàng)建的最小的 VPS 時(shí),或者當(dāng)我們需要在一個(gè)同樣小的服務(wù)器上讀取大文件時(shí)。

后面的問題就是我們將在本教程中深入探討的。

在 GitHub 上可以找到本教程的源碼。

衡量成功的標(biāo)準(zhǔn)

確保我們對代碼進(jìn)行任何改進(jìn)的唯一方法是測試一個(gè)不好的情況,然后將我們修復(fù)之后的測量與另一個(gè)進(jìn)行比較。換句話說,除非我們知道“解決方案”對我們有多大的幫助(如果有的話),否則我們不知道它是否真的是一個(gè)解決方案。

這里有兩個(gè)我們可以關(guān)系的衡量標(biāo)準(zhǔn)。首先是CPU使用率。我們要處理的進(jìn)程有多快或多慢?第二是內(nèi)存使用情況。腳本執(zhí)行時(shí)需要多少內(nèi)存?這兩個(gè)通常是成反比的 - 這意味著我們可以以CPU使用率為代價(jià)來降低內(nèi)存使用,反之亦然。

在一個(gè)異步執(zhí)行模型(如多進(jìn)程或多線程的PHP應(yīng)用程序)中,CPU和內(nèi)存的使用率是很重要的考量因素。在傳統(tǒng)的PHP架構(gòu)中,當(dāng)任何一個(gè)值達(dá)到服務(wù)器的極限時(shí),這些通常都會(huì)成為問題。

測量PHP內(nèi)的CPU使用率是不切實(shí)際的。如果這是你要關(guān)注的領(lǐng)域,請考慮在Ubuntu或MacOS上使用類似top的工具。對于Windows,請考慮使用Linux子系統(tǒng),以便在Ubuntu中使用top。

為了本教程的目的,我們將測量內(nèi)存使用情況。我們將看看在“傳統(tǒng)”的腳本中使用了多少內(nèi)存。我們將執(zhí)行一些優(yōu)化策略并對其進(jìn)行度量。最后,我希望你能夠做出一個(gè)有經(jīng)驗(yàn)的選擇。

我們查看內(nèi)存使用多少的方法是:

 
 
 
  1. // formatBytes is taken from the php.net documentation 
  2.  
  3. memory_get_peak_usage(); 
  4.  
  5. function formatBytes($bytes, $precision = 2) { 
  6.     $units = array("b", "kb", "mb", "gb", "tb"); 
  7.  
  8.     $bytes = max($bytes, 0); 
  9.     $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); 
  10.     $pow = min($pow, count($units) - 1); 
  11.  
  12.     $bytes /= (1 << (10 * $pow)); 
  13.  
  14.     return round($bytes, $precision) . " " . $units[$pow]; 

我們將在腳本的最后使用這些函數(shù),以便我們能夠看到哪個(gè)腳本一次使用最大的內(nèi)存。

我們的選擇是什么?

這里有很多方法可以有效地讀取文件。但是也有兩種我們可能使用它們的情況。我們想要同時(shí)讀取和處理所有數(shù)據(jù),輸出處理過的數(shù)據(jù)或根據(jù)我們所讀取的內(nèi)容執(zhí)行其他操作。我們也可能想要轉(zhuǎn)換一個(gè)數(shù)據(jù)流,而不需要真正訪問的數(shù)據(jù)。

讓我們設(shè)想一下,對于第一種情況,我們希望讀取一個(gè)文件,并且每10,000行創(chuàng)建一個(gè)獨(dú)立排隊(duì)的處理作業(yè)。我們需要在內(nèi)存中保留至少10000行,并將它們傳遞給排隊(duì)的工作管理器(無論采取何種形式)。

對于第二種情況,我們假設(shè)我們想要壓縮一個(gè)特別大的API響應(yīng)的內(nèi)容。我們不在乎它的內(nèi)容是什么,但我們需要確保它是以壓縮形式備份的。

在這兩種情況下,如果我們需要讀取大文件,首先,我們需要知道數(shù)據(jù)是什么。第二,我們并不在乎數(shù)據(jù)是什么。讓我們來探索這些選擇吧...

逐行讀取文件

有許多操作文件的函數(shù),我們把部分結(jié)合到一個(gè)簡單的文件閱讀器中(封裝為一個(gè)方法):

 
 
 
  1. // from memory.php 
  2.  
  3. function formatBytes($bytes, $precision = 2) { 
  4.     $units = array("b", "kb", "mb", "gb", "tb"); 
  5.  
  6.     $bytes = max($bytes, 0); 
  7.     $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); 
  8.     $pow = min($pow, count($units) - 1); 
  9.  
  10.     $bytes /= (1 << (10 * $pow)); 
  11.  
  12.     return round($bytes, $precision) . " " . $units[$pow]; 
  13.  
  14. print formatBytes(memory_get_peak_usage()); 
  15.  
  16. // from reading-files-line-by-line-1.php 
  17.  
  18. function readTheFile($path) { 
  19.     $lines = []; 
  20.     $handle = fopen($path, "r"); 
  21.  
  22.     while(!feof($handle)) { 
  23.         $lines[] = trim(fgets($handle)); 
  24.     } 
  25.  
  26.     fclose($handle); 
  27.     return $lines; 
  28.  
  29. readTheFile("shakespeare.txt"); 
  30.  
  31. require "memory.php"; 

我們讀取一個(gè)文本文件為莎士比亞全集。文件大小為5.5MB,內(nèi)存占用峰值為12.8MB。現(xiàn)在讓我們用一個(gè)生成器來讀取每一行:

 
 
 
  1. // from reading-files-line-by-line-2.php 
  2.  
  3. function readTheFile($path) { 
  4.     $handle = fopen($path, "r"); 
  5.  
  6.     while(!feof($handle)) { 
  7.         yield trim(fgets($handle)); 
  8.     } 
  9.  
  10.     fclose($handle); 
  11.  
  12. readTheFile("shakespeare.txt"); 
  13.  
  14. require "memory.php"; 

文本文件大小不變,但內(nèi)存使用峰值只是393KB。即使我們能把讀取到的數(shù)據(jù)做一些事情也并不意味著什么。也許我們可以在看到兩條空白時(shí)把文檔分割成塊,像這樣:

 
 
 
  1. // from reading-files-line-by-line-3.php 
  2.  
  3. $iterator = readTheFile("shakespeare.txt"); 
  4.  
  5. $buffer = ""; 
  6.  
  7. foreach ($iterator as $iteration) { 
  8.     preg_match("/\n{3}/", $buffer, $matches); 
  9.  
  10.     if (count($matches)) { 
  11.         print "."; 
  12.         $buffer = ""; 
  13.     } else { 
  14.         $buffer .= $iteration . PHP_EOL; 
  15.     } 
  16.  
  17. require "memory.php"; 

猜到我們使用了多少內(nèi)存嗎?我們把文檔分割為1216塊,仍然只使用了459KB的內(nèi)存,這是否讓你驚訝?考慮到生成器的性質(zhì),我們使用的最多內(nèi)存是使用在 迭代中 我們需要存儲(chǔ)的最大文本塊。在本例中,最大的塊為101985字符。

我已經(jīng)撰寫了 使用生成器提示性能 和 Nikita Popov的迭代器庫 ,如果你感興趣就去看看吧!

生成器還有其它用途,但是最明顯的好處就是高性能讀取大文件。如果我們需要處理這些數(shù)據(jù),生成器可能是最好的方法。

管道間的文件

在我們不需要處理數(shù)據(jù)的情況下,我們可以把文件數(shù)據(jù)傳遞到另一個(gè)文件。通常被稱為管道(大概是因?yàn)槲覀兛床坏匠藘啥说墓茏永锩?,?dāng)然,它也是不透明的),我們可以通過使用流方法實(shí)現(xiàn)。讓我們先寫一個(gè)腳本從一個(gè)文件傳到另一個(gè)文件。這樣我們可以測量內(nèi)存的占用情況:

 
 
 
  1. // from piping-files-1.php 
  2.  
  3. file_put_contents( 
  4.     "piping-files-1.txt", file_get_contents("shakespeare.txt") 
  5. ); 
  6.  
  7. require "memory.php"; 

不出所料,這個(gè)腳本使用更多的內(nèi)存來進(jìn)行文本文件復(fù)制。這是因?yàn)樗x取(和保留)文件內(nèi)容在內(nèi)存中,直到它被寫到新文件中。對于小文件這種方法也許沒問題。當(dāng)為更大的文件時(shí),就 捉襟見肘了…

讓我們嘗試用流(管道)來傳送一個(gè)文件到另一個(gè):

 
 
 
  1. // from piping-files-2.php 
  2.  
  3. $handle1 = fopen("shakespeare.txt", "r"); 
  4. $handle2 = fopen("piping-files-2.txt", "w"); 
  5.  
  6. stream_copy_to_stream($handle1, $handle2); 
  7.  
  8. fclose($handle1); 
  9. fclose($handle2); 
  10.  
  11. require "memory.php"; 

這段代碼稍微有點(diǎn)陌生。我們打開了兩文件的句柄,第一個(gè)是只讀模式,第二個(gè)是只寫模式,然后我們從第一個(gè)復(fù)制到第二個(gè)中。最后我們關(guān)閉了它,也許使你驚訝,內(nèi)存只占用了393KB

這似乎很熟悉。像代碼生成器在存儲(chǔ)它讀到的每一行代碼?那是因?yàn)榈诙€(gè)參數(shù) fgets 規(guī)定了每行讀多少個(gè)字節(jié)(默認(rèn)值是-1或者直到下一行為止)。

第三個(gè)參數(shù) stream_copy_to_stream 和第二個(gè)參數(shù)是同一類參數(shù)(默認(rèn)值相同), stream_copy_to_stream 一次從一個(gè)數(shù)據(jù)流里讀一行,同時(shí)寫到另一個(gè)數(shù)據(jù)流里。 它跳過生成器只有一個(gè)值的部分(因?yàn)槲覀儾恍枰@個(gè)值)。

這篇文章對于我們來說可能是沒用的,所以讓我們想一些我們可能會(huì)用到的例子。假設(shè)我們想從我們的CDN中輸出一張圖片,作為一種重定向的路由應(yīng)用程序。我們可以參照下邊的代碼來實(shí)現(xiàn)它:

 
 
 
  1. // from piping-files-3.php 
  2.  
  3. file_put_contents( 
  4.     "piping-files-3.jpeg", file_get_contents( 
  5.         "https://github.com/assertchris/uploads/raw/master/rick.jpg" 
  6.     ) 
  7. ); 
  8.  
  9. // ...or write this straight to stdout, if we don't need the memory info 
  10.  
  11. require "memory.php"; 

設(shè)想一下,一個(gè)路由應(yīng)用程序讓我們看到這段代碼。但是,我們想從CDN獲取一個(gè)文件,而不是從本地的文件系統(tǒng)獲取。我們可以用一些其他的東西來更好的替換 file_get_contents (就像 Guzzle ),即使在引擎內(nèi)部它們幾乎是一樣的。

圖片的內(nèi)存大概有581K?,F(xiàn)在,讓我們來試試這個(gè)

 
 
 
  1. // from piping-files-4.php 
  2.  
  3. $handle1 = fopen( 
  4.     "https://github.com/assertchris/uploads/raw/master/rick.jpg", "r" 
  5. ); 
  6.  
  7. $handle2 = fopen( 
  8.     "piping-files-4.jpeg", "w" 
  9. ); 
  10.  
  11. // ...or write this straight to stdout, if we don't need the memory info 
  12.  
  13. stream_copy_to_stream($handle1, $handle2); 
  14.  
  15. fclose($handle1); 
  16. fclose($handle2); 
  17.  
  18. require "memory.php"; 

內(nèi)存使用明顯變少(大概 400K ),但是結(jié)果是一樣的。如果我們不關(guān)注內(nèi)存信息,我們依舊可以用標(biāo)準(zhǔn)模式輸出。實(shí)際上,PHP提供了一個(gè)簡單的方式來完成:

 
 
 
  1. $handle1 = fopen( 
  2.     "https://github.com/assertchris/uploads/raw/master/rick.jpg", "r" 
  3. ); 
  4.  
  5. $handle2 = fopen( 
  6.     "php://stdout", "w" 
  7. ); 
  8.  
  9. stream_copy_to_stream($handle1, $handle2); 
  10.  
  11. fclose($handle1); 
  12. fclose($handle2); 
  13.  
  14. // require "memory.php"; 

還有其它一些流,我們可以通過管道來寫入和讀取(或只讀取/只寫入):

  • php://stdin (只讀)
  • php://stderr (只寫, 如php://stdout)
  • php://input (只讀) 這使我們能夠訪問原始請求體
  • php://output (只寫) 讓我們寫入輸出緩沖區(qū)
  • php://memory 和 php://temp (讀-寫) 是我們可以臨時(shí)存儲(chǔ)數(shù)據(jù)的地方。 不同之處在于一旦它變得足夠大 php://temp 會(huì)將數(shù)據(jù)存儲(chǔ)在文件系統(tǒng)中,而 php://memory 將一直持存儲(chǔ)在內(nèi)存中直到資源耗盡。

還有一個(gè)我們可以在stream上使用的技巧,稱為 過濾器 。它們是一種中間的步驟,提供對stream數(shù)據(jù)的一些控制,但不把他們暴露給我們。想象一下,我們 會(huì)使用Zip擴(kuò)展名 來壓縮我們的shakespeare.txt文件。

 
 
 
  1. // from filters-1.php 
  2.  
  3. $zip = new ZipArchive(); 
  4. $filename = "filters-1.zip"; 
  5.  
  6. $zip->open($filename, ZipArchive::CREATE); 
  7. $zip->addFromString("shakespeare.txt", file_get_contents("shakespeare.txt")); 
  8. $zip->close(); 
  9.  
  10. require "memory.php"; 

這是一小段整潔的代碼,但它測量內(nèi)存占用在10.75MB左右。使用過濾器的話,我們可以減少內(nèi)存:

 
 
 
  1. // from filters-2.php 
  2.  
  3. $handle1 = fopen( 
  4.     "php://filter/zlib.deflate/resource=shakespeare.txt", "r" 
  5. ); 
  6.  
  7. $handle2 = fopen( 
  8.     "filters-2.deflated", "w" 
  9. ); 
  10.  
  11. stream_copy_to_stream($handle1, $handle2); 
  12.  
  13. fclose($handle1); 
  14. fclose($handle2); 
  15.  
  16. require "memory.php"; 

此處,我們可以看到名為php://filter/zlib.deflate的過濾器,它讀取并壓縮資源的內(nèi)容。我們可以在之后將壓縮數(shù)據(jù)導(dǎo)出到另一個(gè)文件中。這僅使用了 896KB .

我知道這是不一樣的格式,或者制作zip存檔是有好處的。你不得不懷疑:如果你可以選擇不同的格式并節(jié)省約12倍的內(nèi)存,為什么不選呢?

為了解壓此數(shù)據(jù),我們可以通過執(zhí)行另一個(gè)zlib filter將壓縮后的數(shù)據(jù)還原:

 
 
 
  1. // from filters-2.php 
  2.  
  3. file_get_contents( 
  4.     "php://filter/zlib.inflate/resource=filters-2.deflated" 
  5. ); 

Streams have been extensively covered in Stream在“ 理解PHP中的流 ”和“ U高效使用PHP中的流 ”中已經(jīng)被全面介紹了。如果你喜歡一個(gè)完全不同的視角,可以閱讀一下。


本文名稱:如何在不會(huì)導(dǎo)致服務(wù)器宕機(jī)的情況下,用PHP讀取大文件
標(biāo)題URL:http://www.dlmjj.cn/article/dphspgh.html