web-dev-qa-db-ja.com

クライアント側のJavaScriptでの行ごとのファイルの読み取り

次の問題で私を助けてください。

目標

ファイル全体をメモリにロードせずに、クライアント側(JSおよびHTML5クラス経由のブラウザ)でファイルを1行ずつ読み取ります。

シナリオ

クライアント側でファイルを解析するWebページで作業しています。現在、この 記事 で説明されているようにファイルを読んでいます。

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が一度にファイル全体を読み取るため、大きなファイル(サイズ> = 300 MB)のタブがクラッシュすることです。 _reader.onprogress_を使用しても、制限に達するまで結果が増加するだけなので、問題は解決しません。

車輪の発明

私はインターネットでいくつかの研究を行ってきましたが、これを行う簡単な方法は見つかりませんでした(node.jsのサーバー側でこの正確な機能を説明する記事がたくさんあります)。

それを解決する唯一の方法として、私は以下だけを見ます:

  1. チャンクでファイルを分割(File.split(startByte, endByte)メソッド経由)
  2. そのチャンク内の最後の改行文字を見つけます( '/ n')
  3. 最後の改行文字の後の部分を除くそのチャンクを読み取り、それを文字列に変換し、行で分割します
  4. ステップ2で見つかった最後の改行文字から始まる次のチャンクを読み取ります

しかし、エントロピーの成長を避けるために、既存のものを使用する方が良いでしょう。

20
Anton Purin

最終的に、新しい行ごとのリーダーを作成しましたが、これは以前のリーダーとはまったく異なります。

機能は次のとおりです。

  • ファイルへのインデックスベースのアクセス(シーケンシャルおよびランダム)
  • 繰り返しランダム読み取り用に最適化されているため(過去にナビゲートされた行にバイトオフセットが保存されたマイルストーン)、すべてのファイルを一度読み取った後、行43422145へのアクセスは行12へのアクセスとほぼ同じ速度になります。
  • ファイル内の検索:find nextおよびfind all
  • 正確なインデックス、オフセット、一致の長さ、それらを簡単に強調表示できます

これを確認してください 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) { ... });

パフォーマンスは以前のソリューションと同じです。 jsFiddleで「読み取り」を呼び出して測定できます。

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

13
Anton Purin

更新: LineNavigator 代わりに私の2番目の回答から、その読者の方がはるかに優れていることを確認してください。

私は自分のリーダーを作りました。それは私のニーズを満たします。

パフォーマンス

問題は巨大なファイルにのみ関係しているため、パフォーマンスが最も重要な部分でした。 enter image description here

ご覧のとおり、パフォーマンスは直接読み取りとほぼ同じです(上記の質問で説明)。 現在、私はそれを改善しようとしています。より長い時間の消費者は、呼び出しスタック制限ヒットを避けるために非同期呼び出しであるため、実行の問題には不要ではありません。 パフォーマンスの問題が解決しました。

品質

次のケースがテストされました。

  • 空のファイル
  • 単一行ファイル
  • 末尾に改行文字があり、なしのファイル
  • 解析された行を確認する
  • 同じページでの複数の実行
  • 行が失われることはなく、注文の問題もありません

コードと使用法

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);
        }
    };
};
8
Anton Purin

同じ目的で 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>

1
Vikas Gautam