Як я читаю вміст потоку Node.js в змінну рядка?


113

Я зламаю програму Node, яка використовує smtp-protocolдля зйомки SMTP-електронних листів та дії над поштовими даними. Бібліотека надає поштові дані як потік, і я не знаю, як це перетворити в рядок.

Я зараз пишу це в stdout stream.pipe(process.stdout, { end: false }), але, як я вже сказав, мені потрібні дані потоку в рядку, який я можу використовувати, коли потік закінчився.

Як зібрати всі дані з потоку Node.js у рядок?


Ви повинні скопіювати потік або позначити його (autoClose: false). Це погана практика забруднювати пам’ять.
19

Відповіді:


41

(Ця відповідь була з років тому, коли це була найкраща відповідь. Зараз є краща відповідь нижче цього. Я не відставав від node.js, і я не можу видалити цю відповідь, оскільки вона позначена "правильною в цьому питанні. ". Якщо ви думаєте про натискання вниз, що ви хочете, щоб я робив це?)

Ключовим моментом є використання dataта endподії читаного потоку . Послухайте ці події:

stream.on('data', (chunk) => { ... });
stream.on('end', () => { ... });

Коли ви отримаєте data подію, додайте новий фрагмент даних до буфера, створеного для збору даних.

Коли ви отримаєте endподію, перетворіть заповнений буфер у рядок, якщо це необхідно. Потім зробіть те, що вам потрібно з цим зробити.


149
Пару рядків коду, що ілюструє відповідь, бажано просто вказати на посилання на API. Не погоджуйтеся з відповіддю, просто не вважайте, що вона є достатньо повною.
arcseldon

3
З новішими версіями node.js це очищення: stackoverflow.com/a/35530615/271961
Simon A. Eugster

Відповідь слід оновити, щоб не рекомендувати використовувати бібліотеку Обіцянь, а використовувати рідні Обіцянки.
Дан Даскалеску

@DanDascalescu Я з вами згоден. Проблема в тому, що я написав цю відповідь 7 років тому, і я не встигав за node.js. Якщо ви хтось ще хотів би оновити його, це було б чудово. Або я міг би просто її видалити, оскільки, здається, вже є краща відповідь. Що б ти порадив?
ControlAltDel

@ControlAltDel: Я вдячний за вашу ініціативу видалити відповідь, яка вже не найкраща. Бажаючи, щоб інші мали подібну дисципліну .
Дан Даскалеску

129

Іншим способом було б перетворення потоку в обіцянку (див. Приклад нижче) та використання then(або await) для присвоєння розв'язаному значенню змінній.

function streamToString (stream) {
  const chunks = []
  return new Promise((resolve, reject) => {
    stream.on('data', chunk => chunks.push(chunk))
    stream.on('error', reject)
    stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
  })
}

const result = await streamToString(stream)

Я дійсно новим для потоків і обіцянок , і я отримую цю помилку: SyntaxError: await is only valid in async function. Що я роблю неправильно?
ДжонК

Вам потрібно викликати функцію потокового потоку в межах функції асинхронізації. Щоб цього уникнути, ви також можете зробитиstreamToString(stream).then(function(response){//Do whatever you want with response});
Enclo Creations

23
Це має бути головна відповідь. Вітаємо, що створили єдине рішення, в якому все налагоджено, (1) зберігання фрагментів як буферів і лише виклик .toString("utf8")в кінці, щоб уникнути проблеми відмови декодування, якщо шматок розділений посередині багатобайтового символу; (2) фактична обробка помилок; (3) введення коду у функцію, щоб він міг повторно використовуватись, а не вставляти копію; (4) використовуючи Обіцянки, щоб функція могла бути awaitвключена; (5) невеликий код, який не перетягує мільйон залежностей, на відміну від певних бібліотек npm; (6) Синтаксис ES6 та найкращі сучасні практики.
MultiplyByZer0

Чому б не перемістити масив шматок у обіцянку?
Дженні О'Рейлі

1
Після того, як я придумав фактично той самий код, використовуючи поточний верхній відповідь, як і підказку, я помітив, що вищезгаданий код може вийти з ладу, Uncaught TypeError [ERR_INVALID_ARG_TYPE]: The "list[0]" argument must be an instance of Buffer or Uint8Array. Received type stringякщо потік створює stringшматки замість Buffer. Використання chunks.push(Buffer.from(chunk))повинно працювати з обома stringта Bufferкусками.
Андрій LED

67

Ніщо з перерахованого вище не працювало для мене. Мені потрібно було використовувати об'єкт Buffer:

  const chunks = [];

  readStream.on("data", function (chunk) {
    chunks.push(chunk);
  });

  // Send the buffer or you can put it into a var
  readStream.on("end", function () {
    res.send(Buffer.concat(chunks));
  });

7
це насправді найчистіший спосіб зробити це;)
Іво

7
Чудово працює. Лише зауваження: якщо вам потрібний правильний тип рядка, вам потрібно буде зателефонувати .toString () на отриманий об'єкт Buffer з виклику concat ()
Брайан Джонсон

64

Сподіваюся, це корисніше, ніж наведена вище відповідь:

var string = '';
stream.on('data',function(data){
  string += data.toString();
  console.log('stream data ' + part);
});

stream.on('end',function(){
  console.log('final output ' + string);
});

Зауважте, що конкатенація рядків - не найефективніший спосіб збирання частин рядків, але він використовується для простоти (і, можливо, ваш код не піклується про ефективність).

Крім того, цей код може спричинити непередбачувані збої для тексту, що не належить до ASCII (передбачається, що кожен символ входить в байт), але, можливо, вас це теж не хвилює.


4
Який був би більш ефективний спосіб збирання частин струн? TY
sean2078

2
ви можете використовувати буфер docs.nodejitsu.com/articles/advanced/buffers/how-to-use-buffers, але це дійсно залежить від вашого використання.
Tom Carchrae

2
Використовуйте масив рядків, де ви додаєте кожен новий фрагмент до масиву та зателефонуєте join("")до масиву в кінці.
Валерій Палош

14
Це неправильно. Якщо буфер знаходиться на півдорозі через багатобайтову кодову точку, то toString () отримає неправильну форму utf-8, і ви отримаєте кучу у рядку.
alextgordon

2
@alextgordon має рацію. У деяких дуже рідкісних випадках, коли в мене було багато шматок, я отримував ці на початку та наприкінці шматків. Особливо, коли там, де російські символи по краях. Тож правильним є збивання шматочків та перетворення їх наприкінці замість того, щоб перетворювати шматки та об'єднувати їх. У моєму випадку запит робився від однієї служби до іншої з request.js з кодуванням за замовчуванням
Майк Єрмолаєв

21

Я зазвичай використовую цю просту функцію для перетворення потоку в рядок:

function streamToString(stream, cb) {
  const chunks = [];
  stream.on('data', (chunk) => {
    chunks.push(chunk.toString());
  });
  stream.on('end', () => {
    cb(chunks.join(''));
  });
}

Приклад використання:

let stream = fs.createReadStream('./myFile.foo');
streamToString(stream, (data) => {
  console.log(data);  // data is now my string variable
});

1
Корисна відповідь, але схоже, що кожен фрагмент повинен бути перетворений на рядок перед тим, як його натиснути в масив:chunks.push(chunk.toString());
Ніколя Ле Тьєррі д'Енненекен

1
Це єдиний, хто працював на мене! Велика подяка
538ROMEO

1
Це була чудова відповідь!
Aft3rL1f3

12

І ще одна для рядків із використанням обіцянок:

function getStream(stream) {
  return new Promise(resolve => {
    const chunks = [];

    # Buffer.from is required if chunk is a String, see comments
    stream.on("data", chunk => chunks.push(Buffer.from(chunk)));
    stream.on("end", () => resolve(Buffer.concat(chunks).toString()));
  });
}

Використання:

const stream = fs.createReadStream(__filename);
getStream(stream).then(r=>console.log(r));

при необхідності видаліть .toString()для використання бінарні дані.

оновлення : @AndreiLED правильно вказав, що це проблеми з рядками. Я не міг отримати рядки, що повертають потоки, з версією вузла, який у мене є, але api зазначає, що це можливо.


Я помітив, що вищезгаданий код може не вдатися, Uncaught TypeError [ERR_INVALID_ARG_TYPE]: The "list[0]" argument must be an instance of Buffer or Uint8Array. Received type stringякщо потік створює stringшматки замість Buffer. Використання chunks.push(Buffer.from(chunk))повинно працювати з обома stringта Bufferкусками.
Андрій LED

хороший момент, я оновив відповідь. Дякую.
Естані

8

З документації на nodejs ви повинні це зробити - завжди пам'ятайте рядок, не знаючи, що кодування є лише купою байтів:

var readable = getReadableStreamSomehow();
readable.setEncoding('utf8');
readable.on('data', function(chunk) {
  assert.equal(typeof chunk, 'string');
  console.log('got %d characters of string data', chunk.length);
})

6

У потоків немає ні простої .toString()функції (яку я розумію), ні чогось подібного до .toStringAsync(cb)функції (якої я не розумію).

Тому я створив свою власну функцію помічника:

var streamToString = function(stream, callback) {
  var str = '';
  stream.on('data', function(chunk) {
    str += chunk;
  });
  stream.on('end', function() {
    callback(str);
  });
}

// how to use:
streamToString(myStream, function(myStr) {
  console.log(myStr);
});

4

У мене було більше удачі, використовуючи таке:

let string = '';
readstream
    .on('data', (buf) => string += buf.toString())
    .on('end', () => console.log(string));

Я використовую вузол, v9.11.1і readstreamце відповідь від http.getзворотного дзвінка.


3

Найчистішим рішенням може бути використання пакету "string-stream", який перетворює потік у рядок із обіцянкою.

const streamString = require('stream-string')

streamString(myStream).then(string_variable => {
    // myStream was converted to a string, and that string is stored in string_variable
    console.log(string_variable)

}).catch(err => {
     // myStream emitted an error event (err), so the promise from stream-string was rejected
    throw err
})

3

Простий спосіб за допомогою популярної (понад 5 мільйонів завантажень щотижня) та легкої бібліотеки отримання потоків :

https://www.npmjs.com/package/get-stream

const fs = require('fs');
const getStream = require('get-stream');

(async () => {
    const stream = fs.createReadStream('unicorn.txt');
    console.log(await getStream(stream)); //output is string
})();

2

А як щодо чогось типу редуктора потоку?

Ось приклад використання класів ES6, як ним користуватися.

var stream = require('stream')

class StreamReducer extends stream.Writable {
  constructor(chunkReducer, initialvalue, cb) {
    super();
    this.reducer = chunkReducer;
    this.accumulator = initialvalue;
    this.cb = cb;
  }
  _write(chunk, enc, next) {
    this.accumulator = this.reducer(this.accumulator, chunk);
    next();
  }
  end() {
    this.cb(null, this.accumulator)
  }
}

// just a test stream
class EmitterStream extends stream.Readable {
  constructor(chunks) {
    super();
    this.chunks = chunks;
  }
  _read() {
    this.chunks.forEach(function (chunk) { 
        this.push(chunk);
    }.bind(this));
    this.push(null);
  }
}

// just transform the strings into buffer as we would get from fs stream or http request stream
(new EmitterStream(
  ["hello ", "world !"]
  .map(function(str) {
     return Buffer.from(str, 'utf8');
  })
)).pipe(new StreamReducer(
  function (acc, v) {
    acc.push(v);
    return acc;
  },
  [],
  function(err, chunks) {
    console.log(Buffer.concat(chunks).toString('utf8'));
  })
);

1

Це працювало для мене і базується на документах Node v6.7.0 :

let output = '';
stream.on('readable', function() {
    let read = stream.read();
    if (read !== null) {
        // New stream data is available
        output += read.toString();
    } else {
        // Stream is now finished when read is null.
        // You can callback here e.g.:
        callback(null, output);
    }
});

stream.on('error', function(err) {
  callback(err, null);
})

1

setEncoding ('utf8');

Молодці Себастьян Дж вище.

У мене була "проблема буфера" з декількома рядками тестового коду, який я мав, і додав інформацію про кодування, і вона вирішила її, див. Нижче.

Продемонструйте проблему

програмне забезпечення

// process.stdin.setEncoding('utf8');
process.stdin.on('data', (data) => {
    console.log(typeof(data), data);
});

вхід

hello world

вихід

object <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64 0d 0a>

Продемонструйте розчин

програмне забезпечення

process.stdin.setEncoding('utf8'); // <- Activate!
process.stdin.on('data', (data) => {
    console.log(typeof(data), data);
});

вхід

hello world

вихід

string hello world

1

Усі перелічені відповіді, як видається, відкривають читабельний потік у поточному режимі, що не є типовим для NodeJS, і може мати обмеження, оскільки йому не вистачає підтримки зворотного тиску, яку надає NodeJS у призупиненому режимі читання потоку. Ось реалізація з використанням просто буферів, Native Stream та Native Stream перетворює та підтримку для Object Mode

import {Transform} from 'stream';

let buffer =null;    

function objectifyStream() {
    return new Transform({
        objectMode: true,
        transform: function(chunk, encoding, next) {

            if (!buffer) {
                buffer = Buffer.from([...chunk]);
            } else {
                buffer = Buffer.from([...buffer, ...chunk]);
            }
            next(null, buffer);
        }
    });
}

process.stdin.pipe(objectifyStream()).process.stdout

1

Що ти думаєш про це ?

// lets a ReadableStream under stream variable 
const chunks = [];

for await (let chunk of stream) {
    chunks.push(chunk)
}

const buffer  = Buffer.concat(chunks);
const str = buffer.toString("utf-8")

Працює, дуже чисто, без залежностей, приємно!
ViRuSTriNiTy

0

Використовуючи досить популярний stream-buffersпакет, який, напевно, вже є у ваших залежностях від проекту, це досить просто:

// imports
const { WritableStreamBuffer } = require('stream-buffers');
const { promisify } = require('util');
const { createReadStream } = require('fs');
const pipeline = promisify(require('stream').pipeline);

// sample stream
let stream = createReadStream('/etc/hosts');

// pipeline the stream into a buffer, and print the contents when done
let buf = new WritableStreamBuffer();
pipeline(stream, buf).then(() => console.log(buf.getContents().toString()));

0

У моєму випадку заголовки відповідей типу вмісту були Content-Type: text / plain . Отже, я читав дані з буфера на зразок:

let data = [];
stream.on('data', (chunk) => {
 console.log(Buffer.from(chunk).toString())
 data.push(Buffer.from(chunk).toString())
});
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.