Чтение по одному файлу в JavaScript на стороне клиента
Не могли бы вы помочь мне со следующей проблемой.
Цель
Прочитайте файл на стороне клиента (в браузере через классы JS и HTML5), за строкой, без загрузки всего файла в память.
Сценарий
Я работаю на веб-странице, которая должна анализировать файлы на стороне клиента. В настоящее время я читаю файл, как описано в этой статье .
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);
}
Проблема заключается в том, что FileReader считывает весь файл сразу, что приводит к сбою вкладок для больших файлов (размеp >= 300 МБ). Использование reader.onprogress
не решает проблему, так как она просто увеличивает результат, пока он не достигнет предела.
Изобретая колесо
Я провел некоторое исследование в Интернете и не нашел простого способа сделать это (есть куча статей, описывающих эту точную функциональность, но на стороне сервера для node.js).
Как единственный способ его решить, я вижу только следующее:
- Разделить файл кусками (с помощью метода
File.split(startByte, endByte)
)
- Найти последний новый символ строки в этом фрагменте ('/n')
- Прочитайте этот фрагмент за исключением части после последнего нового символа строки и преобразуйте его в строку и разделите по строкам
- Прочитайте следующий фрагмент, начиная с последнего нового символа линии, найденного на шаге 2
Но я лучше использую то, что уже существует, чтобы избежать роста энтропии.
Ответы
Ответ 1
В конце концов я создал новый линейный считыватель, который полностью отличается от предыдущего.
Особенности:
- Индексный доступ к файлу (последовательный и случайный)
- Оптимизировано для повторного произвольного чтения (вехи с смещением байта, сохраненные для уже проложенных ранее строк), поэтому после того, как вы прочитали весь файл один раз, доступ к строке 43422145 будет почти таким же быстрым, как доступ к строке 12.
- Поиск в файле: найти следующий и найти все.
- Точный индекс, смещение и длина совпадений, поэтому вы можете легко выделить их
Для примера рассмотрите jsFiddle.
Использование:
// 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) { ... });
Производительность аналогична предыдущему решению. Вы можете измерить его, используя "Read" в jsFiddle.
GitHub: https://github.com/anpur/client-line-navigator/wiki
Ответ 2
Обновление: проверьте LineNavigator из моего второго ответа вместо этого, что читатель лучше.
Я сделал своего собственного читателя, который удовлетворяет мои потребности.
Производительность
Поскольку проблема связана только с огромной производительностью файлов, это самая важная часть.
![enter image description here]()
Как вы можете видеть, производительность почти такая же, как прямое чтение (как описано в вопросе выше).
В настоящее время я пытаюсь сделать это лучше, так как более крупный потребитель времени - это асинхронный вызов, чтобы избежать превышения предельного уровня стека вызовов, что не является ненужным для проблемы с выполнением. Проблема с производительностью решена.
Качество
Были проверены следующие случаи:
- Пустой файл
- Одиночный файл
- Файл с новой строкой char в конце и без
- Проверить проанализированные строки
- Несколько запусков на одной странице
- Никакие строки не теряются и проблем с порядком
Код и использование
Html:
<input type="file" id="file-test" name="files[]" />
<div id="output-test"></div>
Использование:
$("#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();
});
код:
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);
}
};
};
Ответ 3
Я написал модуль с именем line-reader-browser с той же целью. Он использует Promises
.
Синтаксис (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)
}
Использование: -
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!"))
}
Попробуйте выполнить фрагмент кода, чтобы увидеть, как работает модуль.
<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>