web-dev-qa-db-fra.com

Lecture de fichiers ligne par ligne en JavaScript côté client

Pourriez-vous s'il vous plaît m'aider avec le problème suivant.

Objectif

Lire le fichier côté client (dans le navigateur via les classes JS et HTML5) ligne par ligne, sans charger le fichier entier en mémoire.

Scénario

Je travaille sur une page Web qui devrait analyser les fichiers côté client. Actuellement, je lis le fichier tel qu'il est décrit dans ce article .

HTML:

<input type="file" id="files" name="files[]" />

JavaScript:

$("#files").on('change', function(evt){
    // creating FileReader
    var reader = new FileReader();

    // assigning handler
    reader.onloadend = function(evt) {      
        lines = evt.target.result.split(/\r?\n/);

        lines.forEach(function (line) {
            parseLine(...);
        }); 
    };

    // getting File instance
    var file = evt.target.files[0];

    // start reading
    reader.readAsText(file);
}

Le problème est que FileReader lit le fichier entier à la fois, ce qui provoque un blocage de l'onglet pour les gros fichiers (taille> = 300 Mo). L'utilisation de reader.onprogress Ne résout pas un problème, car il incrémente simplement un résultat jusqu'à ce qu'il atteigne la limite.

Inventer une roue

J'ai fait des recherches sur Internet et je n'ai trouvé aucun moyen simple de le faire (il y a un tas d'articles décrivant cette fonctionnalité exacte mais côté serveur pour node.js).

Comme seul moyen de le résoudre, je ne vois que ce qui suit:

  1. Diviser le fichier par morceaux (via la méthode File.split(startByte, endByte))
  2. Trouver le dernier nouveau caractère de ligne dans ce morceau ('/ n')
  3. Lisez ce morceau sauf la partie après le dernier nouveau caractère de ligne et convertissez-le en chaîne et divisé par lignes
  4. Lire le morceau suivant à partir du dernier nouveau caractère de ligne trouvé à l'étape 2

Mais je ferais mieux d'utiliser quelque chose qui existe déjà pour éviter la croissance d'entropie.

20
Anton Purin

Finalement, j'ai créé un nouveau lecteur ligne par ligne, ce qui est totalement différent du précédent.

Les fonctionnalités sont:

  • Accès indexé au fichier (séquentiel et aléatoire)
  • Optimisé pour la lecture aléatoire répétée (jalons avec décalage d'octet enregistrés pour les lignes déjà parcourues dans le passé), donc après avoir lu tous les fichiers une fois, l'accès à la ligne 43422145 sera presque aussi rapide que l'accès à la ligne 12.
  • Recherche dans un fichier: trouver ensuite et trouver tout .
  • Index, décalage et longueur exacts des correspondances, afin que vous puissiez facilement les mettre en évidence

Vérifiez ceci jsFiddle pour des exemples.

Usage:

// Initialization
var file; // HTML5 File object
var navigator = new FileNavigator(file);

// Read some amount of lines (best performance for sequential file reading)
navigator.readSomeLines(startingFromIndex, function (err, index, lines, eof, progress) { ... });

// Read exact amount of lines
navigator.readLines(startingFromIndex, count, function (err, index, lines, eof, progress) { ... });

// Find first from index
navigator.find(pattern, startingFromIndex, function (err, index, match) { ... });

// Find all matching lines
navigator.findAll(new RegExp(pattern), indexToStartWith, limitOfMatches, function (err, index, limitHit, results) { ... });

Les performances sont identiques à la solution précédente. Vous pouvez le mesurer en invoquant "Lire" dans jsFiddle.

GitHub: https://github.com/anpur/client-line-navigator/wiki

13
Anton Purin

Mise à jour: vérifiez LineNavigator de ma deuxième réponse à la place, ce lecteur est bien meilleur.

J'ai fait mon propre lecteur, qui répond à mes besoins.

Performances

Comme le problème n'est lié qu'aux fichiers volumineux, les performances étaient la partie la plus importante. enter image description here

Comme vous pouvez le voir, les performances sont presque les mêmes que la lecture directe (comme décrit dans la question ci-dessus). Actuellement, j'essaye de l'améliorer, car le plus grand consommateur de temps est un appel asynchrone pour éviter le hit de limite de pile d'appels, ce qui n'est pas inutile pour un problème d'exécution. Problème de performance résolu.

Qualité

Les cas suivants ont été testés:

  • Fichier vide
  • Fichier à ligne unique
  • Fichier avec un nouveau caractère de ligne à la fin et sans
  • Vérifier les lignes analysées
  • Plusieurs exécutions sur la même page
  • Aucune ligne n'est perdue et aucun problème de commande

Code et utilisation

Html:

<input type="file" id="file-test" name="files[]" />
<div id="output-test"></div>

Usage:

$("#file-test").on('change', function(evt) {
    var startProcessing = new Date();
    var index = 0;
    var file = evt.target.files[0];
    var reader = new FileLineStreamer();
    $("#output-test").html("");

    reader.open(file, function (lines, err) {
        if (err != null) {
            $("#output-test").append('<span style="color:red;">' + err + "</span><br />");
            return;
        }
        if (lines == null) {
            var milisecondsSpend = new Date() - startProcessing;
            $("#output-test").append("<strong>" + index + " lines are processed</strong> Miliseconds spend: " + milisecondsSpend + "<br />");           
            return;
        }

        // output every line
        lines.forEach(function (line) {
            index++;
            //$("#output-test").append(index + ": " + line + "<br />");
        });

        reader.getNextLine();
    });

    reader.getNextLine();   
});

Code:

function FileLineStreamer() {   
    var loopholeReader = new FileReader();
    var chunkReader = new FileReader(); 
    var delimiter = "\n".charCodeAt(0); 

    var expectedChunkSize = 15000000; // Slice size to read
    var loopholeSize = 200;         // Slice size to search for line end

    var file = null;
    var fileSize;   
    var loopholeStart;
    var loopholeEnd;
    var chunkStart;
    var chunkEnd;
    var lines;
    var thisForClosure = this;
    var handler;

    // Reading of loophole ended
    loopholeReader.onloadend = function(evt) {
        // Read error
        if (evt.target.readyState != FileReader.DONE) {
            handler(null, new Error("Not able to read loophole (start: )"));
            return;
        }
        var view = new DataView(evt.target.result);

        var realLoopholeSize = loopholeEnd - loopholeStart;     

        for(var i = realLoopholeSize - 1; i >= 0; i--) {                    
            if (view.getInt8(i) == delimiter) {
                chunkEnd = loopholeStart + i + 1;
                var blob = file.slice(chunkStart, chunkEnd);
                chunkReader.readAsText(blob);
                return;
            }
        }

        // No delimiter found, looking in the next loophole
        loopholeStart = loopholeEnd;
        loopholeEnd = Math.min(loopholeStart + loopholeSize, fileSize);
        thisForClosure.getNextBatch();
    };

    // Reading of chunk ended
    chunkReader.onloadend = function(evt) {
        // Read error
        if (evt.target.readyState != FileReader.DONE) {
            handler(null, new Error("Not able to read loophole"));
            return;
        }

        lines = evt.target.result.split(/\r?\n/);       
        // Remove last new line in the end of chunk
        if (lines.length > 0 && lines[lines.length - 1] == "") {
            lines.pop();
        }

        chunkStart = chunkEnd;
        chunkEnd = Math.min(chunkStart + expectedChunkSize, fileSize);
        loopholeStart = Math.min(chunkEnd, fileSize);
        loopholeEnd = Math.min(loopholeStart + loopholeSize, fileSize);

        thisForClosure.getNextBatch();
    };

    this.getProgress = function () {
        if (file == null)
            return 0;
        if (chunkStart == fileSize)
            return 100;         
        return Math.round(100 * (chunkStart / fileSize));
    }

    // Public: open file for reading
    this.open = function (fileToOpen, linesProcessed) {
        file = fileToOpen;
        fileSize = file.size;
        loopholeStart = Math.min(expectedChunkSize, fileSize);
        loopholeEnd = Math.min(loopholeStart + loopholeSize, fileSize);
        chunkStart = 0;
        chunkEnd = 0;
        lines = null;
        handler = linesProcessed;
    };

    // Public: start getting new line async
    this.getNextBatch = function() {
        // File wasn't open
        if (file == null) {     
            handler(null, new Error("You must open a file first"));
            return;
        }
        // Some lines available
        if (lines != null) {
            var linesForClosure = lines;
            setTimeout(function() { handler(linesForClosure, null) }, 0);
            lines = null;
            return;
        }
        // End of File
        if (chunkStart == fileSize) {
            handler(null, null);
            return;
        }
        // File part bigger than expectedChunkSize is left
        if (loopholeStart < fileSize) {
            var blob = file.slice(loopholeStart, loopholeEnd);
            loopholeReader.readAsArrayBuffer(blob);
        }
        // All file can be read at once
        else {
            chunkEnd = fileSize;
            var blob = file.slice(chunkStart, fileSize);
            chunkReader.readAsText(blob);
        }
    };
};
8
Anton Purin

J'ai écrit un module nommé line-reader-browser dans le même but. Il utilise Promises.

Syntaxe (TypeScript): -

import { LineReader } from "line-reader-browser"

// file is javascript File Object returned from input element
// chunkSize(optional) is number of bytes to be read at one time from file. defaults to 8 * 1024
const file: File
const chunSize: number
const lr = new LineReader(file, chunkSize)

// context is optional. It can be used to inside processLineFn   
const context = {}
lr.forEachLine(processLineFn, context)
  .then((context) => console.log("Done!", context))

// context is same Object as passed while calling forEachLine
function processLineFn(line: string, index: number, context: any) {
   console.log(index, line)
}

Usage:-

import { LineReader } from "line-reader-browser"

document.querySelector("input").onchange = () => {
   const input = document.querySelector("input")
   if (!input.files.length) return
   const lr = new LineReader(input.files[0], 4 * 1024)
   lr.forEachLine((line: string, i) => console.log(i, line)).then(() => console.log("Done!"))
}

Essayez l'extrait de code suivant pour voir le module fonctionner.

<html>
   <head>
      <title>Testing line-reader-browser</title>
   </head>
   <body>
      <input type="file">
      <script src="https://cdn.rawgit.com/Vikasg7/line-reader-browser/master/dist/tests/bundle.js"></script>
   </body>
</html>

1
Vikas Gautam