关于javascript:逐行读取FileReader对象,而无需将整个文件加载到RAM中

Read FileReader object line-by-line without loading the whole file into RAM

现在,许多浏览器都支持使用HTML5的FileReader读取本地文件,这为网站打开了大门,这些网站超越了"数据库前端"到脚本的工作范围,这些脚本可以对本地数据进行有用的处理而不必先将其发送到服务器。

在上传之前对图像和视频进行预处理,FileReader的一个重要应用是将某种形式的磁盘表(CSV,TSV等)中的数据加载到浏览器中进行操作-可能是在D3中作图或分析.js或在WebGL中创建景观。

问题是,StackOverflow和其他站点上的大多数示例都使用FileReader的.readAsText()属性,该属性在返回结果之前将整个文件读入RAM。

javascript:如何逐行解析FileReader对象

要在不将数据加载到RAM的情况下读取文件,则需要使用.readAsArrayBuffer(),因此我最能得出一个很好的答案是这样的帖子:

大文件上的filereader api

但是,它对于特定的问题来说太具体了,说实话,我可以花几天时间尝试使解决方案更通用,并且空手而归,因为我不了解块大小的重要性或为什么使用Uint8Array。解决一个更普遍的问题的解决方案:使用用户可定义的行分隔符逐行读取文件(理想情况下使用.split(),因为它也接受正则表达式),然后每行执行一些操作(如将其打印到console.log)会很理想。


我在以下Gist URL上创建了LineReader类。正如我在评论中提到的那样,通常不使用除LF,CR / LF甚至CR以外的其他行分隔符。因此,我的代码仅将LF和CR / LF视为行分隔符。

https://gist.github.com/peteroupc/b79a42fffe07c2a87c28

示例:

1
2
3
new LineReader(file).readLines(function(line){
 console.log(line);
});


这里是Peter O的代码的改编的TypeScript类版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
export class BufferedFileLineReader {
  bufferOffset = 0;
  callback: (line: string) => void = () => undefined;
  currentLine = '';
  decodeOptions: TextDecodeOptions = { 'stream': true };
  decoder = new TextDecoder('utf-8', { 'ignoreBOM': true });
  endCallback: () => void = () => undefined;
  lastBuffer: Uint8Array | undefined;
  offset = 0;
  omittedCR = false;
  reader = new FileReader();
  sawCR = false;

  readonly _error = (event: Event): void => {
    throw event;
  };

  readonly _readFromView = (dataArray: Uint8Array, offset: number): void => {
    for (let i = offset; i < dataArray.length; i++) {
      // Treats LF and CRLF as line breaks
      if (dataArray[i] == 0x0A) {
        // Line feed read
        const lineEnd = (this.sawCR ? i - 1 : i);
        if (lineEnd > 0) {
          this.currentLine += this.decoder.decode(dataArray.slice(this.bufferOffset, lineEnd), this.decodeOptions);
        }
        this.callback(this.currentLine);
        this.decoder.decode(new Uint8Array([]));
        this.currentLine = '';
        this.sawCR = false;
        this.bufferOffset = i + 1;
        this.lastBuffer = dataArray;
      } else if (dataArray[i] == 0x0D) {
        if (this.omittedCR) {
          this.currentLine += '\
'
;
        }
        this.sawCR = true;
      } else if (this.sawCR) {
        if (this.omittedCR) {
          this.currentLine += '\
'
;
        }
        this.sawCR = false;
      }
      this.omittedCR = false;
    }

    if (this.bufferOffset != dataArray.length) {
      // Decode the end of the line if no current line was reached
      const lineEnd = (this.sawCR ? dataArray.length - 1 : dataArray.length);
      if (lineEnd > 0) {
        this.currentLine += this.decoder.decode(dataArray.slice(this.bufferOffset, lineEnd), this.decodeOptions);
      }
      this.omittedCR = this.sawCR;
    }
  };

  readonly _viewLoaded = (): void => {
    if (!this.reader.result) {
      this.endCallback();
    }

    const dataArray = new Uint8Array(this.reader.result as ArrayBuffer);
    if (dataArray.length > 0) {
      this.bufferOffset = 0;
      this._readFromView(dataArray, 0);
      this.offset += dataArray.length;
      const s = this.file.slice(this.offset, this.offset + 256);
      this.reader.readAsArrayBuffer(s);
    } else {
      if (this.currentLine.length > 0) {
        this.callback(this.currentLine);
      }
      this.decoder.decode(new Uint8Array([]));
      this.currentLine = '';
      this.sawCR = false;
      this.endCallback();
    }
  }

  constructor(private file: File) {
    this.reader.addEventListener('load', this._viewLoaded);
    this.reader.addEventListener('error', this._error);
  }

  public readLines(callback: (line: string) => void, endCallback: () => void) {
    this.callback = callback;
    this.endCallback = endCallback;
    const slice = this.file.slice(this.offset, this.offset + 8192);
    this.reader.readAsArrayBuffer(slice);
  }
}

再次感谢Peter O的精彩回答。