8

我经常发现自己在读取一个大的 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 内置的一些较低级别的方法从文件中流式传输数据来完成?

4

4 回答 4

3

现在回想一下 Deno 1.0 已经发布,以防其他人有兴趣做这样的事情。我能够拼凑出一个适用于我的用例的小类。它不像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
于 2020-05-21T04:30:32.843 回答
2

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

或者,尽管这不能直接回答您的问题,但处理大型 Json 数据集的常见模式是让文件包含由换行符分隔的 Json 对象。(每行一个 Json 对象。)例如,Hadoop 和 Spark、AWS S3 select,可能还有许多其他人使用这种格式。如果您可以获取该格式的输入数据,那可能会帮助您使用更多工具。readString('\n')然后,您也可以使用Deno 标准库中的方法流式传输数据: 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.`)
    }
于 2019-10-22T07:58:07.747 回答
2

这是我用于包含 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.`);
}
于 2020-02-15T20:39:19.827 回答
0

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

它是在 MIT 下发布的,所以享受吧!我希望它能解决你的问题。:)

于 2021-07-23T22:10:17.587 回答