llamar - Leyendo el archivo línea por línea en JavaScript en el lado del cliente
llamar javascript desde html (3)
¿Podría por favor ayudarme con el siguiente problema.
Gol
Lea el archivo en el lado del cliente (en el navegador a través de las clases JS y HTML5) línea por línea, sin cargar el archivo completo en la memoria.
Guión
Estoy trabajando en una página web que debería analizar los archivos en el lado del cliente. Actualmente, estoy leyendo el archivo como se describe en este 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);
}
El problema es que FileReader lee el archivo completo a la vez, lo que provoca que la pestaña se bloquee para archivos grandes (tamaño> = 300 MB). El uso de reader.onprogress
no resuelve un problema, ya que solo incrementa el resultado hasta que llega al límite.
Inventar una rueda
He investigado un poco en Internet y no he encontrado una forma sencilla de hacer esto (hay un montón de artículos que describen esta funcionalidad exacta pero en el lado del servidor para node.js).
Como única forma de resolverlo solo veo lo siguiente:
- Dividir el archivo por fragmentos (mediante el
File.split(startByte, endByte)
) - Encuentre el último carácter de nueva línea en ese fragmento (''/ n'')
- Lea ese fragmento, excepto la parte después del último carácter de nueva línea y conviértalo a la cadena y divídalo por líneas
- Lea el siguiente fragmento a partir del último carácter de nueva línea que se encuentra en el paso 2
Pero mejor usaré algo que ya existe para evitar el crecimiento de la entropía.
Finalmente, he creado un nuevo lector línea por línea, que es totalmente diferente del anterior.
Las características son:
- Acceso basado en índices al archivo (secuencial y aleatorio)
- Optimizado para la lectura aleatoria repetida (hitos con compensación de bytes guardados para líneas que ya se navegaron en el pasado), por lo que después de haber leído todo el archivo una vez, acceder a la línea 43422145 será casi tan rápido como acceder a la línea 12.
- Buscando en el archivo: encuentra el siguiente y encuentra todo .
- Índice exacto, desplazamiento y longitud de las coincidencias, para que pueda resaltarlas fácilmente
Compruebe este jsFiddle para ejemplos.
Uso:
// 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) { ... });
El rendimiento es igual a la solución anterior. Puedes medirlo invocando ''Leer'' en jsFiddle.
He escrito un módulo llamado line-reader-browser para el mismo propósito. Utiliza Promises
.
Sintaxis (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)
}
Uso:-
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!"))
}
Intente seguir el fragmento de código para ver cómo funciona el módulo.
<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>
Espero que le ahorre el tiempo a alguien!
Actualización: marque https://github.com/anpur/client-line-navigator/wiki en mi segunda respuesta, ese lector es mucho mejor.
He hecho mi propio lector, que satisface mis necesidades.
Actuación
Como el problema está relacionado solo con los archivos enormes, el rendimiento fue la parte más importante.
Como puede ver, el rendimiento es casi el mismo que la lectura directa (como se describe en la pregunta anterior). Actualmente estoy intentando mejorarlo, ya que el consumidor de mayor tiempo es una llamada asíncrona para evitar el golpe de límite de la pila de llamadas, lo que no es innecesario para el problema de ejecución. Problema de rendimiento resuelto.
Calidad
Los siguientes casos fueron probados:
- Archivo vacío
- Archivo de una sola línea
- Archivo con nueva línea char en el final y sin
- Compruebe las líneas analizadas
- Múltiples carreras en la misma página.
- No hay líneas perdidas ni problemas de orden.
Código y uso
Html:
<input type="file" id="file-test" name="files[]" />
<div id="output-test"></div>
Uso:
$("#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();
});
Código:
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);
}
};
};