在 Deno 中读取大型 JSON 文件

Reading large JSON file in Deno

我经常发现自己读取一个大 JSON 文件(通常是一个对象数组),然后操作每个对象并写回一个新文件。

为了在 Node 中实现这一点(至少是读取数据部分),我通常使用 stream-json 模块来做类似的事情。

const fs = require('fs');
const StreamArray = require('stream-json/streamers/StreamArray');

const pipeline = fs.createReadStream('sample.json')
  .pipe(StreamArray.withParser());

pipeline.on('data', data => {
    //do something with each object in file
});

我最近发现了 Deno,并希望能够使用 Deno 完成此工作流程。

看起来标准库中的 readJSON 方法将文件的全部内容读入内存,所以我不知道它是否适合处理大文件。

有没有一种方法可以通过使用 Deno 中内置的一些较低级别的方法从文件中流式传输数据来完成此操作?

我认为像 stream-json 这样的包在 Deno 上和在 NodeJs 上一样有用,所以一种方法肯定是获取该包的源代码并使其在 Deno 上运行. (而且这个答案很快就会过时,因为有很多人在做这样的事情,而且很快就会有人——也许是你——做出他们的结果 public 并且可以导入任何 Deno 脚本。)

或者,虽然这不能直接回答您的问题,但处理 Json 数据的大型数据集的常见模式是让包含 Json 对象的文件以换行符分隔。 (每行一个 Json 个对象。)例如,Hadoop 和 Spark、AWS S3 select 以及许多其他可能使用这种格式。如果您可以获得那种格式的输入数据,那可能会帮助您使用更多的工具。然后,您还可以使用 Deno 标准库中的 readString('\n') 方法流式传输数据:https://github.com/denoland/deno_std/blob/master/io/bufio.ts

具有减少对第三方包的依赖的额外优势。示例代码:

    import { BufReader } from "https://deno.land/std/io/bufio.ts";

    async function stream_file(filename: string) {
        const file = await Deno.open(filename);
        const bufReader = new BufReader(file);
        console.log('Reading data...');
        let line: string;
        let lineCount: number = 0;
        while ((line = await bufReader.readString('\n')) != Deno.EOF) {
            lineCount++;
            // do something with `line`.
        }
        file.close();
        console.log(`${lineCount} lines read.`)
    }

这是我用于包含 13,147,089 行文本的文件的代码。 请注意,它与 Roberts 的代码相同,但使用的是 readLine() 而不是 readString('\n')。 readLine() 是一个低级的行读取原语。大多数呼叫者应改用 readString('\n') 或使用扫描器。`

import { BufReader } from "https://deno.land/std/io/bufio.ts";

export async function stream_file(filename: string) {
  const file = await Deno.open(filename);
  const bufReader = new BufReader(file);
  console.log("Reading data...");
  let line: string | any;
  let lineCount: number = 0;
  while ((line = await bufReader.readLine()) != Deno.EOF) {
    lineCount++;
    // do something with `line`.
  }
  file.close();
  console.log(`${lineCount} lines read.`);
}

现在 Deno 1.0 已经出来了,以防其他人有兴趣做这样的事情。我能够拼凑出一个适用于我的用例的小 class。它不像 stream-json 包那样健壮,但它可以很好地处理大型 JSON 数组。

import { EventEmitter } from "https://deno.land/std/node/events.ts";

export class JSONStream extends EventEmitter {

    private openBraceCount = 0;
    private tempUint8Array: number[] = [];
    private decoder = new TextDecoder();

    constructor (private filepath: string) {
        super();
        this.stream();
    }

    async stream() {
        console.time("Run Time");
        let file = await Deno.open(this.filepath);
        //creates iterator from reader, default buffer size is 32kb
        for await (const buffer of Deno.iter(file)) {

            for (let i = 0, len = buffer.length; i < len; i++) {
                const uint8 = buffer[ i ];

                //remove whitespace
                if (uint8 === 10 || uint8 === 13 || uint8 === 32) continue;

                //open brace
                if (uint8 === 123) {
                    if (this.openBraceCount === 0) this.tempUint8Array = [];
                    this.openBraceCount++;
                };

                this.tempUint8Array.push(uint8);

                //close brace
                if (uint8 === 125) {
                    this.openBraceCount--;
                    if (this.openBraceCount === 0) {
                        const uint8Ary = new Uint8Array(this.tempUint8Array);
                        const jsonString = this.decoder.decode(uint8Ary);
                        const object = JSON.parse(jsonString);
                        this.emit('object', object);
                    }
                };
            };
        }
        file.close();
        console.timeEnd("Run Time");
    }
}

用法示例

const stream = new JSONStream('test.json');

stream.on('object', (object: any) => {
    // do something with each object
});

正在处理一个约 4.8 MB json 文件,其中包含约 20,000 个小对象

[
    {
      "id": 1,
      "title": "in voluptate sit officia non nesciunt quis",
      "urls": {
         "main": "https://www.placeholder.com/600/1b9d08",
         "thumbnail": "https://www.placeholder.com/150/1b9d08"
      }
    },
    {
      "id": 2,
      "title": "error quasi sunt cupiditate voluptate ea odit beatae",
      "urls": {
          "main": "https://www.placeholder.com/600/1b9d08",
          "thumbnail": "https://www.placeholder.com/150/1b9d08"
      }
    }
    ...
]

用了 127 毫秒。

❯ deno run -A parser.ts
Run Time: 127ms

2021 年 7 月更新:我有同样的需求,但找不到可行的解决方案,所以我写了一个库来为 Deno 解决这个问题:https://github.com/xtao-org/jsonhilo

可以像典型的基于 SAX 的解析器一样使用:

import {JsonHigh} from 'https://deno.land/x/jsonhilo@v0.1.0/mod.js'
const stream = JsonHigh({
  openArray: () => console.log('<array>'),
  openObject: () => console.log('<object>'),
  closeArray: () => console.log('</array>'),
  closeObject: () => console.log('</object>'),
  key: (key) => console.log(`<key>${key}</key>`),
  value: (value) => console.log(`<value type="${typeof value}">${value}</value>`),
})
stream.push('{"tuple": [null, true, false, 1.2e-3, "[demo]"]}')

/* OUTPUT:
<object>
<key>tuple</key>
<array>
<value type="object">null</value>
<value type="boolean">true</value>
<value type="boolean">false</value>
<value type="number">0.0012</value>
<value type="string">[demo]</value>
</array>
</object>
*/

还有一个独特的低级接口,可以实现非常快速(此处的基准:https://github.com/xtao-org/jsonhilo-benchmarks)无损解析。

麻省理工下发布,尽情享受吧!我希望它能解决你的问题。 :)