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:
File.split(startByte, endByte)
)Mais je ferais mieux d'utiliser quelque chose qui existe déjà pour éviter la croissance d'entropie.
Finalement, j'ai créé un nouveau lecteur ligne par ligne, ce qui est totalement différent du précédent.
Les fonctionnalités sont:
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.
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.
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:
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 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>