在Node.js中同步从文件中逐行读取内容

47

我需要使用Node.js逐行解析以下格式的文件:

13
13
0 5
4 3
0 1
9 12
6 4
5 4
0 2
11 12
9 10
0 6
7 8
9 11
5 3

这代表一个图形。前两行是边和顶点的数量,其后是边。

我可以使用类似以下内容完成任务:

var fs = require('fs');
var readline = require('readline');
var read_stream = fs.createReadStream(filename);
var rl = readline.createInterface({
    input: read_stream
});
var c = 0;
var vertexes_number;
var edges_number;
var edges = [];
rl.on('line', function(line){
    if (c==0) {
        vertexes_number = parseInt(line);
    } else if (c==1) {
        edges_number = parseInt(line);
    } else {
        edges.push(line.split(' '));
    }
    c++;
})
.on('end', function(){
    rl.close();
})

我知道这种事情可能不是 Node.js 设计的初衷,但 line 回调中级联的 if 看起来并不优雅 / 可读。

是否有一种方式可以像其他编程语言那样同步从流中逐行读取?

如果没有内置的解决方案,我愿意使用插件。

[编辑]

抱歉,我应该更清楚地表明我想避免事先在内存中加载整个文件


https://nodejs.org/api/fs.html#fs_fs_readfilesync_file_options - Alexey Ten
是的,使用fs.readFileSync加载它,然后在按新行分割后同步地使用您的代码解析它,即 lines = fs.readFileSync('graph.txt').split(/[\n\r]/); - Nikos M.
5个回答

82

我通常用以下代码来完成这种简单任务:

var lines = require('fs').readFileSync(filename, 'utf-8')
    .split('\n')
    .filter(Boolean);

lines 是一个不包含空字符串的字符串数组。


24
谢谢。然而我仍然对不需要将整个文件加载到内存中的精简解决方案感兴趣。 - Andrea Casaccia
10
这种方法无法处理大文件,比如一份有10000000行的日志文件,因为你可以使用缓冲区将大文件加载到内存中,但是Node.js的toString方法无法处理过大的缓冲区对象。 - Sugar
2
如果你有数十亿个几千行长的txt日志文件,这段代码就能很好地工作。每个txt文件都是单个测试执行日志,并且您希望解析它们以获取相关数据,并将该数据转储到JSON日志格式中,以便使用AWS ECS集群将旧的测试数据迁移到新的Kabana仪表板上。;-) - Seth Eden
2
不要忘记使用 const {EOL} = require('os'); 代替 '\n' 实现多操作系统兼容性。 - Ryu S.
我非常非常感激这个精准的答案。以防万一有人遇到“TypeError: aaa.split不是一个函数”的情况,可以使用“readFileSync.toString()”来解决。 - Rich KS
@RichKS 的 readFileSync 的第二个参数确保它返回一个字符串,因此它会具有 .split 方法。 - Alexey Ten

22

这个在github.com上的项目恰好满足了我的需求:

https://github.com/nacholibre/node-readlines

var readlines = require('n-readlines');
var liner = new readlines(filename);

var vertexes_number = parseInt(liner.next().toString('ascii'));
var edges_number = parseInt(liner.next().toString('ascii'));
var edges = [];
var next;
while (next = liner.next()) {
    edges.push(next.toString('ascii').split(' '));
}

有没有一种好的方法可以使用stdin来完成这个任务?我正在使用一个代码提交网站,也无法将/dev/stdin读取为文件。 - Colin D
如果您发布一个带有更多细节的新问题,而不是在此处发表评论,那么您获得答案的机会将更大。 - Andrea Casaccia
已完成 https://dev59.com/x1cP5IYBdhLWcg3w6-EH - Colin D

3
为什么不将它们全部读入数组,然后使用splice方法取出前两个元素。我假设你的示例已经大大简化,否则你只需将整个文件读入内存并分割即可。如果你的实际情况存储了多个图形,并且你想在每个图形加载时执行某些操作,你可以在line事件中添加一个测试。
var fs = require('fs');
var readline = require('readline');
var read_stream = fs.createReadStream(filename);
var rl = readline.createInterface({
    input: read_stream
});

var buffer = [];

rl.on('line', function(line){
    buffer.push(line.split(' '));
    //Not sure what your actual requirement is but if you want to do 
    //something  like display a graph once one has loaded
    //obviously need to be able to determine when one has completed loading
    if ( buffer.length == GRAPHLENGTH) {  //or some other test
        displayGraph(buffer);
        buffer = [];
    }    
})
.on('close', function(){
    //or do it here if there is only one graph
    //displayGraph(buffer);
    rl.close();
})

function displayGraph(buffer){
    var vertexes_number = parseInt(buffer.splice(0,1));
    var edges_number = parseInt(buffer.splice(0,1));
    var edges = buffer;

    //doYourThing(vertexes_number, edges_number, edges);
}

1
需要注意的是,在文件系统中,没有“end”事件,而是在文件结束时发出“close”事件。 - Wiktor Zychla
3
这个例子是同步的吗?关于“line”和“end”的部分是异步的还是同步的? - dchang

3

个人而言,我喜欢使用event-stream来处理流。虽然在这里不是必需的,但我在代码示例中使用了它。它很简单,我将所有内容解析为整数并放入edges中,然后当文件读取完成时,我获取第一个元素,即vertexes_number,新的第一个元素是edges_number

var fs = require('fs');
var es = require('event-stream');

var filename = 'parse-file.txt';

var vertexes_number, edges_number;
var edges = [];

fs.createReadStream(filename)
    .pipe(es.split()) // split by lines
    .pipe(es.map(function (line, next) {
        // split and convert all to numbers
        edges.push(line.split(' ').map((n) => +n));

        next(null, line);
    })).pipe(es.wait(function (err, body) {
        // the first element is an array containing vertexes_number
        vertexes_number = edges.shift().pop();

        // the following element is an array containing edges_number
        edges_number = edges.shift().pop();

        console.log('done');
        console.log('vertexes_number: ' + vertexes_number);
        console.log('edges_number: ' + edges_number);
        console.log('edges: ' + JSON.stringify(edges, null, 3));
    }));

到目前为止,如果您不需要将整个文件加载到内存中,这是迄今为止最好的方法。 - Nidhin David
4
是的,这种方法非常好,但仍然是异步的,问题说明了同步模式,在这种情况下并不适用。 - Ualter Jr.

0
我发现在这个答案中,Node 11添加了一个异步迭代器语法,使得这个过程非常简单。
let c = 0
for await (const line of rl) {
  parseLine(c, line)
  c++
}

你显然也可以手动使用相同的迭代器逐行读取文件,如果你想单独读取文件的前两行标题的话。
const it = rl[Symbol.asyncIterator]();
const line1 = await it.next() // { value: "...", done: false }
const line2 = await it.next() // { value: "...", done: false }
const vertex_count = parseInt(line1.value)
const edge_count = parseInt(line2.value)

let result
do {
  result = await it.next()
  parseEdge(result.value)
} while (!result.done)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接