如何利用PHP讀取大文件

這篇文章主要介紹了如何利用PHP讀取大文件的相關(guān)知識,內(nèi)容詳細(xì)易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇如何利用PHP讀取大文件文章都會有所收獲,下面我們一起來看看吧。

網(wǎng)站建設(shè)哪家好,找創(chuàng)新互聯(lián)!專注于網(wǎng)頁設(shè)計、網(wǎng)站建設(shè)、微信開發(fā)、成都小程序開發(fā)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了達(dá)坂城免費建站歡迎大家使用!

衡量成功

唯一能確認(rèn)我們對代碼所做改進(jìn)是否有效的方式是:衡量一個糟糕的情況,然后對比我們已經(jīng)應(yīng)用改進(jìn)后的衡量情況。換言之,除非我們知道“解決方案”能幫我們到什么程度(如果有的話),否則我們并不知道它是否是一個解決方案。

我們可以關(guān)注兩個指標(biāo)。首先是CPU使用率。我們要處理的過程運行得有多快或多慢?其次是內(nèi)存使用率。腳本執(zhí)行要占用多少內(nèi)存?這些通常是成反比的—這意味著我們能夠以CPU使用率為代價減少內(nèi)存的使用率,反之亦可。

在一個異步處理模型(例如多進(jìn)程或多線程PHP應(yīng)用程序)中,CPU和內(nèi)存使用率都是重要的考量。在傳統(tǒng)PHP架構(gòu)中,任一達(dá)到服務(wù)器所限時這些通常都會成為一個麻煩。

測量PHP內(nèi)部的CPU使用率是難以實現(xiàn)的。如果你確實關(guān)注這一塊,可用考慮在Ubuntu或macOS中使用類似于 top 的命令。對于Windows,則可用考慮使用Linux子系統(tǒng),這樣你就能夠在Ubuntu中使用 top 命令了。

以下是我們用于查看內(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é)尾處使用這些方法,以便于我們了解哪個腳本一次使用了最多的內(nèi)存。

我們有什么選擇?

我們有許多方法來有效地讀取文件。有以下兩種場景會使用到他們。我們可能希望同時讀取和處理所有數(shù)據(jù),對處理后的數(shù)據(jù)進(jìn)行輸出或者執(zhí)行其他操作。 我們還可能希望對數(shù)據(jù)流進(jìn)行轉(zhuǎn)換而不需要訪問到這些數(shù)據(jù)。

想象以下,對于第一種情況,如果我們希望讀取文件并且把每 10,000 行的數(shù)據(jù)交給單獨的隊列進(jìn)行處理。我們則需要至少把 10,000 行的數(shù)據(jù)加載到內(nèi)存中,然后把它們交給隊列管理器(無論使用哪種)。

對于第二種情況,假設(shè)我們想要壓縮一個 API 響應(yīng)的內(nèi)容,這個 API 響應(yīng)特別大。雖然這里我們不關(guān)心它的內(nèi)容是什么,但是我們需要確保它被以一種壓縮格式備份起來。

這兩種情況,我們都需要讀取大文件。不同的是,第一種情況我們需要知道數(shù)據(jù)是什么,而第二種情況我們不關(guān)心數(shù)據(jù)是什么。接下來,讓我們來深入討論一下這兩種做法...

逐行讀取文件

PHP 處理文件的函數(shù)很多,讓我們將其中一些函數(shù)結(jié)合起來實現(xià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";

我們正在閱讀一個包括莎士比亞全部著作的文本文件。該文件大小大約為 5.5 MB。內(nèi)存使用峰值為 12.8 MB?,F(xiàn)在,讓我們使用生成器來讀取每一行:

// 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。這個數(shù)據(jù)意義大不大,因為我們需要加入對文件數(shù)據(jù)的處理。例如,當(dāng)出現(xiàn)兩個空白行時,將文檔拆分為多個塊:

// 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";

有人猜測這次使用多少內(nèi)存嗎?即使我們將文本文檔分為 126 個塊,我們?nèi)匀恢皇褂?459 KB 的內(nèi)存。鑒于生成器的性質(zhì),我們將使用的最大內(nèi)存是在迭代中需要存儲最大文本塊的內(nèi)存。在這種情況下,最大的塊是 101985 個字符。

我已經(jīng)寫過 使用生成器提高性能 以及 生成器擴(kuò)展包,感興趣的可以去查看更多相關(guān)內(nèi)容。

生成器還有其他用途,但顯然它可以很好的讀取大型文件。如果我們需要處理數(shù)據(jù),生成器可能是最好的方法。

文件之間的管道

在不需要處理數(shù)據(jù)的情況下,我們可以將文件數(shù)據(jù)從一個文件傳遞到另一個文件。這通常稱為管道 (大概是因為除了兩端之外,我們看不到管道內(nèi)的任何東西,當(dāng)然,只要它是不透明的)。我們可以通過流(stream)來實現(xiàn),首先,我們編寫一個腳本實現(xiàn)一個文件到另一個文件的傳輸,以便我們可以測量內(nèi)存使用情況:

// from piping-files-1.php

file_put_contents(
    "piping-files-1.txt", file_get_contents("shakespeare.txt")
);

require "memory.php";

結(jié)果并沒有讓人感到意外。該腳本比其復(fù)制的文本文件使用更多的內(nèi)存來運行。這是因為腳本必須在內(nèi)存中讀取整個文件直到將其寫入另外一個文件。對于小的文件而言,這種操作是 OK 的。但是將其用于大文件時,就不是那么回事了。

讓我們嘗試從一個文件流式傳輸(或管道傳輸)到另一個文件:

// 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";

這段代碼有點奇怪。我們打開兩個文件的句柄,第一個處于讀取模式,第二個處于寫入模式。然后,我們從第一個復(fù)制到第二個。我們通過再次關(guān)閉兩個文件來完成。當(dāng)你知道內(nèi)存使用為 393 KB 時,可能會感到驚訝。

這個數(shù)字看起來很熟悉,這不就是利用生成器保存逐行讀取內(nèi)容時所使用的內(nèi)存嗎。這是因為  fgets 的第二個參數(shù)定義了每行要讀取的字節(jié)數(shù)(默認(rèn)為 -1 或到達(dá)新行之前的長度)。

stream_copy_to_stream 的第三個參數(shù)是相同的(默認(rèn)值完全相同)。stream_copy_to_stream 一次從一個流讀取一行,并將其寫入另一流。由于我們不需要處理該值,因此它會跳過生成器產(chǎn)生值的部分

單單傳輸文字還不夠?qū)嵱?,所以考慮下其他例子。假設(shè)我們想從 cdn 輸出圖像,可以用以下代碼來描述

// 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),它們的實際效果是一樣的。

內(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 提供了一種簡單的方法來執(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";

其他流

還存在一些流可以通過管道來讀寫。

  • php://stdin  只讀

  • php://stderr  只寫,與 php://stdout 相似

  • php://input 只讀,使我們可以訪問原始請求內(nèi)容

  • php://output 只寫,可讓我們寫入輸出緩沖區(qū)

  • php://memoryphp://temp (可讀寫) 是臨時存儲數(shù)據(jù)的地方。區(qū)別在于數(shù)據(jù)足夠大時 php:/// temp 就會將數(shù)據(jù)存儲在文件系統(tǒng)中,而php:/// memory將繼續(xù)存儲在內(nèi)存中直到耗盡。

過濾器

我們可以對流使用另一個技巧,稱為過濾器。它介于兩者之間,對數(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)存。我們可以使用過濾器來進(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 過濾器,該過濾器讀取和壓縮資源的內(nèi)容。然后我們可以將該壓縮數(shù)據(jù)通過管道傳輸?shù)搅硪粋€文件中。這僅使用了 896KB 內(nèi)存。

雖然格式不同,或者說使用 zip 壓縮文件有其他諸多好處。但是,你不得不考慮:如果選擇其他格式你可以節(jié)省 12 倍的內(nèi)存,你會不會心動?

要對數(shù)據(jù)進(jìn)行解壓,只需要通過另外一個 zlib 過濾器:

// from filters-2.php

file_get_contents(
    "php://filter/zlib.inflate/resource=filters-2.deflated"
);

關(guān)于流,在 Understanding Streams in PHP 和 Using PHP Streams Effectively 文章中已經(jīng)進(jìn)行了廣泛的討論,如果你想要換個角度思考,可以查看以上這兩篇文章。

自定義流

fopenfile_get_contents 具有它們自己的默認(rèn)選項集,但是它們是完全可定制的。要定義它們,我們需要創(chuàng)建一個新的流上下文

// from creating-contexts-1.php

$data = join("&", [
    "twitter=assertchris",
]);

$headers = join("\r\n", [
    "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ā)送一個 POST 請求給 API。API 端點是安全的,不過我們?nèi)匀皇褂昧?http 上下文屬性(可用于 http 或者 https)。我們設(shè)置了一些頭部,并打開了 API 的文件句柄。我們可以將句柄以只讀方式打開,上下文負(fù)責(zé)編寫。

自定義的內(nèi)容很多,如果你想了解更多信息,可查看對應(yīng) 文檔。

創(chuàng)建自定義協(xié)議和過濾器

在總結(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 )
}

我們并不打算實現(xiàn)其中一個,因為我認(rèn)為它值得擁有自己的教程。有很多工作要做。但是一旦完成工作,我們就可以很容易地注冊流包裝器:

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)建自定義流過濾器。 文檔 有一個示例過濾器類:

Filter {
    public $filtername;
    public $params
    public int filter ( resource $in , resource $out , int &$consumed ,
        bool $closing )
    public void onClose ( void )
    public bool onCreate ( void )
}

可被輕松注冊

$handle = fopen("story.txt", "w+");
stream_filter_append($handle, "highlight-names", STREAM_FILTER_READ);

highlight-names 需要與新過濾器類的 filtername 屬性匹配。還可以在 php:///filter/highligh-names/resource=story.txt 字符串中使用自定義過濾器。定義過濾器比定義協(xié)議要容易得多。原因之一是協(xié)議需要處理目錄操作,而過濾器僅需要處理每個數(shù)據(jù)塊。

如果您愿意,我強(qiáng)烈建議您嘗試創(chuàng)建自定義協(xié)議和過濾器。如果您可以將過濾器應(yīng)用于stream_copy_to_stream操作,則即使處理令人討厭的大文件,您的應(yīng)用程序也將幾乎不使用任何內(nèi)存。想象一下編寫調(diào)整大小圖像過濾器或加密應(yīng)用程序過濾器。

如果你愿意,我強(qiáng)烈建議你嘗試創(chuàng)建自定義協(xié)議和過濾器。如果你可以將過濾器應(yīng)用于 stream_copy_to_stream 操作,即使處理煩人的大文件,你的應(yīng)用程序也幾乎不使用任何內(nèi)存。想象下編寫 resize-image 過濾器和  encrypt-for-application 過濾器吧。

關(guān)于“如何利用PHP讀取大文件”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對“如何利用PHP讀取大文件”知識都有一定的了解,大家如果還想學(xué)習(xí)更多知識,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。

文章標(biāo)題:如何利用PHP讀取大文件
網(wǎng)頁鏈接:http://muchs.cn/article28/jojdcp.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供用戶體驗商城網(wǎng)站、、關(guān)鍵詞優(yōu)化軟件開發(fā)、服務(wù)器托管

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

營銷型網(wǎng)站建設(shè)