当前位置: 首页 > 科技观察

如何在Node.js中流式处理大型JSON文件_0

时间:2023-03-18 15:02:56 科技观察

解决一个问题不仅仅是寻找最终答案,而是寻找答案的过程。善于思考和总结总是好的。本文介绍一种设计模式的概念SAX。虽然这个概念不是从Node.js衍生出来的,但是当我们在Node.js或其他一些编程语言中遇到类似问题时,它的一些解决问题的思路也会受到启发,本文后面会介绍如何流处理一个大的JSON文件。下面给出以下两个问题。你可以先想想如果是你你会怎么做?场景描述问题一:假设有一个场景,有一个很大的JSON文件,需要读取每一条数据并进行处理,然后输出到一个文件或者生成报表数据。它如何以流式方式一次读取一条记录?[{"id":1},{"id":2},...]问题2:同一个大JSON文件,我只读取了一段数据,如果只想获取对象怎么办列表数组?{"list":[],"otherList":[]}在??Node.js中,我们可以通过以下方式读取数据,通常第一个想到的是:fs.readFile():这是一个timeread将数据传输到内存并不是一个好的方法。如果数据量很大,会占用内存。很容易造成内存溢出。fs.createReadStream():创建可读流,可以解决避免大量数据占用内存的问题。这是系统提供的一个基础API。它是一个一个地读取数据块,因为我们的JSON对象是结构化的,并不能直接解决上面提到的两个问题。还有一个require()也是可以加载JSON文件的,但是稍微熟悉Node.jsCommonJS规范的应该知道,require加载后会被缓存,会一直占用服务的内存。了解什么是SAXSAX是SimpleAPIforXML的缩写。目前,没有标准的SAX参考标准。它最早是在Java编程语言中实现和普及的。SAX在Java中的实现后来被认为是一种规范。其他语言的实现也遵循这个规律。虽然每种语言的实现方式不同,但是有一个重要的概念“事件驱动”是相同的。实现SAX的解析器有一个事件驱动的API,像Stream一样工作,边读边解析。用户可以定义回调函数来获取数据。无论XML内容有多大,内存占用总是很小的。这在本节中对我们有何帮助?当我们读取和解析一个大的JSON文件时,我们无法将所有数据加载到内存中。我们还需要SAX这样的工具来帮助我们实现它。StreamingJSONparserbasedonSAX这是一个流式JSON解析器https://github1s.com/creationix/jsonparse每周下载量超过600万,但是这个源码好像很难整理。如果是学习,推荐一个更简单的基于SAX的版本https://gist.github.com/creationix/1821394有兴趣的可以看看。JSON有自己的标准,具有特定的数据类型和格式。这个JSON解析器在解析成特定的格式或类型后也会触发相应的事件,我们在使用的时候也需要注册相应的回调函数。下面的例子创建一个可读流对象,在流的data事件中注册SaxParser实例对象的parse方法,即将读取到的原始数据(默认为Buffer类型)传递给parse()函数进行解析,当解析接收到数据后会触发相应的事件。对应的Node.js代码如下:constSaxParser=require('./jsonparse').SaxParser;constp=newSaxParser({onNull:function(){console.log("onNull")},onBoolean:function(value){console.log("onBoolean",value)},onNumber:function(value){console.log("onNumber",value)},onString:function(value){console.log("onString",value)},onStartObject:function(){console.log("onStartObject")},onColon:function(){console.log("onColon")},onComma:function(){console.log("onComma")},onEndObject:function(){console.log("onEndObject")},onStartArray:function(){console.log("onEndObject")},onEndArray:function(){console.log("onEndArray")}});conststream=require('fs').createReadStream("./example.json");constpparse=p.parse.bind(p);stream.on('data',parse);如何解析一个JSON文件的数据已经解决了,但是如果直接这样使用,还是需要做一些处理工作。JSONStream处理大文件。这里推荐一个NPM模块JSONStream。在其实现中,它依赖于jsonparse模块来解析原始数据。在此基础上进行一些处理,根据一些匹配模式返回用户想要的数据。简单易用。下面我们使用JSONStream来解决上面提到的两个问题。问题一:假设有一个场景,有一个很大的JSON文件,需要读取每一条数据并处理,然后输出到一个文件或者生成报表数据。它如何以流式方式一次读取一条记录?因为测试,我调整了highWaterMark的值,现在我们的数据如下。[{"id":1},{"id":2}]重点是JSONStream的parse方法,我们传入一个'.',这个数据事件也是模块自己处理的,会返回对于我们每次一个对象:第一个return{id:1}第二个return{id:2}constfs=require('fs');constJSONStream=require('JSONStream');(async()=>{constreadable=fs.createReadStream('./list.json',{encoding:'utf8',highWaterMark:10})constparser=JSONStream.parse('.');readable.pipe(parser);parser.on('data',console.log);})()问题2:同一个大JSON文件,我只读取了某条数据,如果只想获取list的数组对象怎么办?解决了第二个问题,现在我们的JSON文件是这样的。{"list":[{"name":"1"},{"name":"2"}],"other":[{"key":"val"}]}与第一种方案不同的是最重要的是改了parse('list.*')方法,现在只返回list数组,其他的不返回。其实名单看完了,工作就结束了。第一个返回{name:'1'}第二个返回{name:'2'}(async()=>{constreadable=fs.createReadStream('./list.json',{encoding:'utf8',highWaterMark:10})constparser=JSONStream.parse('list.*');readable.pipe(parser);parser.on('data',console.log);})();总结当我们遇到类似大文件需要的时候待处理时,尽量避免将所有数据存放在内存中进行操作。应用服务的内存是有限的,这不是最好的处理方式。本文主要介绍如何对类似的大文件进行流处理,更重要的是掌握一些编程思想。比如SAX的核心点之一就是实现“事件驱动”的设计模式,同时结合Stream实现边读边解析。有多种方法可以解决这个问题。您还可以在生成JSON文件时将一个大文件拆分成不同的小文件。学会寻找答案。NPM生态系统发展良好。基本上,您可能遇到的大多数问题都已经有了一些解决方案。比如这道题不知道怎么用Stream读取JSON文件,可以在NPM搜索关键字上找找试试。