Читать файл по одной строке в node.js?
Я пытаюсь прочитать большой файл по одной строке за раз. Я нашел вопрос о Quora, который касался этой темы, но мне не хватает некоторых связей, чтобы все это сошлось воедино.
var Lazy=require("lazy");
new Lazy(process.stdin)
.lines
.forEach(
function(line) {
console.log(line.toString());
}
);
process.stdin.resume();
Я хотел бы выяснить, как можно читать по одной строке за раз из файла вместо STDIN, как в этом примере.
Я старался:
fs.open('./VeryBigFile.csv', 'r', '0666', Process);
function Process(err, fd) {
if (err) throw err;
// DO lazy read
}
но это не работает Я знаю, что в крайнем случае я мог бы вернуться к использованию чего-то вроде PHP, но я хотел бы выяснить это.
Я не думаю, что другой ответ будет работать, так как файл намного больше, чем у сервера, на котором он запущен, есть память.
30 ответов
Начиная с Node.js v0.12 и начиная с Node.js v4.0.0, существует стабильный основной модуль readline. Вот самый простой способ чтения строк из файла без каких-либо внешних модулей:
var lineReader = require('readline').createInterface({
input: require('fs').createReadStream('file.in')
});
lineReader.on('line', function (line) {
console.log('Line from file:', line);
});
Последняя строка читается правильно (по состоянию на Node v0.12 или более поздней), даже если нет финальной \n
,
ОБНОВЛЕНИЕ: этот пример был добавлен в официальную документацию API Node.
Для такой простой операции не должно быть никакой зависимости от сторонних модулей. Полегче.
var fs = require('fs'),
readline = require('readline');
var rd = readline.createInterface({
input: fs.createReadStream('/path/to/file'),
output: process.stdout,
console: false
});
rd.on('line', function(line) {
console.log(line);
});
Обновление в 2019 году
Замечательный пример уже опубликован в официальной документации Nodejs. Вот
Для этого на вашем компьютере должна быть установлена последняя версия Nodejs. >11,4
const fs = require('fs');
const readline = require('readline');
async function processLineByLine() {
const fileStream = fs.createReadStream('input.txt');
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
// Note: we use the crlfDelay option to recognize all instances of CR LF
// ('\r\n') in input.txt as a single line break.
for await (const line of rl) {
// Each line in input.txt will be successively available here as `line`.
console.log(`Line from file: ${line}`);
}
}
processLineByLine();
Вам не нужно open
файл, но вместо этого вы должны создать ReadStream
,
fs.createReadStream
Затем передайте этот поток Lazy
require('fs').readFileSync('file.txt', 'utf-8').split(/\r?\n/).forEach(function(line){
console.log(line);
})
Есть очень хороший модуль для чтения файла построчно, он называется line-reader
с этим вы просто пишете:
var lineReader = require('line-reader');
lineReader.eachLine('file.txt', function(line, last) {
console.log(line);
// do whatever you want with line...
if(last){
// or check if it's the last one
}
});
Вы можете даже перебрать файл с помощью интерфейса в стиле java, если вам нужно больше контроля:
lineReader.open('file.txt', function(reader) {
if (reader.hasNextLine()) {
reader.nextLine(function(line) {
console.log(line);
});
}
});
Старая тема, но это работает:
var rl = readline.createInterface({
input : fs.createReadStream('/path/file.txt'),
output: process.stdout,
terminal: false
})
rl.on('line',function(line){
console.log(line) //or parse line
})
Просто. Нет необходимости во внешнем модуле.
Вы всегда можете свернуть свой собственный читатель строки. Я пока не тестировал этот фрагмент, но он правильно разбивает входящий поток фрагментов на строки без завершающего '\n'
var last = "";
process.stdin.on('data', function(chunk) {
var lines, i;
lines = (last+chunk).split("\n");
for(i = 0; i < lines.length - 1; i++) {
console.log("line: " + lines[i]);
}
last = lines[i];
});
process.stdin.on('end', function() {
console.log("line: " + last);
});
process.stdin.resume();
Я придумал это, работая над сценарием быстрого анализа журнала, который должен был накапливать данные во время анализа журнала, и я чувствовал, что было бы неплохо попробовать сделать это, используя js и node вместо использования perl или bash.
В любом случае, я чувствую, что небольшие скрипты nodejs должны быть автономными и не полагаться на сторонние модули, поэтому после прочтения всех ответов на этот вопрос, каждый из которых использует различные модули для обработки разборов строк, может оказаться интересным решение 13 SLOC для нативных nodejs.
В Node.js v18.11.0 добавлена новая функция для чтения файлов построчно.
- filehandle.readLines([параметры])
Вот как вы используете это с текстовым файлом, который хотите прочитать
import { open } from 'node:fs/promises';
myFileReader();
async function myFileReader() {
const file = await open('./TextFileName.txt');
for await (const line of file.readLines()) {
console.log(line)
}
}
Чтобы понять больше, прочитайте документацию Node.js, вот ссылка для файловой системы readlines():https://nodejs.org/api/fs.html#filehandlereadlinesoptions
var carrier = require('carrier');
process.stdin.resume();
carrier.carry(process.stdin, function(line) {
console.log('got one line: ' + line);
});
Я закончил с огромной утечкой памяти, используя Lazy для построчного чтения при попытке затем обработать эти строки и записать их в другой поток из-за того, как работает сток / пауза / возобновление в узле (см.: http: // Elegantcode.com / 2011/04/06 / брать ребенка шаги с узлом js накачка данных между потоками / (я люблю этого парня, кстати)). Я недостаточно внимательно посмотрел на Lazy, чтобы точно понять, почему, но я не мог приостановить поток чтения, чтобы допустить утечку без выхода Lazy.
Я написал код для обработки больших CSV-файлов в XML-документы, вы можете увидеть код здесь: https://github.com/j03m/node-csv2xml
Если вы запускаете предыдущие ревизии с ленивой линией, она просачивается. Последняя редакция вообще не содержит утечек, и вы, вероятно, можете использовать ее в качестве основы для считывателя / процессора. Хотя у меня там есть кое-что нестандартное.
Изменить: Я думаю, я должен также отметить, что мой код с Lazy работал нормально, пока я не обнаружил, что пишу достаточно большие фрагменты XML, которые истощают / пауза / резюме, потому что это необходимо. Для небольших кусков это было хорошо.
В большинстве случаев этого должно быть достаточно:
const fs = require("fs")
fs.readFile('./file', 'utf-8', (err, file) => {
const lines = file.split('\n')
for (let line of lines)
console.log(line)
});
Редактировать:
Используйте поток преобразования.
С BufferedReader вы можете читать строки.
new BufferedReader ("lorem ipsum", { encoding: "utf8" })
.on ("error", function (error){
console.log ("error: " + error);
})
.on ("line", function (line){
console.log ("line: " + line);
})
.on ("end", function (){
console.log ("EOF");
})
.read ();
После публикации моего исходного ответа я обнаружил, что split - очень простой в использовании модуль узла для чтения строк в файле; Который также принимает необязательные параметры.
var split = require('split');
fs.createReadStream(file)
.pipe(split())
.on('data', function (line) {
//each chunk now is a seperate line!
});
Не проверял на очень больших файлах. Дайте нам знать, если вы делаете.
Я был разочарован отсутствием комплексного решения для этого, поэтому я собрал свою собственную попытку ( git / npm). Вставленный в копию список функций:
- Интерактивная обработка строк (на основе обратного вызова, без загрузки всего файла в оперативную память)
- Опционально, вернуть все строки в массиве (подробный или необработанный режим)
- Интерактивно прерывать потоковую передачу или выполнять обработку, подобную карте / фильтру
- Обнаружение любого соглашения новой строки (ПК /Mac/Linux)
- Правильное лечение eof / last line
- Корректная обработка многобайтовых символов UTF-8
- Извлечение информации о смещении байта и длине байта для каждой строки
- Произвольный доступ с использованием линейных или байтовых смещений
- Автоматически отображать информацию о смещении линии, чтобы ускорить произвольный доступ
- Нулевые зависимости
- тесты
NIH? Вам решать:-)
function createLineReader(fileName){
var EM = require("events").EventEmitter
var ev = new EM()
var stream = require("fs").createReadStream(fileName)
var remainder = null;
stream.on("data",function(data){
if(remainder != null){//append newly received data chunk
var tmp = new Buffer(remainder.length+data.length)
remainder.copy(tmp)
data.copy(tmp,remainder.length)
data = tmp;
}
var start = 0;
for(var i=0; i<data.length; i++){
if(data[i] == 10){ //\n new line
var line = data.slice(start,i)
ev.emit("line", line)
start = i+1;
}
}
if(start<data.length){
remainder = data.slice(start);
}else{
remainder = null;
}
})
stream.on("end",function(){
if(null!=remainder) ev.emit("line",remainder)
})
return ev
}
//---------main---------------
fileName = process.argv[2]
lineReader = createLineReader(fileName)
lineReader.on("line",function(line){
console.log(line.toString())
//console.log("++++++++++++++++++++")
})
Я хотел решить эту проблему, в основном то, что в Perl будет:
while (<>) {
process_line($_);
}
Мой вариант использования был просто автономным скриптом, а не сервером, так что синхронный был в порядке. Это были мои критерии:
- Минимальный синхронный код, который можно использовать во многих проектах.
- Нет ограничений на размер файла или количество строк.
- Нет ограничений по длине линий.
- Способен обрабатывать полный Unicode в UTF-8, включая символы за пределами BMP.
- Умеет обрабатывать *nix и окончания строк Windows (старый стиль Mac мне не нужен).
- Символы конца строки должны быть включены в строки.
- Может обрабатывать последнюю строку с или без символов конца строки.
- Не используйте внешние библиотеки, не включенные в дистрибутив node.js.
Это проект для меня, чтобы получить представление о низкоуровневом коде типа сценариев в node.js и решить, насколько он жизнеспособен в качестве замены для других языков сценариев, таких как Perl.
После удивительного усилия и нескольких неудачных попыток, это код, который я придумал. Это довольно быстро, но менее тривиально, чем я ожидал: (раскошелиться на GitHub)
var fs = require('fs'),
StringDecoder = require('string_decoder').StringDecoder,
util = require('util');
function lineByLine(fd) {
var blob = '';
var blobStart = 0;
var blobEnd = 0;
var decoder = new StringDecoder('utf8');
var CHUNK_SIZE = 16384;
var chunk = new Buffer(CHUNK_SIZE);
var eolPos = -1;
var lastChunk = false;
var moreLines = true;
var readMore = true;
// each line
while (moreLines) {
readMore = true;
// append more chunks from the file onto the end of our blob of text until we have an EOL or EOF
while (readMore) {
// do we have a whole line? (with LF)
eolPos = blob.indexOf('\n', blobStart);
if (eolPos !== -1) {
blobEnd = eolPos;
readMore = false;
// do we have the last line? (no LF)
} else if (lastChunk) {
blobEnd = blob.length;
readMore = false;
// otherwise read more
} else {
var bytesRead = fs.readSync(fd, chunk, 0, CHUNK_SIZE, null);
lastChunk = bytesRead !== CHUNK_SIZE;
blob += decoder.write(chunk.slice(0, bytesRead));
}
}
if (blobStart < blob.length) {
processLine(blob.substring(blobStart, blobEnd + 1));
blobStart = blobEnd + 1;
if (blobStart >= CHUNK_SIZE) {
// blobStart is in characters, CHUNK_SIZE is in octets
var freeable = blobStart / CHUNK_SIZE;
// keep blob from growing indefinitely, not as deterministic as I'd like
blob = blob.substring(CHUNK_SIZE);
blobStart -= CHUNK_SIZE;
blobEnd -= CHUNK_SIZE;
}
} else {
moreLines = false;
}
}
}
Вероятно, это можно было бы убрать и дальше, это было результатом проб и ошибок.
Читатель линии на основе генератора: https://github.com/neurosnap/gen-readlines
var fs = require('fs');
var readlines = require('gen-readlines');
fs.open('./file.txt', 'r', function(err, fd) {
if (err) throw err;
fs.fstat(fd, function(err, stats) {
if (err) throw err;
for (var line of readlines(fd, stats.size)) {
console.log(line.toString());
}
});
});
Если вы хотите прочитать файл построчно и записать это в другом:
var fs = require('fs');
var readline = require('readline');
var Stream = require('stream');
function readFileLineByLine(inputFile, outputFile) {
var instream = fs.createReadStream(inputFile);
var outstream = new Stream();
outstream.readable = true;
outstream.writable = true;
var rl = readline.createInterface({
input: instream,
output: outstream,
terminal: false
});
rl.on('line', function (line) {
fs.appendFileSync(outputFile, line + '\n');
});
};
При выполнении таких операций мы должны задать себе два вопроса:
- Какой объем памяти используется для этого?
- С увеличением размера файла резко увеличивается потребление памяти?
Решения вроде require('fs').readFileSync()
загружает весь файл в память. Это означает, что объем памяти, необходимый для выполнения операций, будет почти равен размеру файла. Мы должны избегать этого для чего-либо большего, чем50mbs
Мы можем легко отслеживать объем памяти, используемый функцией, разместив эти строки кода после вызова функции:
const used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`
);
Сейчас лучший способ читать отдельные строки из большого файла с помощью узла Readline. В документации есть замечательные примеры.
Хотя для этого нам не нужен сторонний модуль. Но если вы пишете корпоративный код, вам придется обрабатывать множество крайних случаев. Мне пришлось написать очень легкий модуль под названием Apick File Storage для обработки всех этих крайних случаев.
Модуль хранилища файлов Apick: https://www.npmjs.com/package/apickfs Документация: https://github.com/apickjs/apickFS
Пример файла: https://1drv.ms/t/s!AtkMCsWInsSZiGptXYAFjalXOpUx
Пример: установить модуль
npm i apickfs
// import module
const apickFileStorage = require('apickfs');
//invoke readByLineNumbers() method
apickFileStorage
.readByLineNumbers(path.join(__dirname), 'big.txt', [163845])
.then(d => {
console.log(d);
})
.catch(e => {
console.log(e);
});
Этот метод был успешно протестирован с файлами плотностью до 4 ГБ.
big.text - это плотный текстовый файл с 163845 строками и размером 124 Мб. Сценарий для чтения 10 различных строк из этого файла использует только около 4,63 МБ памяти. И он бесплатно анализирует действительный JSON на объекты или массивы. Потрясающе!!
Мы можем прочитать одну строку файла или сотни строк файла с очень небольшим потреблением памяти.
Это мой любимый способ просмотра файла, простое собственное решение для прогрессивного (а не в режиме "slurp" или все-в-памяти) чтения с помощью современных
async/await
. Это решение, которое я считаю "естественным" при обработке больших текстовых файлов без использования
readline
пакет или любую неосновную зависимость.
let buf = '';
for await ( const chunk of fs.createReadStream('myfile') ) {
const lines = buf.concat(chunk).split(/\r?\n/);
buf = lines.pop();
for( const line of lines ) {
console.log(line);
}
}
if(buf.length) console.log(buf); // last line, if file does not end with newline
Вы можете настроить кодировку в
fs.createReadStream
или используйте
chunk.toString(<arg>)
. Также это позволит вам лучше настроить разделение строк на свой вкус, т.е. использовать
.split(/\n+/)
чтобы пропустить пустые строки и контролировать размер блока с помощью
{ highWaterMark: <chunkSize> }
.
Не забудьте создать такую функцию, как
processLine(line)
чтобы избежать повторения кода обработки строки дважды из-за окончания
buf
остатки. К сожалению,
ReadStream
instance не обновляет свои флаги конца файла в этой настройке, поэтому, afaik, невозможно определить в цикле, что мы находимся на последней итерации, без некоторых более подробных трюков, таких как сравнение размера файла из
fs.Stats()
с участием
.bytesRead
. Отсюда последний
buf
решение для обработки, если вы не уверены, что ваш файл заканчивается новой строкой
\n
, в этом случае
for await
петли должно хватить.
★ Если вы предпочитаете четную асинхронную версию, это будет она:
let buf = '';
fs.createReadStream('myfile')
.on('data', chunk => {
const lines = buf.concat(chunk).split(/\r?\n/);
buf = lines.pop();
for( const line of lines ) {
console.log(line);
}
})
.on('end', () => buf.length && console.log(buf) );
★ Теперь, если вы не против импортировать
stream
core, то это эквивалентная версия конвейерного потока, которая позволяет объединять преобразования, такие как распаковка gzip:
const { Writable } = require('stream');
let buf = '';
fs.createReadStream('myfile').pipe(
new Writable({
write: (chunk, enc, next) => {
const lines = buf.concat(chunk).split(/\r?\n/);
buf = lines.pop();
for (const line of lines) {
console.log(line);
}
next();
}
})
).on('finish', () => buf.length && console.log(buf) );
var fs = require('fs');
function readfile(name,online,onend,encoding) {
var bufsize = 1024;
var buffer = new Buffer(bufsize);
var bufread = 0;
var fd = fs.openSync(name,'r');
var position = 0;
var eof = false;
var data = "";
var lines = 0;
encoding = encoding || "utf8";
function readbuf() {
bufread = fs.readSync(fd,buffer,0,bufsize,position);
position += bufread;
eof = bufread ? false : true;
data += buffer.toString(encoding,0,bufread);
}
function getLine() {
var nl = data.indexOf("\r"), hasnl = nl !== -1;
if (!hasnl && eof) return fs.closeSync(fd), online(data,++lines), onend(lines);
if (!hasnl && !eof) readbuf(), nl = data.indexOf("\r"), hasnl = nl !== -1;
if (!hasnl) return process.nextTick(getLine);
var line = data.substr(0,nl);
data = data.substr(nl+1);
if (data[0] === "\n") data = data.substr(1);
online(line,++lines);
process.nextTick(getLine);
}
getLine();
}
У меня была та же проблема, и я предложил вышеупомянутое решение, похожее на других, но это aSync и может очень быстро читать большие файлы.
Надеется, что это помогает
У меня есть небольшой модуль, который делает это хорошо и используется многими другими проектами. Npm readline Обратите внимание, что в узле v10 есть собственный модуль readline, поэтому я переиздал свой модуль как linebyline https://www.npmjs.com/package/linebyline
если вы не хотите использовать модуль, функция очень проста:
var fs = require('fs'),
EventEmitter = require('events').EventEmitter,
util = require('util'),
newlines = [
13, // \r
10 // \n
];
var readLine = module.exports = function(file, opts) {
if (!(this instanceof readLine)) return new readLine(file);
EventEmitter.call(this);
opts = opts || {};
var self = this,
line = [],
lineCount = 0,
emit = function(line, count) {
self.emit('line', new Buffer(line).toString(), count);
};
this.input = fs.createReadStream(file);
this.input.on('open', function(fd) {
self.emit('open', fd);
})
.on('data', function(data) {
for (var i = 0; i < data.length; i++) {
if (0 <= newlines.indexOf(data[i])) { // Newline char was found.
lineCount++;
if (line.length) emit(line, lineCount);
line = []; // Empty buffer.
} else {
line.push(data[i]); // Buffer new line data.
}
}
}).on('error', function(err) {
self.emit('error', err);
}).on('end', function() {
// Emit last line if anything left over since EOF won't trigger it.
if (line.length){
lineCount++;
emit(line, lineCount);
}
self.emit('end');
}).on('close', function() {
self.emit('close');
});
};
util.inherits(readLine, EventEmitter);
const fs = require("fs")
fs.readFile('./file', 'utf-8', (err, data) => {
var innerContent;
console.log("Asynchronous read: " + data.toString());
const lines = data.toString().split('\n')
for (let line of lines)
innerContent += line + '<br>';
});
Другое решение - запустить логику через последовательного исполнителя nsynjs. Он читает файл построчно, используя модуль readline узла, и не использует обещания или рекурсию, поэтому не собирается завершать работу с большими файлами. Вот как будет выглядеть код:
var nsynjs = require('nsynjs');
var textFile = require('./wrappers/nodeReadline').textFile; // this file is part of nsynjs
function process(textFile) {
var fh = new textFile();
fh.open('path/to/file');
var s;
while (typeof(s = fh.readLine(nsynjsCtx).data) != 'undefined')
console.log(s);
fh.close();
}
var ctx = nsynjs.run(process,{},textFile,function () {
console.log('done');
});
Код выше основан на этом экзамене: https://github.com/amaksr/nsynjs/blob/master/examples/node-readline/index.js
Хотя вы, вероятно, должны использовать readline
модуль, как предполагает верхний ответ, readline
Похоже, что он ориентирован на интерфейсы командной строки, а не на чтение строк. Это также немного более непрозрачно в отношении буферизации. (Любой, кому нужен читатель, ориентированный на потоковую линию, вероятно, захочет настроить размер буфера). Модуль readline составляет ~1000 строк, а со статистикой и тестами - 34.
const EventEmitter = require('events').EventEmitter;
class LineReader extends EventEmitter{
constructor(f, delim='\n'){
super();
this.totalChars = 0;
this.totalLines = 0;
this.leftover = '';
f.on('data', (chunk)=>{
this.totalChars += chunk.length;
let lines = chunk.split(delim);
if (lines.length === 1){
this.leftover += chunk;
return;
}
lines[0] = this.leftover + lines[0];
this.leftover = lines[lines.length-1];
if (this.leftover) lines.pop();
this.totalLines += lines.length;
for (let l of lines) this.onLine(l);
});
// f.on('error', ()=>{});
f.on('end', ()=>{console.log('chars', this.totalChars, 'lines', this.totalLines)});
}
onLine(l){
this.emit('line', l);
}
}
//Command line test
const f = require('fs').createReadStream(process.argv[2], 'utf8');
const delim = process.argv[3];
const lineReader = new LineReader(f, delim);
lineReader.on('line', (line)=> console.log(line));
Вот еще более короткая версия без статистики в 19 строк:
class LineReader extends require('events').EventEmitter{
constructor(f, delim='\n'){
super();
this.leftover = '';
f.on('data', (chunk)=>{
let lines = chunk.split(delim);
if (lines.length === 1){
this.leftover += chunk;
return;
}
lines[0] = this.leftover + lines[0];
this.leftover = lines[lines.length-1];
if (this.leftover)
lines.pop();
for (let l of lines)
this.emit('line', l);
});
}
}
Я оборачиваю всю логику ежедневной обработки строк в виде модуля npm: line-kit https://www.npmjs.com/package/line-kit
// example
var count = 0
require('line-kit')(require('fs').createReadStream('/etc/issue'),
(line) => { count++; },
() => {console.log(`seen ${count} lines`)})
Я использую это:
function emitLines(stream, re){
re = re && /\n/;
var buffer = '';
stream.on('data', stream_data);
stream.on('end', stream_end);
function stream_data(data){
buffer += data;
flush();
}//stream_data
function stream_end(){
if(buffer) stream.emmit('line', buffer);
}//stream_end
function flush(){
var re = /\n/;
var match;
while(match = re.exec(buffer)){
var index = match.index + match[0].length;
stream.emit('line', buffer.substring(0, index));
buffer = buffer.substring(index);
re.lastIndex = 0;
}
}//flush
}//emitLines
используйте эту функцию в потоке и слушайте события линии, которые будут излучать.
gr-
Я просмотрел все приведенные выше ответы, все они используют стороннюю библиотеку для ее решения. Это простое решение в Node API. например
const fs= require('fs')
let stream = fs.createReadStream('<filename>', { autoClose: true })
stream.on('data', chunk => {
let row = chunk.toString('ascii')
}))
Я использую приведенный ниже код для чтения строк после того, как убедитесь, что это не каталог и его нет в списке файлов.
(function () {
var fs = require('fs');
var glob = require('glob-fs')();
var path = require('path');
var result = 0;
var exclude = ['LICENSE',
path.join('e2e', 'util', 'db-ca', 'someother-file'),
path.join('src', 'favicon.ico')];
var files = [];
files = glob.readdirSync('**');
var allFiles = [];
var patternString = [
'trade',
'order',
'market',
'securities'
];
files.map((file) => {
try {
if (!fs.lstatSync(file).isDirectory() && exclude.indexOf(file) === -1) {
fs.readFileSync(file).toString().split(/\r?\n/).forEach(function(line){
patternString.map((pattern) => {
if (line.indexOf(pattern) !== -1) {
console.log(file + ' contain `' + pattern + '` in in line "' + line +'";');
result = 1;
}
});
});
}
} catch (e) {
console.log('Error:', e.stack);
}
});
process.exit(result);
})();