
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.
Business in a box.
Scopri le nostre soluzioni.
Parla con il nostro team di vendita
Una delle domande che riceviamo occasionalmente è, come posso convalidare in blocco elenchi di email con la convalida del destinatario? Ci sono due opzioni qui, una è caricare un file attraverso l'interfaccia utente di SparkPost per la convalida, e l'altra è effettuare chiamate individuali per email all'API (poiché l'API è per la convalida di singole email).
La prima opzione funziona benissimo ma ha una limitazione di 20Mb (circa 500.000 indirizzi). Cosa succede se qualcuno ha un elenco di email contenente milioni di indirizzi? Potrebbe significare dover suddividere in 1000 caricamenti di file CSV.
Dato che caricare migliaia di file CSV sembra un po' inverosimile, ho considerato quel caso d'uso e ho cominciato a chiedermi quanto velocemente potrei far funzionare l'API. In questo post del blog, spiegherò cosa ho provato e come alla fine sono arrivato a un programma che poteva ottenere circa 100.000 convalide in 55 secondi (mentre nell'interfaccia utente ho ottenuto circa 100.000 convalide in 1 minuto e 10 secondi). E mentre questo richiederebbe ancora circa 100 ore per completare circa 654 milioni di convalide, 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 molte aree ed è incredibilmente semplice. Tuttavia, un'area in cui non eccelle è nei processi concorrenti. Anche se python ha la capacità di eseguire funzioni asincrone, ha quello che è noto come The Python Global Interpreter Lock o GIL.
“The Python Global Interpreter Lock o GIL, in parole semplici, è un mutex (o un blocco) che consente solo a un thread di mantenere il controllo dell'interprete Python.
Ciò significa che solo un thread può essere in uno stato di esecuzione in qualsiasi momento. L'impatto del GIL non è visibile agli sviluppatori che eseguono programmi a singolo thread, ma può essere un collo di bottiglia delle prestazioni nel codice legato alla CPU e multi-threaded.
Poiché il GIL consente solo a un thread di eseguire alla volta anche in un'architettura multi-thread con più di un core della CPU, il GIL ha guadagnato la reputazione di una funzionalità “famigerata” 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 aggiungessi, riuscivo comunque a ottenere 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 scartato l'uso di Python e sono tornato al tavolo da disegno...
Mi sono deciso a utilizzare NodeJS per la sua capacità di eseguire operazioni di i/o non bloccanti estremamente bene. Inoltre, sono abbastanza familiare con la programmazione in NodeJS.
Sfruttando gli aspetti asincroni di NodeJS, ciò ha finito per funzionare bene. Per ulteriori 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 nella memoria
Scomposizione del codice finale
Dopo aver letto e validato gli argomenti del terminale, eseguo il seguente codice. Innanzitutto, leggo il file CSV delle email e conto ogni riga. Ci sono due scopi per questa funzione, 1) mi permette 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 equivale alle validazioni completate. Ho aggiunto un timer per poter eseguire confronti e assicurarmi di ottenere buoni risultati.
let count = 0; //Conteggio 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 di input e aumenta il conteggio per ogni riga .on("close", function () { //Alla fine del file di input, dopo che tutte le righe sono state contate, eseguire la funzione di validazione dei destinatari validateRecipients.validateRecipients(count, myArgs); });
Poi chiamo la funzione validateRecipients. Nota che questa funzione è asincrona. Dopo aver validato che il file di input e il file di output sono CSV, scrivo una riga di intestazione e avvio un timer del programma usando la libreria JSDOM.
async function validateRecipients(email_count, myArgs) { if ( //Se sia il file di input che quello 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 di righe restituisce #lines - 1, questo viene fatto per correggere il numero di righe //Avvia 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" ); //Scrivi le intestazioni nel file di output
Lo script seguente è veramente il fulcro del programma, quindi lo spezzetterò e spiegherò cosa sta succedendo. Per ogni riga del file di input:
Prendere asincronamente quella riga e chiamare la API di validazione del destinatario.
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, chiamare la SparkPost Recipient Validation API
Quindi, sulla risposta
Aggiungere l'email al JSON (per poter stampare l'email nel CSV)
Verifica se il motivo è nullo, e se lo è, inserire un valore vuoto (questo per mantenere il formato CSV coerente, poiché in alcuni casi il motivo è fornito nella risposta)
Imposta le opzioni e le chiavi per il modulo json2csv.
Converti il JSON in CSV e output (utilizzando json2csv)
Scrivi il progresso nel terminale
Infine, se il numero di email nel file è uguale alle validazioni completate, fermare il timer e stampare i risultati
.then(function (response) { response.data.results.email = String(email); //Aggiunge l'email come coppia chiave/valore al JSON di risposta da utilizzare per l'output response.data.results.reason ? null : (response.data.results.reason = ""); //Se il motivo è nullo, impostalo a vuoto in modo che il CSV sia uniforme //Utilizza json-2-csv per convertire il JSON in formato CSV e output let options = { prependHeader: false, //Disabilita l'aggiunta dei 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++; //Incrementa il contatore API process.stdout.write(`Fatto con ${completed} / ${email_count}\r`); //Output stato di Completo / Totale alla console senza mostrare nuove righe //Se tutte le email hanno completato la validazione if (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 è stato che mentre questo funzionava bene su Mac, ho incontrato il seguente errore su Windows dopo circa 10.000 validazioni:
Error: connect ENOBUFS XX.XX.XXX.XXX:443 – Local (undefined:undefined) con email XXXXXXX@XXXXXXXXXX.XXX
Dopo ulteriori ricerche, sembra essere un problema con il pool di connessioni del client HTTP di NodeJS che non riutilizza le connessioni. Ho trovato questo articolo su Stackoverflow sul problema e, dopo ulteriori approfondimenti, ho trovato un buon config predefinito 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.
Passi successivi
Per qualcuno che cerca un programma semplice e veloce che accetti un csv, chiami la recipient validation API e produca un CSV, questo programma è per te.
Alcuni aggiunte a questo programma sarebbero le seguenti:
Costruire un frontend o un'interfaccia utente più semplice per l'uso
Migliore gestione degli errori e dei tentativi perché se per qualche motivo l'API genera un errore, attualmente il programma non riprova la chiamata
Sarei anche curioso di vedere se si possono ottenere risultati più veloci con un altro linguaggio come Golang o Erlang/Elixir.
Sentiti libero di fornirmi qualsiasi feedback o suggerimento per ampliare questo progetto.