Entwicklung eines Bulk-Asynchronen Vogel-Empfänger-Validierungstools

Für jemanden, der nach einem einfachen, schnellen Programm sucht, das eine CSV-Datei einliest, die Empfänger-Validierungs-API aufruft und eine CSV-Datei ausgibt, ist dieses Programm genau das Richtige für Sie.

Author

Zachary Samuels

Kategorie

E-Mail

Entwicklung eines Bulk-Asynchronen Vogel-Empfänger-Validierungstools

Für jemanden, der nach einem einfachen, schnellen Programm sucht, das eine CSV-Datei einliest, die Empfänger-Validierungs-API aufruft und eine CSV-Datei ausgibt, ist dieses Programm genau das Richtige für Sie.

Author

Zachary Samuels

Kategorie

E-Mail

Entwicklung eines Bulk-Asynchronen Vogel-Empfänger-Validierungstools

Für jemanden, der nach einem einfachen, schnellen Programm sucht, das eine CSV-Datei einliest, die Empfänger-Validierungs-API aufruft und eine CSV-Datei ausgibt, ist dieses Programm genau das Richtige für Sie.

Author

Zachary Samuels

Kategorie

E-Mail

Eines der Fragen, die wir gelegentlich erhalten, ist, wie kann ich E-Mail-Listen im Bulk mit Empfänger-Validierung validieren? Es gibt zwei Optionen: eine ist, eine Datei über die SparkPost UI zur Validierung hochzuladen, und die andere besteht darin, einzelne Anfragen pro E-Mail an die API zu stellen (da die API die Validierung einzelner E-Mails ist).


Die erste Option funktioniert großartig, hat jedoch eine Begrenzung von 20 Mb (etwa 500.000 Adressen). Was, wenn jemand eine E-Mail-Liste mit Millionen von Adressen hat? Das könnte bedeuten, dass man diese in 1.000 CSV-Datei-Uploads aufteilen muss.


Da das Hochladen von Tausenden von CSV-Dateien etwas weit hergeholt erscheint, habe ich diesen Anwendungsfall genommen und begonnen, mich zu fragen, wie schnell ich die API zum Laufen bringen könnte. In diesem Blogbeitrag werde ich erklären, was ich versucht habe und wie ich schließlich zu einem Programm gekommen bin, das etwa 100.000 Validierungen in 55 Sekunden durchführen konnte (während ich in der UI etwa 100.000 Validierungen in 1 Minute und 10 Sekunden erhalten habe). Und während dies immer noch etwa 100 Stunden dauern würde, um mit etwa 654 Millionen Validierungen abgeschlossen zu werden, kann dieses Skript im Hintergrund laufen und damit erheblich Zeit sparen.


Die endgültige Version dieses Programms finden Sie hier.


Mein erster Fehler: Verwendung von Python

Python ist eine meiner Lieblingsprogrammiersprachen. Es glänzt in vielen Bereichen und ist unglaublich unkompliziert. Allerdings ist ein Bereich, in dem es nicht glänzt, die gleichzeitige Verarbeitung. Während Python die Möglichkeit hat, asynchrone Funktionen auszuführen, hat es das, was als der Python Global Interpreter Lock oder GIL bekannt ist.


„Der Python Global Interpreter Lock oder GIL ist in einfachen Worten ein Mutex (oder ein Lock), der es nur einem Thread ermöglicht, die Kontrolle über den Python-Interpreter zu haben.


Das bedeutet, dass zu jedem Zeitpunkt nur ein Thread in einem Zustand der Ausführung sein kann. Die Auswirkungen des GIL sind für Entwickler, die einzeilig programmierte Programme ausführen, nicht sichtbar, können jedoch ein Leistungsengpass in CPU-gebundener und mehrprozessorfähiger Software sein.


Da das GIL nur einen Thread gleichzeitig ausführen lässt, selbst in einer mehrprozessfähigen Architektur mit mehr als einem CPU-Kern, hat das GIL den Ruf eines „berüchtigten“ Merkmals von Python erlangt.“ (https://realpython.com/python-gil/)”


Zu Beginn war mir das GIL nicht bewusst, also begann ich mit der Programmierung in Python. Am Ende, obwohl mein Programm asynchron war, wurde es blockiert, und egal, wie viele Threads ich hinzufügte, ich erhielt immer noch nur etwa 12–15 Iterationen pro Sekunde.


Der Hauptteil der asynchronen Funktion in Python kann unten gesehen werden:

async def validateRecipients(f, fh, apiKey, snooze, count): h = {'Authorization': apiKey, 'Accept': 'application/json'} with tqdm(total=count) as pbar: async with aiohttp.ClientSession() as session: for address in f: for i in address: thisReq = requests.compat.urljoin(url, i) async with session.get(thisReq,headers=h, ssl=False) as resp: content = await resp.json() row = content['results'] row['email'] = i fh.writerow(row) pbar.update(1)

 

Also habe ich die Verwendung von Python verworfen und bin zurück zum Zeichenbrett gegangen…


Ich habe mich entschieden, NodeJS zu nutzen, aufgrund seiner Fähigkeit, nicht blockierende I/O-Operationen extrem gut auszuführen. Ich bin auch ziemlich vertraut mit der Programmierung in NodeJS.


Die Nutzung der asynchronen Aspekte von NodeJS funktionierte gut. Für weitere Details zur asynchronen Programmierung in NodeJS siehe https://blog.risingstack.com/node-hero-async-programming-in-node-js/


Mein zweiter Fehler: Versuche, die Datei in den Speicher zu lesen

Meine ursprüngliche Idee war wie folgt:



Zuerst eine CSV-Liste von E-Mails einfüllen. Zweitens die E-Mails in ein Array laden und überprüfen, ob sie im richtigen Format sind. Drittens die API zur Empfänger-Validierung asynchron aufrufen. Viertens auf die Ergebnisse warten und sie in eine Variable laden. Und schließlich diese Variable in eine CSV-Datei ausgeben.


Dies funktionierte sehr gut für kleinere Dateien. Das Problem trat auf, als ich versuchte, 100.000 E-Mails zu verarbeiten. Das Programm stockte bei etwa 12.000 Validierungen. Mit Hilfe eines unserer Frontend-Entwickler sah ich, dass das Problem beim Laden aller Ergebnisse in eine Variable lag (und damit schnell der Speicherplatz ausging). Wenn Sie die erste Iteration dieses Programms sehen möchten, habe ich es hier verlinkt: Version 1 (NICHT EMPFOHLEN).



Zuerst eine CSV-Liste von E-Mails einlesen. Zweitens die Anzahl der E-Mails in der Datei für Reporting-Zwecke zählen. Drittens, während jede Zeile asynchron gelesen wird, die API zur Empfänger-Validierung aufrufen und die Ergebnisse in eine CSV-Datei ausgeben.


Somit rufe ich für jede gelesene Zeile die API auf und schreibe die Ergebnisse asynchron aus, um keine dieser Daten im langfristigen Speicher zu speichern. Ich habe auch die E-Mail-Syntaxprüfung entfernt, nachdem ich mit dem Team zur Empfänger-Validierung gesprochen habe, da sie mich informiert haben, dass die Empfänger-Validierung bereits Prüfer integriert hat, um zu überprüfen, ob eine E-Mail gültig ist oder nicht.


Die endgültige Code-Bereich aufschlüsseln

Nachdem ich die Terminalargumente gelesen und validiert hatte, führe ich den folgenden Code aus. Zuerst lese ich die CSV-Datei mit E-Mails ein und zähle jede Zeile. Es gibt zwei Zwecke dieser Funktion: 1) Sie ermöglicht mir eine genaue Berichterstattung über den Fortschritt der Datei [wie wir später sehen werden], und 2) Sie ermöglicht mir, einen Timer zu stoppen, wenn die Anzahl der E-Mails in der Datei den abgeschlossenen Validierungen entspricht. Ich habe einen Timer hinzugefügt, damit ich Benchmarks durchführen kann und sicherstellen kann, dass ich gute Ergebnisse erzielte.


let count = 0; //Line count require("fs") .createReadStream(myArgs[1]) .on("data", function (chunk) { for (let i = 0; i < chunk.length; ++i) if (chunk[i] == 10) count++; }) //Reads the infile and increases the count for each line .on("close", function () { //At the end of the infile, after all lines have been counted, run the recipient validation function validateRecipients.validateRecipients(count, myArgs); });

 

Ich rufe dann die validateRecipients-Funktion auf. Beachten Sie, dass diese Funktion asynchron ist. Nachdem ich überprüft habe, dass die Eingabedatei und die Ausgabedatei CSV sind, schreibe ich eine Kopfzeile und starte einen Programmtimer mit der JSDOM-Bibliothek.


async function validateRecipients(email_count, myArgs) { if ( //If both the infile and outfile are in .csv format extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; //Counter for each API call email_count++; //Line counter returns #lines - 1, this is done to correct the number of lines //Start a timer const { window } = new JSDOM(); const start = window.performance.now(); const output = fs.createWriteStream(myArgs[3]); //Outfile output.write( "Email,Valid,Result,Reason,Is_Role,Is_Disposable,Is_Free,Delivery_Confidence\n" ); //Write the headers in the outfile

 

Das folgende Skript ist wirklich der Kern des Programms, daher werde ich es aufteilen und erklären, was passiert. Für jede Zeile der Eingabedatei:


fs.createReadStream(myArgs[1]) .pipe(csv.parse({ headers: false })) .on("data", async (email) => { let url = SPARKPOST_HOST + "/api/v1/recipient-validation/single/" + email; await axios .get(url, { headers: { Authorization: SPARKPOST_API_KEY, }, }) //For each row read in from the infile, call the SparkPost Recipient Validation API

 

Dann, bei der Antwort

  • Fügen Sie die E-Mail zum JSON hinzu (um die E-Mail in der CSV auszugeben)

  • Überprüfen Sie, ob der Grund null ist, und falls ja, fügen Sie einen leeren Wert hinzu (dies ist so, damit das CSV-Format konsistent ist, da in einigen Fällen der Grund in der Antwort genannt wird)

  • Setzen Sie die Optionen und Schlüssel für das Modul json2csv.

  • Konvertieren Sie das JSON in CSV und geben Sie es aus (unter Verwendung von json2csv)

  • Protokollieren Sie den Fortschritt im Terminal

  • Schließlich, wenn die Anzahl der E-Mails in der Datei = abgeschlossenen Validierungen, stoppen Sie den Timer und geben Sie die Ergebnisse aus


.then(function (response) { response.data.results.email = String(email); //Fügt die E-Mail als Wert/Schlüssel-Paar zum Antwort-JSON hinzu, um sie für die Ausgabe verwenden zu können response.data.results.reason ? null : (response.data.results.reason = ""); //Wenn der Grund null ist, setzen Sie ihn leer, damit das CSV einheitlich ist //Verwendet json-2-csv zum Konvertieren des JSON in CSV-Format und Ausgabe let options = { prependHeader: false, //Deaktiviert das Hinzufügen von JSON-Werten als Kopfzeilen für jede Zeile keys: [ "results.email", "results.valid", "results.result", "results.reason", "results.is_role", "results.is_disposable", "results.is_free", "results.delivery_confidence", ], //Setzt die Reihenfolge der Schlüssel }; let json2csvCallback = function (err, csv) { if (err) throw err; output.write(`${csv}\n`); }; converter.json2csv(response.data, json2csvCallback, options); completed++; //Erhöhen Sie den API-Zähler process.stdout.write(`Fertig mit ${completed} / ${email_count}\r`); //Geben Sie den Status von Fertig / Gesamt auf der Konsole aus, ohne neue Zeilen anzuzeigen //Wenn alle E-Mails die Validierung abgeschlossen haben, if (completed == email_count) { const stop = window.performance.now(); //Stoppen Sie den Timer console.log( `Alle E-Mails erfolgreich in ${ (stop - start) / 1000 } Sekunden validiert` ); } })

 

Ein letztes Problem, das ich fand, war, während das großartig auf dem Mac funktionierte, stieß ich auf den folgenden Fehler bei der Verwendung von Windows nach etwa 10.000 Validierungen:


Fehler: verbinden ENOBUFS XX.XX.XXX.XXX:443 – Lokal (undefiniert:undefiniert) mit E-Mail XXXXXXX@XXXXXXXXXX.XXX


Nach weiteren Recherchen stellte sich das als ein Problem mit dem HTTP-Client-Verbindungspool von NodeJS heraus, der keine Verbindungen wiederverwendet. Ich fand diesen Stackoverflow-Artikel zu dem Problem und fand nach weiterer Untersuchung eine gute Standardkonfiguration für die Axios-Bibliothek, die dieses Problem behob. Ich bin mir immer noch nicht sicher, warum dieses Problem nur unter Windows auftritt und nicht unter Mac.


Nächste Schritte

Für jemand, der nach einem einfachen, schnellen Programm sucht, das eine CSV-Datei aufnimmt, die API zur Empfänger-Validierung aufruft und eine CSV-Datei ausgibt, ist dieses Programm genau das Richtige für Sie.


Einige Ergänzungen zu diesem Programm wären die folgenden:

  • Erstellen Sie eine Frontend- oder einfachere UI zur Verwendung

  • Bessere Fehler- und Wiederholungsbehandlung, da das Programm derzeit bei einem Fehler der API den Aufruf nicht erneut versucht


Ich wäre auch neugierig zu sehen, ob mit einer anderen Sprache wie Golang oder Erlang/Elixir schnellere Ergebnisse erzielt werden könnten.


Bitte zögern Sie nicht, mir Feedback oder Vorschläge zur Erweiterung dieses Projekts zu geben.

Bereit, Bird in Aktion zu sehen?

Schedule a demo now.

Die KI-gestützte Plattform für Marketing, Support und Finanzen

Indem Sie auf "Demo anfordern" klicken, stimmen Sie Bird's zu

Die KI-gestützte Plattform für Marketing, Support und Finanzen

Indem Sie auf "Demo anfordern" klicken, stimmen Sie Bird's zu

Die KI-gestützte Plattform für Marketing, Support und Finanzen

Indem Sie auf "Demo anfordern" klicken, stimmen Sie Bird's zu