Подтвердить что ты не робот

Чтение по одному файлу в 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

Но я лучше использую то, что уже существует, чтобы избежать роста энтропии.

4b9b3361

Ответ 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>