【问题标题】:Reading line-by-line file in JavaScript on client side在客户端用 JavaScript 逐行读取文件
【发布时间】:2025-11-21 07:35:02
【问题描述】:

您能帮我解决以下问题吗?

目标

在客户端逐行读取文件(在浏览器中通过 JS 和 HTML5 类),无需将整个文件加载到内存中。

场景

我正在处理应该在客户端解析文件的网页。目前,我正在阅读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);
}

问题在于 FileReader 一次读取整个文件,这会导致大文件(大小 >= 300 MB)的选项卡崩溃。使用reader.onprogress 并不能解决问题,因为它只会增加结果直到达到极限。

发明*

我在互联网上做了一些研究,但没有找到简单的方法来做到这一点(有很多文章描述了这个确切的功能,但在 node.js 的服务器端)。

作为解决它的唯一方法,我只看到以下内容:

  1. 按块分割文件(通过File.split(startByte, endByte) 方法)
  2. 在该块中查找最后一个换行符 ('/n')
  3. 读取该块,除了最后一个换行符之后的部分,并将其转换为字符串并按行分割
  4. 从第 2 步中找到的最后一个换行符开始读取下一个块

但我最好使用已经存在的东西来避免熵增长。

【问题讨论】:

    标签: javascript html client-side filereader


    【解决方案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) { ... });
    

    性能与之前的解决方案相同。您可以在 jsFiddle 中调用“读取”来测量它。

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

    【讨论】:

    • npm 包即将推出
    【解决方案2】:

    更新:请检查我的第二个答案中的LineNavigator,那个阅读器要好得多。

    我制作了自己的阅读器,满足了我的需求。

    性能

    由于问题仅与大文件有关,因此性能是最重要的部分。

    如您所见,性能几乎与直接读取相同(如上所述)。 目前我正在努力让它变得更好,因为更大的时间消费者是异步调用以避免调用堆栈限制命中,这对于执行问题不是不必要的。性能问题已解决。

    质量

    测试了以下案例:

    • 空文件
    • 单行文件
    • 文件末尾有换行符,没有
    • 检查解析的行
    • 在同一页面上多次运行
    • 没有线路丢失,也没有订单问题

    代码和用法

    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.getNextBatch();
        });
        
        reader.getNextBatch();  
    });
    

    代码:

    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);
            }
        };
    };
    

    【讨论】:

    • 更新,更快的版本即将推出(具有里程碑以加快对已读部分的随机访问)。
    • 你可以在这里找到实际的、正确的版本:github.com/anpur/line-navigator
    【解决方案3】:

    为了同样的目的,我编写了一个名为 line-reader-browser 的模块。它使用Promises

    语法(打字稿):-

    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!"))
    }
    

    尝试以下代码 sn-p 以查看模块工作。

    <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>

    希望它可以节省某人的时间!

    【讨论】: