詳解PHP如何讀取大文件
目錄
- 衡量成功
- 我們有什么選擇?
- 逐行讀取文件
- 文件之間的管道
- 其他流
- 過(guò)濾器
- 自定義流
- 創(chuàng)建自定義協(xié)議和過(guò)濾器
- 總結(jié)
衡量成功
唯一能確認(rèn)我們對(duì)代碼所做改進(jìn)是否有效的方式是:衡量一個(gè)糟糕的情況,然后對(duì)比我們已經(jīng)應(yīng)用改進(jìn)后的衡量情況。換言之,除非我們知道 “解決方案” 能幫我們到什么程度 (如果有的話),否則我們并不知道它是否是一個(gè)解決方案。
我們可以關(guān)注兩個(gè)指標(biāo)。首先是 CPU 使用率。我們要處理的過(guò)程運(yùn)行得有多快或多慢?其次是內(nèi)存使用率。腳本執(zhí)行要占用多少內(nèi)存?這些通常是成反比的 — 這意味著我們能夠以 CPU 使用率為代價(jià)減少內(nèi)存的使用率,反之亦可。
在一個(gè)異步處理模型 (例如多進(jìn)程或多線程 PHP 應(yīng)用程序) 中,CPU 和內(nèi)存使用率都是重要的考量。在傳統(tǒng) PHP 架構(gòu)中,任一達(dá)到服務(wù)器所限時(shí)這些通常都會(huì)成為一個(gè)麻煩。
測(cè)量 PHP 內(nèi)部的 CPU 使用率是難以實(shí)現(xiàn)的。如果你確實(shí)關(guān)注這一塊,可用考慮在 Ubuntu 或 macOS 中使用類似于 top 的命令。對(duì)于 Windows,則可用考慮使用 Linux 子系統(tǒng),這樣你就能夠在 Ubuntu 中使用 top 命令了。
在本教程中,我們將測(cè)量?jī)?nèi)存使用情況。我們將看一下 “傳統(tǒng)” 腳本會(huì)使用多少內(nèi)存。我們也會(huì)實(shí)現(xiàn)一些優(yōu)化策略并對(duì)它們進(jìn)行度量。最后,我希望你能做一個(gè)合理的選擇。
以下是我們用于查看內(nèi)存使用量的方法:
// formatBytes 方法取材于 php.net 文檔 memory_get_peak_usage(); function formatBytes($bytes, $precision = 2) { $units = array("b", "kb", "mb", "gb", "tb"); $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= (1 << (10 * $pow)); return round($bytes, $precision) . " " . $units[$pow]; }
我們將在腳本的結(jié)尾處使用這些方法,以便于我們了解哪個(gè)腳本一次使用了最多的內(nèi)存。
我們有什么選擇?
我們有許多方法來(lái)有效地讀取文件。有以下兩種場(chǎng)景會(huì)使用到他們。我們可能希望同時(shí)讀取和處理所有數(shù)據(jù),對(duì)處理后的數(shù)據(jù)進(jìn)行輸出或者執(zhí)行其他操作。 我們還可能希望對(duì)數(shù)據(jù)流進(jìn)行轉(zhuǎn)換而不需要訪問(wèn)到這些數(shù)據(jù)。
想象以下,對(duì)于第一種情況,如果我們希望讀取文件并且把每 10,000 行的數(shù)據(jù)交給單獨(dú)的隊(duì)列進(jìn)行處理。我們則需要至少把 10,000 行的數(shù)據(jù)加載到內(nèi)存中,然后把它們交給隊(duì)列管理器(無(wú)論使用哪種)。
對(duì)于第二種情況,假設(shè)我們想要壓縮一個(gè) API 響應(yīng)的內(nèi)容,這個(gè) API 響應(yīng)特別大。雖然這里我們不關(guān)心它的內(nèi)容是什么,但是我們需要確保它被以一種壓縮格式備份起來(lái)。
這兩種情況,我們都需要讀取大文件。不同的是,第一種情況我們需要知道數(shù)據(jù)是什么,而第二種情況我們不關(guān)心數(shù)據(jù)是什么。接下來(lái),讓我們來(lái)深入討論一下這兩種做法.
逐行讀取文件
PHP 處理文件的函數(shù)很多,讓我們將其中一些函數(shù)結(jié)合起來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的文件閱讀器
// from memory.php function formatBytes($bytes, $precision = 2) { $units = array("b", "kb", "mb", "gb", "tb"); $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= (1 << (10 * $pow)); return round($bytes, $precision) . " " . $units[$pow]; } print formatBytes(memory_get_peak_usage()); // from reading-files-line-by-line-1.php function readTheFile($path) { $lines = []; $handle = fopen($path, "r"); while(!feof($handle)) { $lines[] = trim(fgets($handle)); } fclose($handle); return $lines; } readTheFile("shakespeare.txt"); require "memory.php";
我們正在閱讀一個(gè)包括莎士比亞全部著作的文本文件。該文件大小大約為 5.5 MB。內(nèi)存使用峰值為 12.8 MB?,F(xiàn)在,讓我們使用生成器來(lái)讀取每一行:
// from reading-files-line-by-line-2.php function readTheFile($path) { $handle = fopen($path, "r"); while(!feof($handle)) { yield trim(fgets($handle)); } fclose($handle); } readTheFile("shakespeare.txt"); require "memory.php";
文件大小相同,但是內(nèi)存使用峰值為 393 KB。這個(gè)數(shù)據(jù)意義大不大,因?yàn)槲覀冃枰尤雽?duì)文件數(shù)據(jù)的處理。例如,當(dāng)出現(xiàn)兩個(gè)空白行時(shí),將文檔拆分為多個(gè)塊:
// from reading-files-line-by-line-3.php $iterator = readTheFile("shakespeare.txt"); $buffer = ""; foreach ($iterator as $iteration) { preg_match("/n{3}/", $buffer, $matches); if (count($matches)) { print "."; $buffer = ""; } else { $buffer .= $iteration . PHP_EOL; } } require "memory.php";
有人猜測(cè)這次使用多少內(nèi)存嗎?即使我們將文本文檔分為 126 個(gè)塊,我們?nèi)匀恢皇褂?459 KB 的內(nèi)存。鑒于生成器的性質(zhì),我們將使用的最大內(nèi)存是在迭代中需要存儲(chǔ)最大文本塊的內(nèi)存。在這種情況下,最大的塊是 101985 個(gè)字符。
生成器還有其他用途,但顯然它可以很好的讀取大型文件。如果我們需要處理數(shù)據(jù),生成器可能是最好的方法。
文件之間的管道
在不需要處理數(shù)據(jù)的情況下,我們可以將文件數(shù)據(jù)從一個(gè)文件傳遞到另一個(gè)文件。這通常稱為管道 (大概是因?yàn)槌藘啥酥?,我們看不到管道?nèi)的任何東西,當(dāng)然,只要它是不透明的)。我們可以通過(guò)流 (stream) 來(lái)實(shí)現(xiàn),首先,我們編寫一個(gè)腳本實(shí)現(xiàn)一個(gè)文件到另一個(gè)文件的傳輸,以便我們可以測(cè)量?jī)?nèi)存使用情況:
// from piping-files-1.php file_put_contents( "piping-files-1.txt", file_get_contents("shakespeare.txt") ); require "memory.php";
結(jié)果并沒(méi)有讓人感到意外。該腳本比其復(fù)制的文本文件使用更多的內(nèi)存來(lái)運(yùn)行。這是因?yàn)槟_本必須在內(nèi)存中讀取整個(gè)文件直到將其寫入另外一個(gè)文件。對(duì)于小的文件而言,這種操作是 OK 的。但是將其用于大文件時(shí),就不是那么回事了。
讓我們嘗試從一個(gè)文件流式傳輸 (或管道傳輸) 到另一個(gè)文件:
// from piping-files-2.php $handle1 = fopen("shakespeare.txt", "r"); $handle2 = fopen("piping-files-2.txt", "w"); stream_copy_to_stream($handle1, $handle2); fclose($handle1); fclose($handle2); require "memory.php";
這段代碼有點(diǎn)奇怪。我們打開(kāi)兩個(gè)文件的句柄,第一個(gè)處于讀取模式,第二個(gè)處于寫入模式。然后,我們從第一個(gè)復(fù)制到第二個(gè)。我們通過(guò)再次關(guān)閉兩個(gè)文件來(lái)完成。當(dāng)你知道內(nèi)存使用為 393 KB 時(shí),可能會(huì)感到驚訝。這個(gè)數(shù)字看起來(lái)很熟悉,這不就是利用生成器保存逐行讀取內(nèi)容時(shí)所使用的內(nèi)存嗎。這是因?yàn)閒gets的第二個(gè)參數(shù)定義了每行要讀取的字節(jié)數(shù) (默認(rèn)為-1
或到達(dá)新行之前的長(zhǎng)度)。stream_copy_to_stream 的第三個(gè)參數(shù)是相同的(默認(rèn)值完全相同)。stream_copy_to_stream 一次從一個(gè)流讀取一行,并將其寫入另一流。由于我們不需要處理該值,因此它會(huì)跳過(guò)生成器產(chǎn)生值的部分
單單傳輸文字還不夠?qū)嵱?,所以考慮下其他例子。假設(shè)我們想從 CDN 輸出圖像,可以用以下代碼來(lái)描述
// from piping-files-3.php file_put_contents( "piping-files-3.jpeg", file_get_contents( "https://github.com/assertchris/uploads/raw/master/rick.jpg" ) ); // ...or write this straight to stdout, if we don't need the memory info require "memory.php";
想象一下應(yīng)用程度執(zhí)行到該步驟。這次我們不是要從本地文件系統(tǒng)中獲取圖像,而是從 CDN 獲取。我們用 file_get_contents 代替更優(yōu)雅的處理方式 (例如 Guzzle),它們的實(shí)際效果是一樣的。
內(nèi)存使用情況為 581KB,現(xiàn)在,我們?nèi)绾螄L試進(jìn)行流傳輸呢?
// from piping-files-4.php $handle1 = fopen( "https://github.com/assertchris/uploads/raw/master/rick.jpg", "r" ); $handle2 = fopen( "piping-files-4.jpeg", "w" ); // ...or write this straight to stdout, if we don't need the memory info stream_copy_to_stream($handle1, $handle2); fclose($handle1); fclose($handle2); require "memory.php";
內(nèi)存使用比剛才略少 (400 KB),但是結(jié)果是相同的。如果我們不需要內(nèi)存信息,也可以打印至標(biāo)準(zhǔn)輸出。PHP 提供了一種簡(jiǎn)單的方法來(lái)執(zhí)行此操作:
$handle1 = fopen( "https://github.com/assertchris/uploads/raw/master/rick.jpg", "r" ); $handle2 = fopen( "php://stdout", "w" ); stream_copy_to_stream($handle1, $handle2); fclose($handle1); fclose($handle2); // require "memory.php";
其他流
還存在一些流可以通過(guò)管道來(lái)讀寫。
- php://stdin只讀
- php://stderr只寫,與php://stdout相似
- php://input只讀,使我們可以訪問(wèn)原始請(qǐng)求內(nèi)容
- php://output只寫,可讓我們寫入輸出緩沖區(qū)
- php://memory與php://temp(可讀寫) 是臨時(shí)存儲(chǔ)數(shù)據(jù)的地方。區(qū)別在于數(shù)據(jù)足夠大時(shí)php:/// temp就會(huì)將數(shù)據(jù)存儲(chǔ)在文件系統(tǒng)中,而php:/// memory將繼續(xù)存儲(chǔ)在內(nèi)存中直到耗盡。
過(guò)濾器
我們可以對(duì)流使用另一個(gè)技巧,稱為過(guò)濾器。它介于兩者之間,對(duì)數(shù)據(jù)進(jìn)行了適當(dāng)?shù)目刂剖蛊洳槐┞督o外接。假設(shè)我們要壓縮shakespeare.txt文件。我們可以使用 Zip 擴(kuò)展
// from filters-1.php $zip = new ZipArchive(); $filename = "filters-1.zip"; $zip->open($filename, ZipArchive::CREATE); $zip->addFromString("shakespeare.txt", file_get_contents("shakespeare.txt")); $zip->close(); require "memory.php";
這段代碼雖然整潔,但是總共使用了大概 10.75 MB 的內(nèi)存。我們可以使用過(guò)濾器來(lái)進(jìn)行優(yōu)化
// from filters-2.php $handle1 = fopen( "php://filter/zlib.deflate/resource=shakespeare.txt", "r" ); $handle2 = fopen( "filters-2.deflated", "w" ); stream_copy_to_stream($handle1, $handle2); fclose($handle1); fclose($handle2); require "memory.php";
在這里,我們可以看到php:///filter/zlib.deflate過(guò)濾器,該過(guò)濾器讀取和壓縮資源的內(nèi)容。然后我們可以將該壓縮數(shù)據(jù)通過(guò)管道傳輸?shù)搅硪粋€(gè)文件中。這僅使用了 896KB 內(nèi)存。
雖然格式不同,或者說(shuō)使用 zip 壓縮文件有其他諸多好處。但是,你不得不考慮:如果選擇其他格式你可以節(jié)省 12 倍的內(nèi)存,你會(huì)不會(huì)心動(dòng)?
要對(duì)數(shù)據(jù)進(jìn)行解壓,只需要通過(guò)另外一個(gè) zlib 過(guò)濾器:
// from filters-2.php file_get_contents( "php://filter/zlib.inflate/resource=filters-2.deflated" );
自定義流
fopen和file_get_contents具有它們自己的默認(rèn)選項(xiàng)集,但是它們是完全可定制的。要定義它們,我們需要?jiǎng)?chuàng)建一個(gè)新的流上下文
// from creating-contexts-1.php $data = join("&", [ "twitter=assertchris", ]); $headers = join("rn", [ "Content-type: application/x-www-form-urlencoded", "Content-length: " . strlen($data), ]); $options = [ "http" => [ "method" => "POST", "header"=> $headers, "content" => $data, ], ]; $context = stream_content_create($options); $handle = fopen("https://example.com/register", "r", false, $context); $response = stream_get_contents($handle); fclose($handle);
本例中,我們嘗試發(fā)送一個(gè) POST 請(qǐng)求給 API。API 端點(diǎn)是安全的,不過(guò)我們?nèi)匀皇褂昧?http 上下文屬性(可用于 http 或者 https)。我們?cè)O(shè)置了一些頭部,并打開(kāi)了 API 的文件句柄。我們可以將句柄以只讀方式打開(kāi),上下文負(fù)責(zé)編寫。
創(chuàng)建自定義協(xié)議和過(guò)濾器
在總結(jié)之前,我們先談?wù)剟?chuàng)建自定義協(xié)議。
Protocol { public resource $context; public __construct ( void ) public __destruct ( void ) public bool dir_closedir ( void ) public bool dir_opendir ( string $path , int $options ) public string dir_readdir ( void ) public bool dir_rewinddir ( void ) public bool mkdir ( string $path , int $mode , int $options ) public bool rename ( string $path_from , string $path_to ) public bool rmdir ( string $path , int $options ) public resource stream_cast ( int $cast_as ) public void stream_close ( void ) public bool stream_eof ( void ) public bool stream_flush ( void ) public bool stream_lock ( int $operation ) public bool stream_metadata ( string $path , int $option , mixed $value ) public bool stream_open ( string $path , string $mode , int $options , string &$opened_path ) public string stream_read ( int $count ) public bool stream_seek ( int $offset , int $whence = SEEK_SET ) public bool stream_set_option ( int $option , int $arg1 , int $arg2 ) public array stream_stat ( void ) public int stream_tell ( void ) public bool stream_truncate ( int $new_size ) public int stream_write ( string $data ) public bool unlink ( string $path ) public array url_stat ( string $path , int $flags ) }
我們并不打算實(shí)現(xiàn)其中一個(gè),因?yàn)槲艺J(rèn)為它值得擁有自己的教程。有很多工作要做。但是一旦完成工作,我們就可以很容易地注冊(cè)流包裝器:
if (in_array("highlight-names", stream_get_wrappers())) { stream_wrapper_unregister("highlight-names"); } stream_wrapper_register("highlight-names", "HighlightNamesProtocol"); $highlighted = file_get_contents("highlight-names://story.txt");
同樣,也可以創(chuàng)建自定義流過(guò)濾器。
Filter { public $filtername; public $params public int filter ( resource $in , resource $out , int &$consumed , bool $closing ) public void onClose ( void ) public bool onCreate ( void ) }
可被輕松注冊(cè)
$handle = fopen("story.txt", "w+"); stream_filter_append($handle, "highlight-names", STREAM_FILTER_READ);
highlight-names 需要與新過(guò)濾器類的 filtername 屬性匹配。還可以在 php:///filter/highligh-names/resource=story.txt 字符串中使用自定義過(guò)濾器。定義過(guò)濾器比定義協(xié)議要容易得多。原因之一是協(xié)議需要處理目錄操作,而過(guò)濾器僅需要處理每個(gè)數(shù)據(jù)塊。
如果您愿意,我強(qiáng)烈建議您嘗試創(chuàng)建自定義協(xié)議和過(guò)濾器。如果您可以將過(guò)濾器應(yīng)用于 stream_copy_to_stream 操作,則即使處理令人討厭的大文件,您的應(yīng)用程序也將幾乎不使用任何內(nèi)存。想象一下編寫調(diào)整大小圖像過(guò)濾器或加密應(yīng)用程序過(guò)濾器。
如果你愿意,我強(qiáng)烈建議你嘗試創(chuàng)建自定義協(xié)議和過(guò)濾器。如果你可以將過(guò)濾器應(yīng)用于 stream_copy_to_stream 操作,即使處理煩人的大文件,你的應(yīng)用程序也幾乎不使用任何內(nèi)存。想象下編寫 resize-image 過(guò)濾器和 encrypt-for-application 過(guò)濾器吧。
總結(jié)
雖然這不是我們經(jīng)常遇到的問(wèn)題,但是在處理大文件時(shí)的確很容易搞砸。在異步應(yīng)用中,如果我們不注意內(nèi)存的使用情況,很容易導(dǎo)致服務(wù)器的崩潰。
本教程希望能帶給你一些新的想法(或者更新你的對(duì)這方面的固有記憶),以便你能夠更多的考慮如何有效地讀取和寫入大文件。當(dāng)我們開(kāi)始熟悉和使用流和生成器并停止使用諸如 file_get_contents 這樣的函數(shù)時(shí),這方面的錯(cuò)誤將全部從應(yīng)用程序中消失,這不失為一件好事。
以上就是詳解PHP如何讀取大文件的詳細(xì)內(nèi)容,更多關(guān)于PHP如何讀取大文件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
版權(quán)聲明:
本站所有文章和圖片均來(lái)自用戶分享和網(wǎng)絡(luò)收集,文章和圖片版權(quán)歸原作者及原出處所有,僅供學(xué)習(xí)與參考,請(qǐng)勿用于商業(yè)用途,如果損害了您的權(quán)利,請(qǐng)聯(lián)系網(wǎng)站客服處理。