
Para alguien que busca un programa simple y rápido que acepte un csv, llame a la API de validación de receptores y produzca un CSV, este programa es para ti.
Business in a box.
Descubre nuestras soluciones.
Habla con nuestro equipo de ventas
Una de las preguntas que ocasionalmente recibimos es, ¿cómo puedo validar en masa listas de correos electrónicos con validación de destinatarios? Hay dos opciones aquí, una es cargar un archivo a través de la interfaz de usuario de SparkPost para validación, y la otra es realizar llamadas individuales por correo electrónico a la API (ya que la API es para validación de correos electrónicos individuales).
La primera opción funciona muy bien pero tiene una limitación de 20Mb (aproximadamente 500,000 direcciones). ¿Qué pasa si alguien tiene una lista de correos electrónicos que contiene millones de direcciones? Podría significar dividir eso en miles de cargas de archivos CSV.
Dado que cargar miles de archivos CSV parece un poco improbable, tomé ese caso de uso y comencé a preguntarme qué tan rápido podría hacer que la API funcionara. En esta publicación de blog, explicaré lo que intenté y cómo finalmente llegué a un programa que podría realizar alrededor de 100,000 validaciones en 55 segundos (mientras que en la interfaz de usuario obtuve alrededor de 100,000 validaciones en 1 minuto y 10 segundos). Y aunque esto todavía tomaría alrededor de 100 horas completar aproximadamente 654 millones de validaciones, este script puede ejecutarse en segundo plano ahorrando tiempo significativo.
La versión final de este programa se puede encontrar aquí.
Mi primer error: usar Python
Python es uno de mis lenguajes de programación favoritos. Sobresale en muchas áreas y es increíblemente sencillo. Sin embargo, un área en la que no sobresale es en los procesos concurrentes. Aunque Python tiene la capacidad de ejecutar funciones asincrónicas, tiene lo que se conoce como The Python Global Interpreter Lock o GIL.
“The Python Global Interpreter Lock o GIL, en palabras simples, es un mutex (o un bloqueo) que permite que solo un hilo tenga el control del intérprete de Python.
Esto significa que solo un hilo puede estar en un estado de ejecución en cualquier momento determinado. El impacto del GIL no es visible para los desarrolladores que ejecutan programas de un solo hilo, pero puede ser un cuello de botella de rendimiento en código dependiente de la CPU y multihilo.
Ya que el GIL permite que sólo un hilo se ejecute a la vez incluso en una arquitectura multihilo con más de un núcleo de CPU, el GIL ha ganado una reputación como una característica “infame” de Python.” (https://realpython.com/python-gil/)”
Al principio, no estaba al tanto del GIL, así que comencé a programar en Python. Al terminar, aunque mi programa era asincrónico, se estaba bloqueando, y no importaba cuántos hilos agregara, todavía solo obtenía alrededor de 12-15 iteraciones por segundo.
La porción principal de la función asincrónica en Python se puede ver a continuación:
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)
Así que dejé de usar Python y volví a la mesa de dibujo…
Me decidí por utilizar NodeJS debido a su capacidad para realizar operaciones de entrada/salida no bloqueantes extremadamente bien. También estoy bastante familiarizado con la programación en NodeJS.
Utilizando aspectos asincrónicos de NodeJS, esto terminó funcionando bien. Para más detalles sobre la programación asincrónica en NodeJS, ve https://blog.risingstack.com/node-hero-async-programming-in-node-js/
Mi segundo error: intentar leer el archivo en memoria
Desglosando el código final
Después de leer y validar los argumentos del terminal, ejecuto el siguiente código. Primero, leo el archivo CSV de correos electrónicos y cuento cada línea. Hay dos propósitos de esta función, 1) me permite informar con precisión sobre el progreso del archivo [como veremos más adelante], y 2) me permite detener un temporizador cuando el número de correos electrónicos en el archivo es igual al de validaciones completadas. Agregué un temporizador para poder ejecutar benchmarks y asegurarme de obtener buenos resultados.
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 () { //Al final del infile, después de que todas las líneas han sido contadas, ejecuta la función de validación de destinatarios validateRecipients.validateRecipients(count, myArgs); });
Luego llamo a la función validateRecipients. Note que esta función es asíncrona. Después de validar que tanto el infile como el outfile son CSV, escribo una fila de encabezado y comienzo un temporizador de programa usando la biblioteca JSDOM.
async function validateRecipients(email_count, myArgs) { if ( //Si tanto el infile como el outfile están en formato .csv extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; //Contador para cada llamada API email_count++; //El contador de líneas devuelve #lines - 1, esto se hace para corregir el número de líneas //Inicia un temporizador 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" ); //Escribir los encabezados en el outfile
El siguiente script es realmente el núcleo del programa, así que lo dividiré y explicaré qué está pasando. Por cada línea del infile:
Tomar asíncronicamente esa línea y llamar al API de validación de destinatarios.
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, }, }) //Para cada fila leída del infile, llamar al SparkPost Recipient Validation API
Luego, en la respuesta
Agregar el correo electrónico al JSON (para poder imprimir el correo electrónico en el CSV)
Validar si reason es nulo, y si es así, rellenar con un valor vacío (esto es para que el formato del CSV sea consistente, ya que en algunos casos se proporciona reason en la respuesta)
Establecer las opciones y claves para el módulo json2csv.
Convertir el JSON a CSV y sacar el resultado (utilizando json2csv)
Escribir el progreso en el terminal
Finalmente, si el número de correos electrónicos en el archivo = validaciones completadas, detener el temporizador e imprimir los resultados
.then(function (response) { response.data.results.email = String(email); //Agrega el correo electrónico como par valor/clave al JSON de respuesta para ser utilizado en la salida response.data.results.reason ? null : (response.data.results.reason = ""); //Si reason es nulo, configurarlo en blanco para que el CSV sea uniforme //Utiliza json-2-csv para convertir el JSON a formato CSV y sacar el resultado let options = { prependHeader: false, //Desactiva que los valores JSON sean añadidos como filas de encabezado para cada línea keys: [ "results.email", "results.valid", "results.result", "results.reason", "results.is_role", "results.is_disposable", "results.is_free", "results.delivery_confidence", ], //Establece el orden de las claves }; let json2csvCallback = function (err, csv) { if (err) throw err; output.write(`${csv}\n`); }; converter.json2csv(response.data, json2csvCallback, options); completed++; //Incrementar el contador de API process.stdout.write(`Done with ${completed} / ${email_count}\r`); //Sacar estado de Completado / Total al console sin mostrar nuevas líneas //Si todos los correos fueron validados completed == email_count) { const stop = window.performance.now(); //Detener el temporizador console.log( `Todos los correos electrónicos validados con éxito en ${ (stop - start) / 1000 } segundos` ); } })
Un problema final que encontré fue que, aunque esto funcionó muy bien en Mac, encontré el siguiente error usando Windows después de alrededor de 10,000 validaciones:
Error: connect ENOBUFS XX.XX.XXX.XXX:443 – Local (undefined:undefined) con el correo electrónico XXXXXXX@XXXXXXXXXX.XXX
Después de investigar más, parece ser un problema con el pool de conexiones del cliente HTTP de NodeJS que no reutiliza las conexiones. Encontré este artículo de Stackoverflow sobre el problema, y después de investigar más, encontré una buena configuración predeterminada para la biblioteca axios que resolvió este problema. Aún no estoy seguro por qué este problema solo ocurre en Windows y no en Mac.
Siguientes pasos
Para alguien que esté buscando un programa sencillo y rápido que acepte un CSV, llame a la API de validación de destinatarios y genere un CSV, este programa es para ti.
Algunas adiciones a este programa serían las siguientes:
Construir un front end o una interfaz de usuario más sencilla
Mejor manejo de errores y reintentos porque si por alguna razón la API produce un error, el programa actualmente no reintenta la llamada
También me interesaría ver si se podrían lograr resultados más rápidos con otro lenguaje, como Golang o Erlang/Elixir.
Por favor, siéntete libre de proporcionarme cualquier comentario o sugerencia para expandir este proyecto.