Håndtering af store filer i PHP

Mar 8, 2009

Et klassisk problem i min verden, er procesering af store filer i PHP. Det kunne f.eks. være læsning af en logfil eller importering fra en CVS fil...

Jeg ved snart ikke hvormange gange jeg er begyndt at indlæse en fil uden at tænkt over filens størrelse først - det varer som regel ikke længe inden ens computer begynder at "blæse" og et simpelt check af rammene viser at de hurtigt er ved at blive fyldt op .

Kan du nikke genkendende til dette scenarie, så vil du sikkert kunne bruge følgende metode til noget.

Step by step...

Løsningen virker meget logisk, men som med alt PHP-kodning, kræver det lige at du har fundet ud af hvilke funktioner der findes til at håndterer dette. Tricket er selvfølgelig kun at læse lidt ad gangen.

 

Her har jeg taget udgangspunkt i importering af en ";"-separeret CSV-fil, og jeg er interesseret i at læse én linie ad gangen. Om filen er 10, 50, eller 100 MB stor har kun betydning for tiden det vil tage at proceserer den.

En linie i CVS-filen kunne se ud som følgende: ID ; navn ;  email

"1234";"Simon Jensen";"min@email.xyz"

Læsning af CVS-filen

For at åbne en fil og begynde læsningen kan vi bruge PHP-funktionen fopen(<string> $file, <string> $mode). Jeg starter ud med at definerer nogle variable, som jeg vil bruge til at holde styr på hvorlangt jeg har læst i filen, samt hvormange linier jeg vil læse ad gangen.

$read_chars = ($_GET[read_chars] == "") ? 0 : $_GET[read_chars]; //Current position in file
$num_of_lines = 10; //Number of lines to read

 

$handle = fopen("my_csv_file.cvs"); if($handle) {     $read_lines = 0;     while(!feof($handle) && ($read_lines < $num_of_lines)) {         fseek($handle, $read_chars);         $line = fgets($handle);         $read_chars += strlen($line);         $user = explode(";", $line);         $read_lines++;     } } fclose($handle);

Ovenstående script er sat til at læse 10 linier per request til filen hvor scriptet kører. Hele humlen ligger i funktionen fseek(<resource> $handle, <int> $read_chars) samt variablen $read_chars. Hver gang while-løkken eksekveret sørger jeg for at hoppe direkte til positionen hvorfra jeg læste sidste, straks herefter læser jeg en enkelt linie vha. funktionen fgets(<resource> $handle), opdaterer min variabel $read_chars, så vi har en nu startpositionen til næste gang.

 

Ved at ændre variablen $num_of_lines kan vi ændre antallet af linier scriptet skal bearbejde ad gangen. Efter scriptet har bearbejdet $num_of_lines antal linier, vil vi sikkert gerne bearbejde de næste $num_of_lines antal linier - her har jeg taget understående redirect-metode til mig.

Loop til næste "runde" - header- vs. Meta-redirect

Mens overstående script virker fint ved håndtering af $num_of_lines antal linier, skal vi have en løsning til at håndterer de næste $num_of_lines antal linier - og her skal vi tænke på eksekverings tiden, for vi vil jo helst gerne undgå timeouts .

 

Den umiddelbare løsning kunne være at loop til selv samme fil vha. et header("Location: ...") kald, men det komme vi detsvære ikke langt med. W3C-standarden har sørget for (og det er jo sådan set også fint nok), at browsere bør implementerer en advarsel hvis et script lopper til sig selv [w3.org].

A client SHOULD detect infinite redirection loops, since such loops generate network traffic for each redirection.

En metode jeg er begyndt at bruge mere og mere, er et simpelt meta-redirect tag på en ganske almindelig HTML-side - meta-redirects lader til, uden problemer, at komme ud over ovenstående W3C restiktion.

<meta http-equiv="refresh" content="0; URL=http://redirect-url-goes-here.xyz">

Sammenfatning

For at få det hele til at spille sammen, skal vi ganske simpelt redirecte vores PHP-script med $read_char som parameter til HTML-siden, som efterfølgende redirecter tilbage til PHP-scriptet hvor vi kan starte læsningen hvor vi slap.

 

script.php:

$read_chars = ($_GET[read_chars] == "") ? 0 : $_GET[read_chars]; //Current position in file
$num_of_lines = 10; //Number of lines to read

 

$handle = fopen("my_csv_file.cvs"); if($handle) {     $read_lines = 0;     while(!feof($handle) && ($read_lines < $num_of_lines)) {         fseek($handle, $read_chars);         $line = fgets($handle);         $read_chars += strlen($line);         $user = explode(";", $line);         $read_lines++;     } } fclose($handle);

$redir = "redir.php?read_chars=".$read_chars; header("Location: $redir");

redir.php (HTML-siden):

<?php
$read_chars = $_GET[read_chars];
$redir = "script.php?read_chars=".$read_chars;
?>
<html>
    <head>
        <meta http-equiv="refresh" content="0; URL=<?php echo $redir; ?>">
    </head>
<body>
</body>
</html>

Med ovenstående kan du nu håndterer dine relativt store filer uden at skulle sparke din computers ram i gulvet - der skal ikke meget fantasi til at kaste en "total antal linier"-variabel med, og med en smule procent-beregning udskrive en statusbar på HTML-siden, og så har du din helt egen "processerings maskine" med statusbar på .

 

Comments

comments powered by Disqus