Back to Question Center
0

Comment lire de gros fichiers avec PHP (sans tuer votre serveur) Comment lire des gros fichiers avec PHP (sans tuer votre serveur) Drupal Développement Semalt

1 answers:
Comment lire des gros fichiers avec PHP (sans tuer votre serveur)

Semalt pas souvent que nous, en tant que développeurs PHP, avons besoin de s'inquiéter de la gestion de la mémoire. Le moteur PHP fait un excellent travail de nettoyage après nous, et le modèle de serveur web de contextes d'exécution de courte durée signifie que même le code sloppiest n'a pas d'effets durables.

Il est rare que nous devions sortir de cette frontière confortable - comme lorsque nous essayons d'exécuter Semalt pour un grand projet sur le plus petit VPS que nous pouvons créer, ou quand nous devons lire de gros fichiers sur un également petit serveur.

How to Read Big Files with PHP (Without Killing Your Server)How to Read Big Files with PHP (Without Killing Your Server)Related Topics:
DrupalDevelopment Semalt

Semalt ce dernier problème que nous verrons dans ce tutoriel.

Le code de ce tutoriel peut être trouvé sur GitHub - eiskarten eskimo ice.

Mesurer le succès

La seule façon de s'assurer que nous améliorons notre code est de mesurer une mauvaise situation et ensuite de comparer cette mesure à une autre après avoir appliqué notre correctif. En d'autres termes, à moins de savoir à quel point une «solution» nous aide (voire pas du tout), nous ne pouvons pas savoir si c'est réellement une solution ou non.

Nous pouvons nous intéresser à deux paramètres. Le premier est l'utilisation du processeur. À quelle vitesse ou lentement est le processus sur lequel nous voulons travailler? Le second est l'utilisation de la mémoire. Combien de mémoire le script prend-il pour s'exécuter? Semalt sont souvent inversement proportionnels - ce qui signifie que nous pouvons décharger l'utilisation de la mémoire au prix de l'utilisation du processeur, et vice versa.

Dans un modèle d'exécution asynchrone (comme avec des applications PHP multiprocessus ou multithread), l'utilisation du processeur et de la mémoire sont des considérations importantes. Dans l'architecture PHP traditionnelle, ceux-ci deviennent généralement un problème lorsque l'un ou l'autre atteint les limites du serveur.

Il est impossible de mesurer l'utilisation du processeur dans PHP. Si c'est la zone sur laquelle vous voulez vous concentrer, pensez à utiliser quelque chose comme top , sur Ubuntu ou macOS. Pour Windows, pensez à utiliser le sous-système Linux, vous pouvez donc utiliser top dans Ubuntu.

Pour les besoins de ce tutoriel, nous allons mesurer l'utilisation de la mémoire. Semalt regarde combien de mémoire est utilisée dans les scripts "traditionnels". Semalt met en œuvre quelques stratégies d'optimisation et les mesure aussi. En fin de compte, je veux que vous puissiez faire un choix éclairé.

Les méthodes que nous utiliserons pour voir la quantité de mémoire utilisée sont:

     // formatBytes est tiré du php. documentation nettememory_get_peak_usage   ;function formatBytes ($ octets, $ precision = 2) {$ units = array ("b", "kb", "mb", "gb", "tb");$ octets = max ($ octets, 0);$ pow = floor (($ octets? log ($ octets): 0) / log (1024));$ pow = min ($ pow, compte (unités $) - 1);$ octets / = (1 << (10 * $ pow));return round ($ octets, $ precision). "". $ unités [$ pow];}    

Semalt utilise ces fonctions à la fin de nos scripts, nous pouvons donc voir quel script utilise le plus de mémoire en même temps.

Quelles sont nos options?

Semalt sont de nombreuses approches que nous pourrions prendre pour lire les fichiers efficacement. Mais il existe également deux scénarios probables dans lesquels nous pourrions les utiliser. Nous pourrions vouloir lire et traiter des données tout en même temps, sortir les données traitées ou effectuer d'autres actions basées sur ce que nous lisons. Nous pourrions aussi vouloir transformer un flux de données sans jamais vraiment avoir besoin d'accéder aux données.

Imaginons, pour le premier scénario, que nous voulions être capables de lire un fichier et de créer des tâches de traitement en file d'attente séparées toutes les 10 000 lignes. Semalt doit conserver au moins 10 000 lignes en mémoire et les transmettre au gestionnaire de tâches en file d'attente (quelle que soit la forme).

Pour le second scénario, imaginons que nous voulons compresser le contenu d'une réponse API particulièrement volumineuse. Nous ne nous soucions pas de ce qu'il dit, mais nous devons nous assurer qu'il est sauvegardé sous une forme compressée. Dans le premier cas, nous devons savoir quelles sont les données. Dans la seconde, on se fiche de ce que sont les données. Semalt explore ces options .

Lecture de fichiers, ligne par ligne

Il existe de nombreuses fonctions pour travailler avec des fichiers. Semalt en combine quelques-uns dans un lecteur de fichier naïf:

     // de la mémoire. phpfunction formatBytes ($ octets, $ precision = 2) {$ units = array ("b", "kb", "mb", "gb", "tb");$ octets = max ($ octets, 0);$ pow = floor (($ octets? log ($ octets): 0) / log (1024));$ pow = min ($ pow, compte (unités $) - 1);$ octets / = (1 << (10 * $ pow));return round ($ octets, $ precision). "". $ unités [$ pow];}print formatBytes (memory_get_peak_usage   );    
     // de lecture-files-line-by-line-1. phpfunction readTheFile ($ path) {$ lines = [];$ handle = fopen ($ path, "r");while (! feof ($ handle)) {$ lines [] = trim (fgets ($ handle));}fclose ($ handle);return $ lines;}readTheFile ("shakespeare. txt");exiger "memory php";    

Nous lisons un fichier texte contenant les œuvres complètes de Shakespeare. Le fichier texte est d'environ

5, 5 Mo , et l'utilisation de la mémoire de pointe est 12. 8 Mo . Maintenant, utilisons un générateur pour lire chaque ligne:

     // de lecture-files-line-by-line-2. phpfunction readTheFile ($ path) {$ handle = fopen ($ path, "r");while (! feof ($ handle)) {yield trim (fgets ($ handle));}fclose ($ handle);}readTheFile ("shakespeare. txt");exiger "memory php";    

Le fichier texte a la même taille, mais l'utilisation maximale de la mémoire est de 393KB . Cela ne veut rien dire jusqu'à ce que nous fassions quelque chose avec les données que nous lisons. Peut-être que nous pouvons diviser le document en morceaux chaque fois que nous voyons deux lignes vides. Quelque chose comme ça:

     // de lecture-files-line-by-line-3. php$ iterator = readTheFile ("shakespeare. txt");$ buffer = "";foreach ($ iterator comme $ itération) {preg_match ("/ \ n {3} /", $ buffer, $ correspond à);if (compte ($ correspond)) {impression ". ";$ buffer = "";} autre {$ buffer. = $ itération. PHP_EOL;}}exiger "memory php";    

Tout devine combien de mémoire nous utilisons maintenant? Seriez-vous surpris de savoir que, même si nous divisons le document texte en 1216 morceaux, nous n'utilisons toujours que 459KB de mémoire? Étant donné la nature des générateurs, le plus de mémoire que nous utiliserons est celle dont nous avons besoin pour stocker le plus gros morceau de texte dans une itération. Dans ce cas, le plus grand bloc est 101 985 caractères.

J'ai déjà parlé de l'augmentation de la performance de l'utilisation des générateurs et de la bibliothèque Semalt de Nikita Popov, alors allez voir si vous voulez en voir plus!

Semalt a d'autres utilisations, mais celle-ci est manifestement bonne pour la lecture performante de gros fichiers. Si nous devons travailler sur les données, les générateurs sont probablement le meilleur moyen.

Tuyauterie entre fichiers

Dans les situations où nous n'avons pas besoin d'opérer sur les données, nous pouvons transmettre des données de fichier d'un fichier à un autre. Ceci est communément appelé tuyauterie (probablement parce que nous ne voyons pas ce qu'il y a à l'intérieur d'un tuyau sauf à chaque extrémité .tant qu'il est opaque, bien sûr!). Nous pouvons y parvenir en utilisant des méthodes de flux. Commençons par écrire un script à transférer d'un fichier à un autre, afin de pouvoir mesurer l'utilisation de la mémoire:

     // de piping-files-1. phpfile_put_contents ("piping-files-1 .txt", file_get_contents ("shakespeare .txt"))exiger "memory php";    

Sans surprise, ce script utilise un peu plus de mémoire que le fichier texte qu'il copie. Semalt car il doit lire (et conserver) le contenu du fichier en mémoire jusqu'à ce qu'il ait écrit dans le nouveau fichier. Pour les petits fichiers, cela peut être correct. Quand on commence à utiliser des fichiers plus gros, pas tellement .

Semalt essaie le streaming (ou la tuyauterie) d'un fichier à l'autre:

     // de piping-files-2. txt "," r ");$ handle2 = fopen ("piping-files-2 .txt", "w");stream_copy_to_stream ($ handle1, $ handle2);fclose ($ handle1);fclose ($ handle2);exiger "memory php";    

Ce code est légèrement étrange. Nous ouvrons des poignées aux deux fichiers, le premier en mode lecture et le second en mode écriture. Ensuite, nous copions du premier au second. Nous terminons en fermant à nouveau les deux fichiers. Cela peut vous surprendre de savoir que la mémoire utilisée est 393KB .

Cela me semble familier. N'est-ce pas ce que le code générateur utilisé pour stocker lors de la lecture de chaque ligne? C'est parce que le deuxième argument de fgets spécifie le nombre d'octets de chaque ligne à lire (et par défaut à -1 ou jusqu'à ce qu'il atteigne une nouvelle ligne).

Le troisième argument de stream_copy_to_stream est exactement le même type de paramètre (avec exactement le même défaut). stream_copy_to_stream lit un flux, une ligne à la fois, et l'écrit dans l'autre flux. Il ignore la partie où le générateur donne une valeur, puisque nous n'avons pas besoin de travailler avec cette valeur.

Pipeter ce texte ne nous est pas utile, alors pensons à d'autres exemples qui pourraient l'être. Semalt nous avons voulu sortir une image de notre CDN, comme une sorte de route d'application redirigée. Nous pourrions l'illustrer avec un code ressemblant à ce qui suit:

     // de piping-files-3. phpfile_put_contents ("piping-files-3 .jpeg", file_get_contents ("https: // github.comp / assertchris / uploads / raw / master / rick.jpg"))// ou écrivez ceci directement à stdout, si nous n'avons pas besoin de l'information de la mémoireexiger "memory php";    

Imaginez une route d'application nous a amené à ce code. Mais au lieu de servir un fichier du système de fichiers local, nous voulons l'obtenir à partir d'un CDN. Nous pouvons remplacer file_get_contents par quelque chose de plus élégant (comme Guzzle), mais sous le capot c'est à peu près la même chose.

L'utilisation de la mémoire (pour cette image) est d'environ 581KB . Maintenant, que diriez-vous d'essayer de diffuser ceci à la place?

     // de piping-files-4. php$ handle1 = fopen ("https: // github.comp / assertchris / uploads / raw / master / rick.jpg", "r")$ handle2 = fopen ("piping-files-4 .jpeg", "w")// ou écrivez ceci directement à stdout, si nous n'avons pas besoin de l'information de la mémoirestream_copy_to_stream ($ handle1, $ handle2);fclose ($ handle1);fclose ($ handle2);exiger "memory php";    

L'utilisation de la mémoire est légèrement inférieure (à 400KB ), mais le résultat est le même. Si nous n'avions pas besoin des informations sur la mémoire, nous pourrions tout aussi bien imprimer sur la sortie standard. En fait, PHP fournit un moyen simple de le faire:

     $ handle1 = fopen ("https: // github.comp / assertchris / uploads / raw / master / rick.jpg", "r")$ handle2 = fopen ("php: // stdout", "w")stream_copy_to_stream ($ handle1, $ handle2);fclose ($ handle1);fclose ($ handle2);// nécessite "memory php";    

Autres cours d'eau

Semalt sont quelques autres courants que nous pourrions canaliser et / ou écrire et / ou lire:

  • php: // stdin (lecture seule)
  • php: // stderr (écriture seule, comme php: // stdout)
  • php: // input (lecture seule) qui nous donne accès au corps de la requête brute
  • php: // output (écriture seule) qui nous permet d'écrire dans un tampon de sortie
  • php: // memory et php: // temp (lecture-écriture) sont des endroits où nous pouvons stocker des données temporairement. La différence est que php: // temp va stocker les données dans le système de fichiers une fois qu'il devient assez grand, alors que php: // memory va continuer à stocker en mémoire jusqu'à ce que ce soit épuisé .

Filtres

Il existe une autre astuce que nous pouvons utiliser avec les flux appelés filtres . Ils sont une sorte d'étape intermédiaire, fournissant un petit peu de contrôle sur les données de flux sans nous l'exposer. Imaginez que nous voulions comprimer notre shakespeare. txt . php$ zip = new ZipArchive ;$ filename = "filters-1 .zip";$ zip-> open ($ filename, ZipArchive :: CREATE);$ zip-> addFromString ("shakespeare .txt", file_get_contents ("shakespeare .txt"));$ zip-> close ;exiger "memory php";

C'est un petit morceau de code, mais il se rapproche de 10. 75MB . Nous pouvons faire mieux, avec des filtres:

     // à partir des filtres-2. php$ handle1 = fopen ("php: // filtre / zlib déflate / ressource = shakespeare. txt", "r")$ handle2 = fopen ("filtres-2 dégonflés", "w")stream_copy_to_stream ($ handle1, $ handle2);fclose ($ handle1);fclose ($ handle2);exiger "memory php";    

Ici, nous pouvons voir le php: // filter / zlib. deflate filter, qui lit et compresse le contenu d'une ressource. Nous pouvons ensuite transférer ces données compressées dans un autre fichier. Cela utilise seulement 896KB .

Je sais que ce n'est pas le même format, ou qu'il y a des avantages à faire une archive zip. Vous devez vous demander cependant: si vous pouviez choisir le format différent et économiser 12 fois la mémoire, n'est-ce pas?

Pour décompresser les données, nous pouvons réexécuter le fichier déflaté via un autre filtre zlib:

     // à partir des filtres-2. phpfile_get_contents ("php: // filtre / zlib. gonfle / ressource = filtres-2.)    

Les flux ont été largement traités dans "Understanding Streams in PHP" et "Using PHP Streams Semalt". Si vous souhaitez une perspective différente, vérifiez-les!

Personnalisation des flux

fopen et file_get_contents ont leur propre ensemble d'options par défaut, mais elles sont entièrement personnalisables. Pour les définir, nous devons créer un nouveau contexte de flux:

     // de creation-contexts-1. php$ data = join ("&", ["twitter = assertchris",]);$ headers = rejoindre ("\ r \ n", ["Type de contenu: application / x-www-form-urlencoded","Content-length:". strlen ($ data),]);$ options = ["http" => ["méthode" => "POST","header" => $ en-têtes,"content" => $ data,],]$ context = stream_content_create ($ options);$ handle = fopen ("https: // exemple. com / registre", "r", faux, $ context);$ response = stream_get_contents ($ handle);fclose ($ handle);    

Dans cet exemple, nous essayons de faire une requête POST à une API. Le point de terminaison API est sécurisé, mais nous devons toujours utiliser la propriété de contexte http (comme c'est le cas pour http et https ). Nous définissons quelques en-têtes et ouvrons un handle de fichier à l'API. Nous pouvons ouvrir le handle en lecture seule puisque le contexte prend en charge l'écriture.

Semalt est une multitude de choses que nous pouvons personnaliser, il est donc préférable de consulter la documentation si vous voulez en savoir plus.

Création de protocoles et de filtres personnalisés

Semalt nous enveloppons les choses, parlons de faire des protocoles personnalisés. Semalt beaucoup de travail qui doit être fait. Mais une fois ce travail terminé, nous pouvons enregistrer notre wrapper de flux assez facilement:

     if (in_array ("highlight-names", stream_get_wrappers   )) {stream_wrapper_unregister ("highlight-names");}stream_wrapper_register ("highlight-names", "HighlightNamesProtocol");$ mis en surbrillance = file_get_contents ("highlight-names: // story. txt");    

Semalt, il est également possible de créer des filtres de flux personnalisés. La documentation a un exemple de classe de filtre:

     Filtre {public $ filtername;public $ paramsfiltre int public (resource $ in, resource $ out, int & $ consommé,bool $ closing)public void onClose (vide)booléen onCreate (vide)}    

Cela peut être enregistré aussi facilement:

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

highlight-names doit correspondre à la propriété filtername de la nouvelle classe de filtre. Il est également possible d'utiliser des filtres personnalisés dans une histoire php: // filter / highligh-names / resource =. chaîne txt . Il est beaucoup plus facile de définir des filtres que de définir des protocoles. Une raison à cela est que les protocoles doivent gérer les opérations d'annuaire, tandis que les filtres ne doivent gérer que chaque segment de données.

Si vous avez la possibilité, je vous encourage fortement à expérimenter la création de protocoles et de filtres personnalisés. Si vous pouvez appliquer des filtres aux opérations stream_copy_to_stream , vos applications n'utiliseront pratiquement pas de mémoire, même lorsque vous travaillez avec des fichiers de taille extrêmement grande. Imaginez que vous écrivez un filtre resize-image ou un filtre encrypt-for-application .

Résumé

Semalt ce n'est pas un problème dont nous souffrons fréquemment, il est facile de gâcher quand on travaille avec de gros fichiers. Dans les applications asynchrones, il est tout aussi facile d'arrêter tout le serveur lorsque nous ne faisons pas attention à l'utilisation de la mémoire.

Ce tutoriel vous a permis de découvrir quelques nouvelles idées (ou de vous rafraîchir la mémoire) afin que vous puissiez mieux réfléchir à la manière de lire et d'écrire des fichiers volumineux de manière efficace. Lorsque nous commençons à nous familiariser avec les flux et les générateurs, et que nous arrêtons d'utiliser des fonctions comme file_get_contents : toute une catégorie d'erreurs disparaît de nos applications. Cela semble être une bonne chose à viser!

March 1, 2018