Lire le fichier ligne par ligne en JavaScript côté client

Pourriez-vous m'aider avec le prochain problème.

Objectif

Lisez le fichier côté client (dans le navigateur via les classes JS et HTML5) ligne par ligne, sans charger tout le fichier dans la 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 cet 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 tout le fichier à la fois, ce qui provoque un onglet écrasé pour les gros fichiers (taille> = 300 Mo). L'utilisation de reader.onprogress ne résout pas un problème, car il augmente 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 beaucoup d'articles décrivant cette fonctionnalité exacte, mais sur le côté serveur pour node.js).

Comme seul moyen de le résoudre, je ne vois que les suivantes:

  1. Fichier fractionné par blocs (via la File.split(startByte, endByte) )
  2. Trouvez le dernier caractère de ligne dans ce morceau ('/ n')
  3. Lisez ce morceau, à l'exception de la partie après le dernier caractère de la nouvelle ligne et le convertissez en chaîne et divisez par des lignes
  4. Lire le morceau suivant à partir du dernier nouveau caractère de ligne trouvé à l'étape 2

Mais je vais mieux utiliser quelque chose déjà existant pour éviter la croissance de l'entropie.

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

    Les caractéristiques 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'octets enregistrés pour les lignes déjà parcourues dans le passé), donc après avoir lu tous les fichiers une fois, accéder à la ligne 43422145 sera presque aussi rapide que l'accès à la ligne 12.
    • Recherche dans le fichier: recherchez ensuite et trouvez tous .
    • Indice exact, décalage et durée des matches, afin de pouvoir les mettre en surbrillance facilement

    Vérifiez ce 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) { ... }); 

    La performance est identique à la solution précédente. Vous pouvez le mesurer en appelant 'Read' dans jsFiddle.

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

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

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

    Performance

    Comme le problème est lié uniquement aux fichiers énormes, la performance était la partie la plus importante. Entrez la description de l'image ici

    Comme vous pouvez le voir, la performance est presque identique à la lecture directe (comme décrit ci-dessus). À l'heure actuelle, j'essaie de le faire mieux, car un consommateur de plus grande taille est un appel asynchimique pour éviter le succès de la limite de la pile d'appels, ce qui n'est pas nécessaire pour le 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 nouvelle ligne de char sur la fin et sans
    • Vérifier les lignes analysées
    • Plusieurs runs 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); } }; }; 

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

    Syntaxe (type): –

     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 fonctionnement du module.

     <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>