Costruzione di uno strumento di convalida per destinatari di uccelli in blocco e in modo asincrono

Per chi cerca un programma semplice e veloce che prenda un CSV, chiami l'API di convalida del destinatario e produca un CSV, questo programma fa per te.

Autore

Zachary Samuels

Categoria

Email

Costruzione di uno strumento di convalida per destinatari di uccelli in blocco e in modo asincrono

Per chi cerca un programma semplice e veloce che prenda un CSV, chiami l'API di convalida del destinatario e produca un CSV, questo programma fa per te.

Autore

Zachary Samuels

Categoria

Email

Costruzione di uno strumento di convalida per destinatari di uccelli in blocco e in modo asincrono

Per chi cerca un programma semplice e veloce che prenda un CSV, chiami l'API di convalida del destinatario e produca un CSV, questo programma fa per te.

Autore

Zachary Samuels

Categoria

Email

Una delle domande che riceviamo occasionalmente è: come posso validare in massa le liste di email con la validazione dei destinatari? Ci sono due opzioni qui: una è caricare un file tramite l'interfaccia SparkPost per la validazione, e l'altra è effettuare chiamate individuali per email all'API (poiché l'API è validazione di singole email).


La prima opzione funziona benissimo, ma ha una limitazione di 20Mb (circa 500.000 indirizzi). E se qualcuno avesse una lista di email contenente milioni di indirizzi? Potrebbe significare suddividerla in migliaia di caricamenti di file CSV.


Dato che caricare migliaia di file CSV sembra un po' inverosimile, ho preso in considerazione questo caso d'uso e ho iniziato a chiedermi quanto velocemente potessi far funzionare l'API. In questo articolo del blog, spiegherò cosa ho provato e come sono alla fine arrivato a un programma che poteva effettuare 100.000 validazioni in 55 secondi (mentre nell'interfaccia ho ottenuto circa 100.000 validazioni in 1 minuto e 10 secondi). E mentre ci vorrebbero ancora circa 100 ore per completare circa 654 milioni di validazioni, questo script può funzionare in background risparmiando tempo significativo.


La versione finale di questo programma può essere trovata qui.


Il mio primo errore: usare Python

Python è uno dei miei linguaggi di programmazione preferiti. Eccelle in molti ambiti ed è incredibilmente semplice. Tuttavia, un ambito in cui non eccelle è nei processi concorrenti. Anche se Python ha la capacità di eseguire funzioni asincrone, ha quello che è noto come Il Lock Globale dell'Interprete Python, o GIL.


“Il Lock Globale dell'Interprete Python o GIL, in parole semplici, è un mutex (o una lock) che consente solo a un thread di mantenere il controllo dell'interprete Python.


Questo significa che solo un thread può trovarsi in uno stato di esecuzione in ogni momento. L'impatto del GIL non è visibile agli sviluppatori che eseguono programmi a thread singolo, ma può essere un collo di bottiglia delle prestazioni nei codici legati alla CPU e multi-threaded.


Poiché il GIL consente solo a un thread di eseguire alla volta anche in un'architettura multi-threaded con più di un core CPU, il GIL ha guadagnato una reputazione come una caratteristica "infame" di Python.” (https://realpython.com/python-gil/)”


All'inizio non ero a conoscenza del GIL, quindi ho iniziato a programmare in Python. Alla fine, anche se il mio programma era asincrono, si bloccava, e non importa quanti thread aggiungevo, ottenevo solo circa 12-15 iterazioni al secondo.


La parte principale della funzione asincrona in Python può essere vista di seguito:

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)

 

Quindi ho abbandonato l'uso di Python e sono tornato al tavolo da disegno…


Ho deciso di utilizzare NodeJS grazie alla sua capacità di eseguire operazioni di I/O non bloccanti estremamente bene. Inoltre sono abbastanza familiare con la programmazione in NodeJS.


Utilizzando gli aspetti asincroni di NodeJS, questo ha funzionato bene. Per maggiori dettagli sulla programmazione asincrona in NodeJS, vedere https://blog.risingstack.com/node-hero-async-programming-in-node-js/


Il mio secondo errore: cercare di leggere il file in memoria

La mia idea iniziale era la seguente:



Primo, ingerire una lista di email in formato CSV. Secondo, caricare le email in un array e controllare che siano nel formato corretto. Terzo, chiamare l'API di validazione dei destinatari in modo asincrono. Quarto, attendere i risultati e caricarli in una variabile. E infine, esportare questa variabile in un file CSV.


Questo funzionava molto bene per file di dimensioni più piccole. Il problema è sorto quando ho provato a eseguire 100.000 email. Il programma si è bloccato attorno a 12.000 validazioni. Con l'aiuto di uno dei nostri sviluppatori front-end, ho visto che il problema era legato al caricamento di tutti i risultati in una variabile (e quindi esaurendo rapidamente la memoria). Se desideri vedere la prima iterazione di questo programma, l'ho collegata qui: Versione 1 (NON RACCOMANDATO).



Primo, ingerire una lista di email in formato CSV. Secondo, contare il numero di email nel file per scopi di reportistica. Terzo, mentre ogni riga viene letta in modo asincrono, chiamare l'API di validazione dei destinatari e esportare i risultati in un file CSV.


Quindi, per ogni riga letta, chiamo l'API e scrivo i risultati in modo asincrono in modo da non mantenere nessuno di questi dati in memoria a lungo termine. Ho anche rimosso il controllo della sintassi delle email dopo aver parlato con il team di validazione dei destinatari, poiché mi hanno informato che la validazione dei destinatari ha già controlli incorporati per verificare se un'email è valida o meno.


Analizzare il codice finale

Dopo aver letto e convalidato gli argomenti terminali, eseguo il seguente codice. Per prima cosa, leggo il file CSV delle email e conto ogni riga. Ci sono due scopi di questa funzione: 1) mi consente di riportare accuratamente i progressi del file [come vedremo più avanti], e 2) mi permette di fermare un timer quando il numero di email nel file è uguale alle validazioni completate. Ho aggiunto un timer in modo da poter eseguire benchmark e assicurarmi di ottenere buoni risultati.


let count = 0; //Conteggio delle righe require("fs") .createReadStream(myArgs[1]) .on("data", function (chunk) { for (let i = 0; i < chunk.length; ++i) if (chunk[i] == 10) count++; }) //Legge il file e aumenta il conteggio per ogni riga .on("close", function () { //Alla fine del file, dopo che tutte le righe sono state contate, esegui la funzione di validazione del destinatario validateRecipients.validateRecipients(count, myArgs); });

 

Quindi chiamo la funzione validateRecipients. Nota che questa funzione è asincrona. Dopo aver convalidato che il file di input e il file di output siano CSV, scrivo una riga di intestazione e avvio un timer del programma utilizzando la libreria JSDOM.


async function validateRecipients(email_count, myArgs) { if ( //Se sia il file di input che il file di output sono in formato .csv extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; //Contatore per ogni chiamata API email_count++; //Il contatore delle righe restituisce #righe - 1, questo è fatto per correggere il numero di righe //Inizia un timer const { window } = new JSDOM(); const start = window.performance.now(); const output = fs.createWriteStream(myArgs[3]); //File di output output.write( "Email,Valid,Result,Reason,Is_Role,Is_Disposable,Is_Free,Delivery_Confidence\n" ); //Scrive le intestazioni nel file di output

 

Lo script seguente è davvero il grosso del programma, quindi lo dividerò e spiegherò cosa sta succedendo. Per ogni riga del file di input:


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, }, }) //Per ogni riga letta dal file di input, chiama l'API di validazione dei destinatari di SparkPost

 

Quindi, sulla risposta

  • Aggiungi l'email al JSON (per poter stampare l'email nel CSV)

  • Convalida se il motivo è nullo e, in tal caso, popolalo con un valore vuoto (questo è così che il formato CSV rimane coerente, poiché in alcuni casi il motivo viene fornito nella risposta)

  • Imposta le opzioni e le chiavi per il modulo json2csv.

  • Converti il JSON in CSV e esporta (utilizzando json2csv)

  • Scrivi i progressi nel terminale

  • Infine, se il numero di email nel file = validazioni completate, ferma il timer e stampa i risultati


.then(function (response) { response.data.results.email = String(email); //Aggiunge l'email come valore/coppia chiave al JSON di risposta da usare per l'output response.data.results.reason ? null : (response.data.results.reason = ""); //Se il motivo è nullo, impostalo su vuoto in modo che il CSV sia uniforme //Utilizza json-2-csv per convertire il JSON in formato CSV e esportare let options = { prependHeader: false, //Disabilita l'aggiunta di valori JSON come righe di intestazione per ogni riga keys: [ "results.email", "results.valid", "results.result", "results.reason", "results.is_role", "results.is_disposable", "results.is_free", "results.delivery_confidence", ], //Imposta l'ordine delle chiavi }; let json2csvCallback = function (err, csv) { if (err) throw err; output.write(`${csv}\n`); }; converter.json2csv(response.data, json2csvCallback, options); completed++; //Aumenta il contatore API process.stdout.write(`Fatto con ${completed} / ${email_count}\r`); //Stampa lo stato di Completato / Totale nella console senza mostrare nuove righe //Se tutte le email hanno completato la validazione se (completed == email_count) { const stop = window.performance.now(); //Ferma il timer console.log( `Tutte le email validate con successo in ${ (stop - start) / 1000 } secondi` ); } })

 

Un ultimo problema che ho riscontrato è che mentre questo funzionava alla grande su Mac, ho riscontrato il seguente errore usando Windows dopo circa 10.000 validazioni:


Errore: connect ENOBUFS XX.XX.XXX.XXX:443 – Locale (undefined:undefined) con email XXXXXXX@XXXXXXXXXX.XXX


Dopo aver fatto ulteriori ricerche, sembra che sia un problema con il pool di connessione del client HTTP di NodeJS che non riutilizza le connessioni. Ho trovato questo articolo di Stackoverflow sul problema e dopo ulteriori indagini, ho trovato una buona configurazione predefinita per la libreria axios che ha risolto questo problema. Non sono ancora certo del perché questo problema si verifichi solo su Windows e non su Mac.


Prossimi Passi

Per chi cerca un programma semplice e veloce che prende un csv, chiama l'API di validazione dei destinatari e esporta un CSV, questo programma è per te.


Alcune aggiunte a questo programma sarebbero le seguenti:

  • Costruire un front-end o un'interfaccia più facile da usare

  • Migliore gestione degli errori e dei tentativi perché se per qualche motivo l'API restituisce un errore, attualmente il programma non ripete la chiamata


Sarei anche curioso di vedere se risultati più rapidi potrebbero essere ottenuti con un altro linguaggio come Golang o Erlang/Elixir.


Ti prego di fornirmi qualsiasi feedback o suggerimenti per espandere questo progetto.

Sign up

La piattaforma alimentata dall'IA per Marketing, Supporto e Finanza

Cliccando su "Richiedi una demo" accetti di Bird's

Sign up

La piattaforma alimentata dall'IA per Marketing, Supporto e Finanza

Cliccando su "Richiedi una demo" accetti di Bird's

Sign up

La piattaforma alimentata dall'IA per Marketing, Supporto e Finanza

Cliccando su "Richiedi una demo" accetti di Bird's

Channels

Grow

Engage

Automate

APIs

Resources

Company

Socials

Crescere

Gestire

Automatizzare

Crescere

Gestire

Automatizzare