Wie lese ich den Inhalt eines Node.js-Streams in eine Zeichenfolgenvariable?

111

Ich hacke ein Knotenprogramm, mit smtp-protocoldem SMTP-E-Mails erfasst und auf die E-Mail-Daten reagiert werden. Die Bibliothek stellt die E-Mail-Daten als Stream bereit, und ich weiß nicht, wie ich das in eine Zeichenfolge umwandeln soll.

Ich schreibe es gerade, um mit stdout zu arbeiten stream.pipe(process.stdout, { end: false }), aber wie gesagt, ich brauche stattdessen die Stream-Daten in einer Zeichenfolge, die ich verwenden kann, sobald der Stream beendet ist.

Wie sammle ich alle Daten aus einem Node.js-Stream in einer Zeichenfolge?

obrienmd
quelle
Sie sollten den Stream kopieren oder mit (autoClose: false) kennzeichnen. Es ist eine schlechte Praxis, das Gedächtnis zu verschmutzen.
19h

Antworten:

41

(Diese Antwort stammt aus der Zeit vor Jahren, als es die beste Antwort war. Darunter befindet sich jetzt eine bessere Antwort. Ich habe mit node.js nicht Schritt gehalten und kann diese Antwort nicht löschen, da sie bei dieser Frage als "richtig" markiert ist ". Wenn Sie daran denken, nach unten zu klicken, was soll ich dann tun?)

Der Schlüssel besteht darin, die dataund -Ereignisseend eines lesbaren Streams zu verwenden . Hören Sie sich diese Ereignisse an:

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

Wenn Sie das dataEreignis erhalten, fügen Sie den neuen Datenblock einem Puffer hinzu, der zum Sammeln der Daten erstellt wurde.

Wenn Sie das endEreignis erhalten, konvertieren Sie gegebenenfalls den fertigen Puffer in eine Zeichenfolge. Dann tun Sie, was Sie damit tun müssen.

ControlAltDel
quelle
149
Ein paar Codezeilen, die die Antwort veranschaulichen, sind dem einfachen Zeigen eines Links auf die API vorzuziehen. Stimmen Sie der Antwort nicht zu, glauben Sie einfach nicht, dass sie vollständig genug ist.
Arcseldon
3
Mit neueren node.js-Versionen ist dies sauberer: stackoverflow.com/a/35530615/271961
Simon A. Eugster
Die Antwort sollte aktualisiert werden, um nicht die Verwendung einer Promises-Bibliothek zu empfehlen, sondern native Promises zu verwenden.
Dan Dascalescu
@ DanDascalescu Ich stimme dir zu. Das Problem ist, dass ich diese Antwort vor 7 Jahren geschrieben habe und mit node.js nicht Schritt gehalten habe. Wenn Sie jemand anderes sind, der es aktualisieren möchte, wäre das großartig. Oder ich könnte es einfach löschen, da es bereits eine bessere Antwort zu geben scheint. Was würden Sie empfehlen?
ControlAltDel
@ControlAltDel: Ich freue mich über Ihre Initiative, eine Antwort zu löschen, die nicht mehr die beste ist. Ich wünschte, andere hätten eine ähnliche Disziplin .
Dan Dascalescu
128

Eine andere Möglichkeit wäre, den Stream in ein Versprechen umzuwandeln (siehe Beispiel unten) und den aufgelösten Wert mit then(oder await) einer Variablen zuzuweisen.

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)
Marlon Bernardes
quelle
Ich bin wirklich neu in Streams und Versprechungen und erhalte den folgenden Fehler : SyntaxError: await is only valid in async function. Was mache ich falsch?
JohnK
Sie müssen die Streamtostring-Funktion innerhalb einer asynchronen Funktion aufrufen. Um dies zu vermeiden, können Sie auch tunstreamToString(stream).then(function(response){//Do whatever you want with response});
Enclo Creations
23
Dies sollte die beste Antwort sein. Herzlichen Glückwunsch zur Erstellung der einzigen Lösung, die alles richtig macht: (1) Speichern der Chunks als Puffer und Aufrufen nur .toString("utf8")am Ende, um das Problem eines Dekodierungsfehlers zu vermeiden, wenn ein Chunk in die Mitte eines Multibyte-Zeichens aufgeteilt wird. (2) tatsächliche Fehlerbehandlung; (3) Einfügen des Codes in eine Funktion, damit er wiederverwendet und nicht kopiert werden kann; (4) Verwenden von Versprechungen, damit die Funktion awaitaktiviert werden kann; (5) kleiner Code, der im Gegensatz zu bestimmten npm-Bibliotheken nicht eine Million Abhängigkeiten mit sich bringt; (6) ES6-Syntax und moderne Best Practices.
MultiplyByZer0
Warum nicht das Chunks-Array in das Versprechen verschieben?
Jenny O'Reilly
1
Nachdem ich im Wesentlichen denselben Code mit der aktuellen Top-Antwort als Hinweis gefunden habe, habe ich festgestellt, dass der obige Code fehlschlagen kann, Uncaught TypeError [ERR_INVALID_ARG_TYPE]: The "list[0]" argument must be an instance of Buffer or Uint8Array. Received type stringwenn der Stream stringstattdessen Chunks erzeugt Buffer. Die Verwendung chunks.push(Buffer.from(chunk))sollte mit beiden stringund BufferChunks funktionieren .
Andrei LED
67

Keines der oben genannten hat bei mir funktioniert. Ich musste das Buffer-Objekt verwenden:

  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));
  });
Ricky
quelle
7
Dies ist eigentlich die sauberste Art, es zu tun;)
Ivo
7
Funktioniert super. Nur eine Anmerkung: Wenn Sie einen geeigneten Zeichenfolgentyp wünschen, müssen Sie .toString () für das resultierende Buffer-Objekt aus dem Aufruf von concat () aufrufen
Bryan Johnson
64

Hoffe das ist nützlicher als die obige Antwort:

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

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

Beachten Sie, dass die Verkettung von Zeichenfolgen nicht die effizienteste Methode zum Sammeln der Zeichenfolgenteile ist, sondern der Einfachheit halber verwendet wird (und Ihr Code kümmert sich möglicherweise nicht um die Effizienz).

Dieser Code kann auch zu unvorhersehbaren Fehlern bei Nicht-ASCII-Text führen (es wird davon ausgegangen, dass jedes Zeichen in ein Byte passt), aber das interessiert Sie vielleicht auch nicht.

Tom Carchrae
quelle
4
Was wäre eine effizientere Möglichkeit, Saitenteile zu sammeln? TY
Sean2078
2
Sie könnten einen Puffer verwenden docs.nodejitsu.com/articles/advanced/buffers/how-to-use-buffers, aber es hängt wirklich von Ihrer Verwendung ab.
Tom Carchrae
2
Verwenden Sie ein Array von Zeichenfolgen, in denen Sie jeden neuen Block an das Array anhängen und join("")das Array am Ende aufrufen .
Valeriu Paloş
14
Das ist nicht richtig. Wenn sich der Puffer in der Mitte eines Multi-Byte-Codepunkts befindet, empfängt toString () das fehlerhafte utf-8 und Sie erhalten ein Bündel von in Ihrer Zeichenfolge.
Alextgordon
2
@alextgordon ist richtig. In einigen sehr seltenen Fällen, in denen ich viele Brocken hatte, bekam ich diese am Anfang und am Ende von Brocken. Besonders wenn dort russische Symbole an den Rändern waren. Es ist also richtig, Chunks zu verketten und am Ende zu konvertieren, anstatt Chunks zu konvertieren und zu verketten. In meinem Fall wurde die Anfrage von einem Dienst zum anderen mit request.js mit Standardcodierung gestellt
Mike
21

Normalerweise verwende ich diese einfache Funktion, um einen Stream in einen String umzuwandeln:

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

Anwendungsbeispiel:

let stream = fs.createReadStream('./myFile.foo');
streamToString(stream, (data) => {
  console.log(data);  // data is now my string variable
});
Traumpuls
quelle
1
Nützliche Antwort, aber es sieht so aus, als chunks.push(chunk.toString());
müsste jeder Block
1
Dies ist der einzige, der für mich gearbeitet hat!
Vielen
1
Das war eine großartige Antwort!
Aft3rL1f3
12

Und noch eine für Streicher, die Versprechen verwenden:

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()));
  });
}

Verwendung:

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

Entfernen Sie .toString()bei Bedarf die zur Verwendung mit Binärdaten zu verwendenden.

Update : @AndreiLED hat richtig darauf hingewiesen, dass dies Probleme mit Strings hat. Ich konnte mit der Version des Knotens, die ich habe, keinen Stream zurückgeben, der Zeichenfolgen zurückgibt , aber die API stellt fest, dass dies möglich ist.

estani
quelle
Ich habe festgestellt, dass der obige Code fehlschlagen kann, Uncaught TypeError [ERR_INVALID_ARG_TYPE]: The "list[0]" argument must be an instance of Buffer or Uint8Array. Received type stringwenn der Stream stringstattdessen Chunks erzeugt Buffer. Die Verwendung chunks.push(Buffer.from(chunk))sollte mit beiden stringund BufferChunks funktionieren .
Andrei LED
Guter Punkt, ich habe die Antwort aktualisiert. Vielen Dank.
Estani
8

In der Dokumentation zu nodejs sollten Sie dies tun - denken Sie immer an eine Zeichenfolge, ohne zu wissen, dass die Codierung nur ein Bündel von Bytes ist:

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);
})
Sebastian J.
quelle
6

Streams haben weder eine einfache .toString()Funktion (die ich verstehe) noch so etwas wie eine .toStringAsync(cb)Funktion (die ich nicht verstehe).

Also habe ich meine eigene Hilfsfunktion erstellt:

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);
});
flori
quelle
4

Ich hatte mehr Glück damit:

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

Ich benutze Node v9.11.1und das readstreamist die Antwort von einem http.getRückruf.

vdegenne
quelle
3

Die sauberste Lösung könnte darin bestehen, das Paket "string-stream" zu verwenden, das einen Stream mit einem Versprechen in einen String konvertiert.

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
})
Steve Breese
quelle
3

Einfacher Weg mit der beliebten (über 5 Millionen wöchentliche Downloads) und leichten Get-Stream- Bibliothek:

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
})();
Ville
quelle
2

Was ist mit so etwas wie einem Stromreduzierer?

Hier ist ein Beispiel für die Verwendung von ES6-Klassen.

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'));
  })
);
Fred
quelle
1

Dies hat bei mir funktioniert und basiert auf Node v6.7.0 docs :

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);
})
Anthonygore
quelle
1

setEncoding ('utf8');

Gut gemacht Sebastian J oben.

Ich hatte das "Pufferproblem" mit ein paar Zeilen Testcode, die ich hatte, und fügte die Codierungsinformationen hinzu und es löste es, siehe unten.

Demonstrieren Sie das Problem

Software

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

Eingang

hello world

Ausgabe

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

Demonstrieren Sie die Lösung

Software

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

Eingang

hello world

Ausgabe

string hello world
Ivan
quelle
1

Alle aufgelisteten Antworten scheinen den lesbaren Stream im fließenden Modus zu öffnen, was in NodeJS nicht die Standardeinstellung ist und Einschränkungen aufweisen kann, da NodeJS im angehaltenen lesbaren Stream-Modus keine Unterstützung für den Gegendruck bietet. Hier ist eine Implementierung mit Just Buffers, Native Stream und Native Stream Transforms und Unterstützung für den Objektmodus

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
herlarby
quelle
0

Mit dem sehr beliebten stream-buffersPaket, das Sie wahrscheinlich bereits in Ihren Projektabhängigkeiten haben, ist dies ziemlich einfach:

// 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()));
andrewdotn
quelle
0

In meinem Fall lautete der Inhaltstyp der Antwortheader Inhaltstyp: Text / Klartext . Also habe ich die Daten von Buffer wie folgt gelesen:

let data = [];
stream.on('data', (chunk) => {
 console.log(Buffer.from(chunk).toString())
 data.push(Buffer.from(chunk).toString())
});
Dionis Oros
quelle