
بالنسبة للشخص الذي يبحث عن برنامج بسيط وسريع يقبل ملف بصيغة CSV، ويستدعي واجهة برمجة تطبيقات التحقق من المستلم، ويُخرج ملف CSV، فهذا البرنامج مناسب لك.
عند إنشاء تطبيقات البريد الإلكتروني، يحتاج المطورون في كثير من الأحيان إلى دمج خدمات وواجهات برمجة تطبيقات متعددة. فهم أساسيات واجهة برمجة التطبيقات للبريد الإلكتروني في بنية السحابة يوفر الأساس لبناء أدوات قوية مثل نظام التحقق الضخم الذي سنقوم بإنشائه في هذا الدليل.
أحد الأسئلة التي نتلقاها من حين لآخر هو، كيف يمكنني التحقق من قوائم البريد الإلكتروني بشكل جماعي باستخدام التحقق من المستلم? هناك خياران هنا، الأول هو تحميل ملف من خلال واجهة مستخدم SparkPost للتحقق، والآخر هو إجراء مكالمات فردية لكل بريد إلكتروني إلى واجهة برمجة التطبيقات (حيث إن واجهة برمجة التطبيقات تقوم بالتحقق من بريد إلكتروني واحد فقط).
يعمل الخيار الأول بشكل رائع ولكنه يقتصر عند حد 20 ميغابايت (حوالي 500,000 عنوان). ماذا لو كان لدى شخص ما قائمة بريد إلكتروني تحتوي على ملايين العناوين؟ قد يعني ذلك تقسيمه إلى 1,000 تحميل لملفات CSV.
نظراً لأن تحميل آلاف ملفات CSV يبدو شيئاً ليس عملياً، فقد أخذت هذا السيناريو وبدأت أتساءل عن مدى سرعة تشغيل واجهة برمجة التطبيقات. في هذه التدوينة، سأشرح ما جربته وكيف توصلت في النهاية إلى برنامج يمكنه القيام بحوالي 100,000 عملية تحقق في 55 ثانية (بينما في واجهة المستخدم حصلت على حوالي 100,000 عملية تحقق في 1 دقيقة و10 ثواني). وبينما يستغرق هذا حوالي 100 ساعة لإتمام حوالي 654 مليون عملية تحقق، يمكن لهذا البرنامج أن يعمل في الخلفية موفراً وقتاً كبيراً.
يمكن العثور على النسخة النهائية من هذا البرنامج هنا.
أول خطأ لي: استخدام Python
البايثون هي واحدة من لغات البرمجة المفضلة لدي. تبرز في العديد من المجالات وتعتبر سهلة جداً. ومع ذلك، أحد المجالات التي لا تبرز فيها هو العمليات المتزامنة. بينما البايثون لديها القدرة على تشغيل الوظائف غير المتزامنة، لديها ما يعرف بقفل المفسر العالمي للبايثون أو GIL.
"قفل المفسر العالمي للبايثون أو GIL، ببساطة، هو عبارة عن مغلاق يسمح فقط لخط واحد بالتحكم في مفسر بايثون.
هذا يعني أن خطًا واحدًا فقط يمكن أن يكون في حالة تنفيذ في أي وقت. تأثير GIL غير مرئي للمطورين الذين ينفذون برامج ذات خيط واحد، لكنه يمكن أن يكون عنق الزجاجة للأداء في الأكواد التي تعتمد على المعالج المتعدد الخيوط.
نظرًا لأن GIL يسمح فقط لخط واحد بالتنفيذ في أي وقت حتى في بنية متعددة الخيوط مع أكثر من نواة CPU واحدة، فقد اكتسب GIL سمعة باعتباره ميزة "سيئة السمعة" للبايثون." (https://realpython.com/python-gil/)
في البداية، لم أكن على علم بـ GIL، لذلك بدأت البرمجة في بايثون. في النهاية، على الرغم من أن برنامجي كان غير متزامن، إلا أنه كان محبوسًا، وبغض النظر عن عدد الخيوط التي أضفتها، حصلت فقط على حوالي 12-15 تكرارًا في الثانية.
يمكن رؤية الجزء الرئيسي من الوظيفة غير المتزامنة في البايثون أدناه:
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)
لذلك توقفت عن استخدام البايثون وعدت للتخطيط من جديد...
قررت استخدام NodeJS نظرًا لقدرتها على أداء عمليات الإدخال/الإخراج غير المتزامنة بشكل جيد جداً. خيار آخر ممتاز لمعالجة API غير المتزامنة هو بناء مستهلكات webhook خالية من الخوادم باستخدام وظائف Azure، والتي يمكن أن تتعامل بكفاءة مع أحمال العمل المتغيرة. أنا أيضًا معتاد إلى حد ما على البرمجة في NodeJS.
باستخدام جوانب غير متزامنة من NodeJS، انتهى الأمر بالعمل بشكل جيد. للحصول على مزيد من التفاصيل حول البرمجة غير المتزامنة في NodeJS، راجع https://blog.risingstack.com/node-hero-async-programming-in-node-js/
خطأي الثاني: محاولة قراءة الملف في الذاكرة
تحليل الكود النهائي
بعد قراءة وفحص معطيات الجهاز الطرفي، أقوم بتشغيل الكود التالي. أولاً، أقوم بقراءة ملف CSV من الرسائل الإلكترونية وعد كل سطر. هناك غرضان لهذه الوظيفة: 1) يمكنني من خلالها الإبلاغ بدقة عن تقدم الملف [كما سنرى لاحقًا]، و2) يمكنني التوقف عن المؤقت عندما يساوي عدد الرسائل الإلكترونية في الملف عدد الفحوصات المكتملة. أضفت مؤقتًا حتى أتمكن من تشغيل اختبارات الأداء وضمان الحصول على نتائج جيدة.
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); });
ثم أدعو وظيفة validateRecipients. لاحظ أن هذه الوظيفة غير متزامنة. بعد التأكد من أن infile وoutfile هما بتنسيق CSV، أكتب صفًا للرأس وأبدأ مؤقت البرنامج باستخدام مكتبة JSDOM.
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
النص البرمجي التالي هو الجزء الأكبر من البرنامج لذلك سأقوم بتقسيمه وشرح ما يحدث. لكل سطر من infile:
قم بنقل هذا السطر بشكل غير متزامن واستدعاء واجهة برمجة التطبيقات للتحقق من المستلمين.
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
ثم، على الرد
أضف البريد الإلكتروني إلى JSON (لتمكين طباعة البريد الإلكتروني في CSV)
تحقق مما إذا كان السبب فارغًا، وإذا كان كذلك، املأ بقيمة فارغة (وذلك لضمان تنسيق CSV بشكل متسق، حيث يتم تقديم السبب في بعض الحالات في الرد)
حدد الخيارات والمفاتيح للوحدة json2csv.
قم بتحويل JSON إلى CSV وأخرج (باستخدام json2csv)
اكتب التقدم في الجهاز الطرفي
أخيرًا، إذا كان عدد الرسائل الإلكترونية في الملف = الفحوصات المكتملة، أوقف المؤقت واطبع النتائج
.then(function (response) { response.data.results.email = String(email); //Adds the email as a value/key pair to the response JSON to be used for output response.data.results.reason ? null : (response.data.results.reason = ""); //If reason is null, set it to blank so the CSV is uniform //Utilizes json-2-csv to convert the JSON to CSV format and output let options = { prependHeader: false, //Disables JSON values from being added as header rows for every line keys: [ "results.email", "results.valid", "results.result", "results.reason", "results.is_role", "results.is_disposable", "results.is_free", "results.delivery_confidence", ], //Sets the order of keys }; let json2csvCallback = function (err, csv) { if (err) throw err; output.write(`${csv}\n`); }; converter.json2csv(response.data, json2csvCallback, options); completed++; //Increase the API counter process.stdout.write(`Done with ${completed} / ${email_count}\r`); //Output status of Completed / Total to the console without showing new lines //If all emails have completed validation if (completed == email_count) { const stop = window.performance.now(); //Stop the timer console.log( `All emails successfully validated in ${ (stop - start) / 1000 } seconds` ); } })
كان هناك مشكلة نهائية واجهتها وهي أثناء أن هذا يعمل بشكل رائع على Mac، واجهت الخطأ التالي عند استخدام Windows بعد حوالي 10,000 تحقق:
خطأ: connect ENOBUFS XX.XX.XXX.XXX:443 – Local (undefined:undefined) مع البريد الإلكتروني XXXXXXX@XXXXXXXXXX.XXX
بعد إجراء بعض الأبحاث الإضافية، يبدو أنه مشكلة مع مجموعة اتصال العميل HTTP الخاصة بـ NodeJS التي لا تعيد استخدام الاتصالات. وجدت مقالة على Stackoverflow حول المشكلة، وبعد المزيد من البحث، وجدت إعداد افتراضي جيد لمكتبة axios الذي حل هذه المشكلة. لا أزال غير متأكد من سبب حدوث هذه المشكلة فقط على Windows وليس على Mac.
الخطوات التالية
لشخص يبحث عن برنامج بسيط وسريع يستقبل ملف CSV، ويستدعي API التحقق من المستلم، ويُصدر ملف CSV، هذا البرنامج مناسب لك.
بعض الإضافات لهذا البرنامج ستكون كما يلي:
إنشاء واجهة أمامية أو واجهة مستخدم أسهل للاستخدام
تحسين معالجة الأخطاء والمحاولات لأن البرنامج حالياً لا يعيد المحاولة إذا حدث خطأ في استدعاء API لسبب ما
اعتبر التنفيذ كـ وظيفة Azure بدون خادم لتحقيق التوسع التلقائي وتقليل إدارة البنية التحتية
سأكون فضولياً أيضًا لمعرفة ما إذا كان يمكن تحقيق نتائج أسرع باستخدام لغة أخرى مثل Golang أو Erlang/Elixir. إلى جانب اختيار اللغة، يمكن أن تؤثر قيود البنية التحتية أيضًا على الأداء - لقد تعلمنا ذلك بأنفسنا عندما واجهنا قيود DNS غير موثقة في AWS التي أثرت على أنظمة معالجة البريد الإلكتروني ذات الحجم الكبير لدينا.
للمطورين المهتمين بدمج معالجة API مع أدوات سير العمل البصرية، اطلع على كيفية دمج Flow Builder مع Google Cloud Functions لأتمتة سير العمل بدون كود.
يرجى عدم التردد في تقديم أي ملاحظات أو اقتراحات لتوسيع هذا المشروع.