
Voor iemand die op zoek is naar een eenvoudig en snel programma dat een csv-bestand verwerkt, de ontvanger-validatie API aanroept en een CSV oplevert, is dit programma voor jou.
Business in a box.
Ontdek onze oplossingen.
Praat met ons verkoopteam
Een van de vragen die we af en toe ontvangen is, hoe kan ik e-maillijsten bulkvalideren met ontvangervalidatie? Er zijn hier twee opties: de ene is een bestand uploaden via de SparkPost UI voor validatie, en de andere is om individuele oproepen per e-mail naar de API te maken (aangezien de API eenmalige e-mailvalidatie is).
De eerste optie werkt prima, maar heeft een limiet van 20Mb (ongeveer 500.000 adressen). Wat als iemand een e-maillijst heeft met miljoenen adressen? Dat zou betekenen dat het zou moeten worden opgesplitst in duizenden CSV-bestandsuploads.
Aangezien het uploaden van duizenden CSV-bestanden een beetje vergezocht lijkt, nam ik dat gebruiksgeval en begon ik me af te vragen hoe snel ik de API kon laten draaien. In deze blogpost zal ik uitleggen wat ik heb geprobeerd en hoe ik uiteindelijk tot een programma kwam dat rond de 100.000 validaties in 55 seconden kon krijgen (terwijl in de UI ik rond de 100.000 validaties in 1 minuut en 10 seconden kreeg). Hoewel dit nog steeds ongeveer 100 uur zou duren om ongeveer 654 miljoen validaties af te ronden, kan dit script op de achtergrond draaien, wat aanzienlijke tijd bespaart.
De definitieve versie van dit programma is te vinden hier.
Mijn eerste fout: het gebruik van Python
Python is een van mijn favoriete programmeertalen. Het blinkt uit in veel gebieden en is ongelooflijk eenvoudig. Echter, een gebied waarin het niet uitblinkt is gelijktijdige processen. Hoewel Python de mogelijkheid heeft om asynchrone functies uit te voeren, heeft het iets dat bekend staat als de Python Global Interpreter Lock of GIL.
“De Python Global Interpreter Lock of GIL, in eenvoudige woorden, is een mutex (of een lock) die slechts één thread toestaat de controle van de Python-interpreter vast te houden.
Dit betekent dat slechts één thread tegelijkertijd in een uitvoeringsstatus kan zijn. De impact van de GIL is niet zichtbaar voor ontwikkelaars die enkelvoudig getreadde programma's uitvoeren, maar het kan een prestatieknelpunt zijn in CPU-belastende en multi-threaded code.
Aangezien de GIL slechts één thread toestaat om op een bepaald ogenblik uit te voeren, zelfs in een multi-threaded architectuur met meer dan één CPU-kern, heeft de GIL de reputatie opgebouwd als een “beruchte” eigenschap van Python.” (https://realpython.com/python-gil/)”
In eerste instantie was ik niet op de hoogte van de GIL, dus begon ik met programmeren in Python. Aan het einde, ook al was mijn programma asynchroon, werd het vergrendeld, en hoeveel threads ik ook toevoegde, ik kreeg nog steeds maar ongeveer 12-15 iteraties per seconde.
Het belangrijkste deel van de asynchrone functie in Python is hieronder te zien:
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)
Dus ik stopte met het gebruik van Python en begon opnieuw met het ontwerp...
Ik koos ervoor om NodeJS te gebruiken vanwege zijn vermogen om non-blocking i/o-bewerkingen zeer goed uit te voeren. Ik ben ook vrij bekend met programmeren in NodeJS.
Door gebruik te maken van de asynchrone aspecten van NodeJS, werkte dit uiteindelijk goed. Voor meer details over asynchroon programmeren in NodeJS, zie https://blog.risingstack.com/node-hero-async-programming-in-node-js/
Mijn tweede fout: proberen het bestand in het geheugen te lezen
Het uiteindelijke code ontleden
Na het lezen en valideren van de terminalargumenten voer ik de volgende code uit. Eerst lees ik het CSV-bestand met e-mails in en tel ik elke regel. Er zijn twee doelen van deze functie, 1) het stelt me in staat om nauwkeurig over de voortgang van het bestand te rapporteren [zoals we later zullen zien], en 2) het stelt me in staat om een timer te stoppen wanneer het aantal e-mails in het bestand gelijk is aan de voltooide validaties. Ik heb een timer toegevoegd zodat ik benchmarks kan uitvoeren en ervoor kan zorgen dat ik goede resultaten behaal.
let count = 0; //Lijn tellen require("fs") .createReadStream(myArgs[1]) .on("data", function (chunk) { for (let i = 0; i < chunk.length; ++i) if (chunk[i] == 10) count++; }) //Leest het invoerbestand en verhoogt de telling voor elke regel .on("close", function () { //Aan het einde van het invoerbestand, nadat alle regels zijn geteld, de functievalideringsfunctie validateRecipients.runnen validateRecipients(count, myArgs); });
Vervolgens roep ik de validateRecipients-functie aan. Merk op dat deze functie asynchroon is. Na validatie dat het invoer- en uitvoerbestand CSV-bestanden zijn, schrijf ik een kopregel en start ik een programmatimer met behulp van de JSDOM-bibliotheek.
async function validateRecipients(email_count, myArgs) { if ( //Als zowel het invoer- als uitvoerbestand in .csv formaat is extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; //Teller voor elke API-oproep email_count++; //Lijnteller retourneert #lines - 1, dit wordt gedaan om het aantal regels te corrigeren //Start een timer const { window } = new JSDOM(); const start = window.performance.now(); const output = fs.createWriteStream(myArgs[3]); //Uitvoerbestand output.write( "Email,Valid,Result,Reason,Is_Role,Is_Disposable,Is_Free,Delivery_Confidence\n" ); //Schrijf de koppen in het uitvoerbestand
Het volgende script is eigenlijk de kern van het programma, dus ik zal het opbreken en uitleggen wat er gebeurt. Voor elke regel van het invoerbestand:
Neem die regel asynchroon en roep de recipient validation API aan.
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, }, }) //Voor elke rij die uit het invoerbestand wordt gelezen, roep de SparkPost Recipient Validation API aan
Vervolgens, bij de respons
Voeg de e-mail toe aan de JSON (om de e-mail in de CSV te kunnen printen)
Valideer of de reden nul is, en zo ja, vul een lege waarde in (dit is zodat het CSV-formaat consistent is, aangezien in sommige gevallen een reden wordt gegeven in de respons)
Stel de opties en sleutels in voor de json2csv-module.
Converteer de JSON naar CSV en voer uit (gebruik makend van json2csv)
Schrijf de voortgang in de terminal
Tenslotte, als het aantal e-mails in het bestand = voltooide validaties, stop de timer en print de resultaten uit
.then(function (response) { response.data.results.email = String(email); //Voegt de e-mail toe als een waarde/sleutelpaar aan de respons-JSON voor gebruik in de uitvoer response.data.results.reason ? null : (response.data.results.reason = ""); //Als de reden nul is, zet deze op leeg zodat de CSV uniform is //Maakt gebruik van json-2-csv om de JSON naar CSV-formaat te converteren en uitvoeren let options = { prependHeader: false, //Schakelt JSON-waarden uit om als koprijen voor elke regel toe te voegen keys: [ "results.email", "results.valid", "results.result", "results.reason", "results.is_role", "results.is_disposable", "results.is_free", "results.delivery_confidence", ], //Stelt de volgorde van sleutels in }; let json2csvCallback = function (err, csv) { if (err) throw err; output.write(`${csv}\n`); }; converter.json2csv(response.data, json2csvCallback, options); completed++; //Verhoog de API-teller process.stdout.write(`Klaar met ${completed} / ${email_count}\r`); //Output status van Voltooid / Totaal naar de console zonder nieuwe regels te tonen //Als alle e-mails zijn gevalideerd if (completed == email_count) { const stop = window.performance.now(); //Stop de timer console.log( `Alle e-mails met succes gevalideerd in ${ (stop - start) / 1000 } seconden` ); } })
Een laatste probleem dat ik vond was hoewel dit goed werkte op Mac, ik de volgende fout ondervond op Windows na ongeveer 10.000 validaties:
Fout: connect ENOBUFS XX.XX.XXX.XXX:443 – Lokaal (undefined:undefined) met e-mail XXXXXXX@XXXXXXXXXX.XXX
Na verder onderzoek blijkt het een probleem te zijn met de NodeJS HTTP-client verbinding die geen verbindingen hergebruikt. Ik vond dit Stackoverflow-artikel over het probleem, en na verder graven vond ik een goede standaardconfiguratie voor de axios-bibliotheek die dit probleem oploste. Ik weet nog steeds niet zeker waarom dit probleem alleen op Windows optreedt en niet op Mac.
Volgende Stappen
Voor iemand die op zoek is naar een eenvoudig snel programma dat een CSV bestand inlaadt, de ontvanger validatie API aanroept, en een CSV output, is dit programma voor jou.
Enkele uitbreidingen aan dit programma zouden het volgende zijn:
Bouw een front-end of makkelijker UI voor gebruik
Betere fout- en retry-afhandeling, want als de API om de een of andere reden een fout geeft, herhaalt het programma momenteel de oproep niet
Ik ben ook benieuwd of snellere resultaten bereikt kunnen worden met een andere taal zoals Golang of Erlang/Elixir.
Voel je vrij om me feedback of suggesties te geven voor het uitbreiden van dit project.