【问题标题】:Read n lines of a big text file读取 n 行大文本文件
【发布时间】:2017-01-21 14:09:27
【问题描述】:

我拥有的最小文件有 > 850k 行,每行的长度未知。目标是在浏览器中从此文件中读取n 行。完全阅读它是不可能的。

这是 HTML <input type="file" name="file" id="file"> 和我拥有的 JS:

var n = 10;
var reader = new FileReader();
reader.onload = function(progressEvent) {
  // Entire file
  console.log(this.result);

  // By lines
  var lines = this.result.split('\n');
  for (var line = 0; line < n; line++) {
    console.log(lines[line]);
  }
};

很明显,这里的问题是它首先尝试真实的整个文件,然后用换行符分割它。所以不管n,它都会尝试读取整个文件,最终当文件很大时什么都不读取。

我该怎么做?

注意:我愿意删除整个函数并从头开始,因为我将能够console.log() 我们阅读的每一行。


*"每一行的长度未知" -> 表示文件是这样的:

(0, (1, 2))
(1, (4, 5, 6))
(2, (7))
(3, (8))

编辑:

要走的路类似于filereader api on big files,但我不知道如何修改它以读取文件的n 行...

也可以使用Uint8Array to string in Javascript,从那里开始:

var view = new Uint8Array(fr.result);
var string = new TextDecoder("utf-8").decode(view);
console.log("Chunk " + string);

但这可能不会将最后一行作为一个整体读取,那么您以后将如何确定这些行呢?例如这里是它打印的内容:

((7202), (u'11330875493', u'2554375661'))
((1667), (u'9079074735', u'6883914476',

【问题讨论】:

  • "...但这不重要。" 天哪,你认为这不重要吗?!如果没有行开始位置的索引和在给定索引处增量读取文件的能力,这绝对很重要。
  • @T.J.Crowder 我更新了我的问题以澄清,也许我应该删除该声明,你是对的!
  • 这里需要更多上下文。您正在使用 HTML 和 JavaScript。这是在网络浏览器中运行的 JavaScript 吗?或者这个 JavaScript 是作为来自 HTML POST 之类的响应而执行的?
  • 啊忘了@Alan,更新了!在浏览器中。
  • 看看这个 StackOverflow 的答案,它很相似:stackoverflow.com/questions/25810051/… 它的要点是使用 .slice,以块的形式读取数据。然后随时处理每个块。

标签: javascript html file io bigdata


【解决方案1】:

我需要在浏览器中读取 250MB 的 utf-8 编码文件。我的解决方案是编写类似 TextReader 类的 C#,它给了我类似异步流的行为。


TextReader 类:

class TextReader {
    CHUNK_SIZE = 8192000; // I FOUND THIS TO BE BEST FOR MY NEEDS, CAN BE ADJUSTED
    position = 0;
    length = 0;

    byteBuffer = new Uint8Array(0);

    lines = [];
    lineCount = 0;
    lineIndexTracker = 0;

    fileReader = new FileReader();
    textDecoder = new TextDecoder(`utf-8`);

    get allCachedLinesAreDispatched() {
        return !(this.lineIndexTracker < this.lineCount);
    }

    get blobIsReadInFull() {
        return !(this.position < this.length);
    }

    get bufferIsEmpty() {
        return this.byteBuffer.length === 0;
    }

    get endOfStream() {
        return this.blobIsReadInFull && this.allCachedLinesAreDispatched && this.bufferIsEmpty;
    }

    constructor(blob) {
        this.blob = blob;
        this.length = blob.size;
    }

    blob2arrayBuffer(blob) {
        return new Promise((resolve, reject) => {
            this.fileReader.onerror = reject;
            this.fileReader.onload = () => {
                resolve(this.fileReader.result);
            };

            this.fileReader.readAsArrayBuffer(blob);
        });
    }

    read(offset, count) {
        return new Promise(async (resolve, reject) => {
            if (!Number.isInteger(offset) || !Number.isInteger(count) || count < 1 || offset < 0 || offset > this.length - 1) {
                resolve(new ArrayBuffer(0));
                return
            }

            let endIndex = offset + count;

            if (endIndex > this.length) endIndex = this.length;

            let blobSlice = this.blob.slice(offset, endIndex);

            resolve(await this.blob2arrayBuffer(blobSlice));
        });
    }

    readLine() {
        return new Promise(async (resolve, reject) => {

            if (!this.allCachedLinesAreDispatched) {
                resolve(this.lines[this.lineIndexTracker++] + `\n`);
                return;
            }

            while (!this.blobIsReadInFull) {
                let arrayBuffer = await this.read(this.position, this.CHUNK_SIZE);
                this.position += arrayBuffer.byteLength;

                let tempByteBuffer = new Uint8Array(this.byteBuffer.length + arrayBuffer.byteLength);
                tempByteBuffer.set(this.byteBuffer);
                tempByteBuffer.set(new Uint8Array(arrayBuffer), this.byteBuffer.length);

                this.byteBuffer = tempByteBuffer;

                let lastIndexOfLineFeedCharacter = this.byteBuffer.lastIndexOf(10); // LINE FEED CHARACTER (\n) IS ONE BYTE LONG IN UTF-8 AND IS 10 IN ITS DECIMAL FORM

                if (lastIndexOfLineFeedCharacter > -1) {
                    let lines = this.textDecoder.decode(this.byteBuffer).split(`\n`);
                    this.byteBuffer = this.byteBuffer.slice(lastIndexOfLineFeedCharacter + 1);

                    let firstLine = lines[0];

                    this.lines = lines.slice(1, lines.length - 1);
                    this.lineCount = this.lines.length;
                    this.lineIndexTracker = 0;

                    resolve(firstLine + `\n`);
                    return;
                }
            }

            if (!this.bufferIsEmpty) {
                let line = this.textDecoder.decode(this.byteBuffer);
                this.byteBuffer = new Uint8Array(0);
                resolve(line);
                return;
            }

            resolve(null);
        });
    }
}

用法:

document.getElementById("read").onclick = async () => {
    let file = document.getElementById("fileInput").files[0];
    let textReader = new TextReader(file);

    while(true) {
        let line = await textReader.readLine();
        if(line === null) break;
        // PROCESS LINE
    }

    // OR

    while (!textReader.endOfStream) {
        let line = await textReader.readLine();
        // PROCESS LINE
    }
};

性能:

我能够在 1.5 秒内读取包含 1,398,258 行的单个 250MB utf-8 编码文本文件,JS 堆大小不超过 20MB。相比之下,如果我一次性读取同一个文件,然后将结果字符串按 \n 拆分,仍然需要 ~1.5 秒,但是 JS Heap 会达到 230MB。

【讨论】:

    【解决方案2】:

    流是特色!
    whatwg 团队正在研究关于可写 + 可读流的最后一个变化,并且很快就准备好了。但在那之前,您可以使用web-stream-polyfill。 他们正在研究一种从 blob 以及 [1] 获取 ReadableStream 的方法。但我还创建了一种以流方式获取 blob 的方法:Screw-FileReader

    昨天我还创建了一个node-byline 的简单port 来处理网络流

    所以这可以很简单:

    // Simulate a file
    var csv =
    `apple,1,$1.00
    banana,4,$0.20
    orange,3,$0.79`
    
    var file = new Blob([csv])
    
    var n = 0
    var controller
    var decoder = new TextDecoder
    var stdout = new WritableStream({
      start(c) {
          controller = c
        },
        write(chunk, a) {
          // Calling controller.error will also put the byLine in an errored state
          // Causing the file stream to stop reading more data also
          if (n == 1) controller.error("don't need more lines")
          chunk = decoder.decode(chunk)
          console.log(`chunk[${n++}]: ${chunk}`)
        }
    })
    
    file
      .stream()
      .pipeThrough(byLine())
      // .pipeThrough(new TextDecoder) something like this will work eventually
      .pipeTo(stdout)
    <script src="https://cdn.rawgit.com/creatorrr/web-streams-polyfill/master/dist/polyfill.min.js"></script>
    <script src="https://cdn.rawgit.com/jimmywarting/Screw-FileReader/master/index.js"></script>
    
    <!-- after a year or so you only need byLine -->
    <script src="https://cdn.rawgit.com/jimmywarting/web-byline/master/index.js"></script>

    【讨论】:

    • 有趣的方法,不用多说! :)
    • 谢谢,期待这个功能:)
    • 请不要鼓励将innerHTML 用于外部输入,因为它可能会引入安全漏洞。 document.body.innerHTML += 也很糟糕,因为它会强制重新解析整个文档。考虑改用element.insertAdjacentText 或document.createTextNode + element.appendChild。
    • 他们像一年一样致力于从 blob 开始的 ReadableStream。仍然没有明显的进展。
    【解决方案3】:

    逻辑与我在对filereader api on big files 的回答中所写的非常相似,只是您需要跟踪到目前为止已处理的行数(以及到目前为止读取的最后一行,因为它可能还没有结束)。下一个示例适用于与 UTF-8 兼容的任何编码;如果您需要其他编码,请查看 TextDecoder 构造函数的选项。

    如果您确定输入是 ASCII(或任何其他单字节编码),那么您也可以跳过使用 TextDecoder 并使用 FileReader's readAsText method 直接将输入读取为文本。

    // This is just an example of the function below.
    document.getElementById('start').onclick = function() {
        var file = document.getElementById('infile').files[0];
        if (!file) {
            console.log('No file selected.');
            return;
        }
        var maxlines = parseInt(document.getElementById('maxlines').value, 10);
        var lineno = 1;
        // readSomeLines is defined below.
        readSomeLines(file, maxlines, function(line) {
            console.log("Line: " + (lineno++) + line);
        }, function onComplete() {
            console.log('Read all lines');
        });
    };
    
    /**
     * Read up to and including |maxlines| lines from |file|.
     *
     * @param {Blob} file - The file to be read.
     * @param {integer} maxlines - The maximum number of lines to read.
     * @param {function(string)} forEachLine - Called for each line.
     * @param {function(error)} onComplete - Called when the end of the file
     *     is reached or when |maxlines| lines have been read.
     */
    function readSomeLines(file, maxlines, forEachLine, onComplete) {
        var CHUNK_SIZE = 50000; // 50kb, arbitrarily chosen.
        var decoder = new TextDecoder();
        var offset = 0;
        var linecount = 0;
        var linenumber = 0;
        var results = '';
        var fr = new FileReader();
        fr.onload = function() {
            // Use stream:true in case we cut the file
            // in the middle of a multi-byte character
            results += decoder.decode(fr.result, {stream: true});
            var lines = results.split('\n');
            results = lines.pop(); // In case the line did not end yet.
            linecount += lines.length;
        
            if (linecount > maxlines) {
                // Read too many lines? Truncate the results.
                lines.length -= linecount - maxlines;
                linecount = maxlines;
            }
        
            for (var i = 0; i < lines.length; ++i) {
                forEachLine(lines[i] + '\n');
            }
            offset += CHUNK_SIZE;
            seek();
        };
        fr.onerror = function() {
            onComplete(fr.error);
        };
        seek();
        
        function seek() {
            if (linecount === maxlines) {
                // We found enough lines.
                onComplete(); // Done.
                return;
            }
            if (offset !== 0 && offset >= file.size) {
                // We did not find all lines, but there are no more lines.
                forEachLine(results); // This is from lines.pop(), before.
                onComplete(); // Done
                return;
            }
            var slice = file.slice(offset, offset + CHUNK_SIZE);
            fr.readAsArrayBuffer(slice);
        }
    }
    Read <input type="number" id="maxlines"> lines from
    <input type="file" id="infile">.
    <input type="button" id="start" value="Print lines to console">

    【讨论】:

    • 当用户未提供maxlines 时,我真的不明白这将如何读取整个文件。除此之外,太棒了!
    • @gsamaras 当您将maxlines 设置为任意高的值(例如Infinity)时,所有涉及maxlines 的条件评估为假,因此您可以想象if-blocks 包含它们不存在。那么也应该很容易看出seek 只会在它读取到文件末尾(offset &gt;= file.size)时返回。
    猜你喜欢
    • 2019-02-28
    • 2015-09-07
    • 2011-07-28
    • 1970-01-01
    • 1970-01-01
    • 2011-03-29
    相关资源
    最近更新 更多