
بالنسبة للشخص الذي يبحث عن برنامج بسيط وسريع يقبل ملف بصيغة CSV، ويستدعي واجهة برمجة تطبيقات التحقق من المستلم، ويُخرج ملف CSV، فهذا البرنامج مناسب لك.
Business in a box.
اكتشف حلولنا.
تحدث إلى فريق المبيعات لدينا
أحد الأسئلة التي نتلقاها أحياناً هو، كيف يمكنني التحقق من قوائم البريد الإلكتروني بشكل كبير باستخدام التحقق من صحة المستلم? هناك خياران هنا، أحدهما هو تحميل ملف من خلال واجهة مستخدم SparkPost للتحقق، والآخر هو إجراء مكالمات فردية لكل بريد إلكتروني إلى API (حيث أن API هو للتحقق من صحة البريد الإلكتروني الفردي).
الخيار الأول يعمل بشكل رائع ولكنه لديه حد يبلغ 20 ميغا بايت (حوالي 500,000 عنوان). ماذا لو كان لدى شخص ما قائمة بريد إلكتروني تحتوي على ملايين العناوين؟ قد يعني ذلك تقسيمها إلى تحميل 1,000 من ملفات CSV.
نظراً لأن تحميل الآلاف من ملفات CSV يبدو مبالغاً فيه قليلاً، أخذت هذه الحالة وبديت أتساءل عن مدى سرعة جلبي للـ API للعمل. في هذه المقالة سأشرح ما حاولت وكيف وصلت في النهاية إلى برنامج يمكنه عمل حوالي 100,000 عملية تحقق في 55 ثانية (في حين أنني حصلت على حوالي 100,000 عملية تحقق في واجهة المستخدم في دقيقة واحدة و10 ثواني). ورغم أن هذا سيستغرق حوالي 100 ساعة لإنجاز حوالي 654 مليون عملية تحقق، إلا أن هذه السكربت يمكن أن تعمل في الخلفية وتوفر وقتاً كبيراً.
يمكن العثور على النسخة النهائية من هذا البرنامج هنا.
أول خطأ لي: استخدام Python
بايثون هي واحدة من لغات البرمجة المفضلة لدي. إنها تتفوق في العديد من المجالات وهي غاية في البساطة. ومع ذلك، فإن أحد المجالات التي لا تتفوق فيها هو العمليات المتزامنة. بينما تحتوي بايثون على القدرة على تشغيل الوظائف غير المتزامنة، فإنها تمتلك ما يعرف بقفل المترجم العالمي لبايثون أو GIL.
“القفل المترجم العالمي لبايثون أو GIL، بكلمات بسيطة، هو قفل (mutex) يسمح فقط لخيط واحد بالتحكم في مترجم بايثون.
هذا يعني أن خيطًا واحدًا فقط يمكن أن يكون في حالة تنفيذ في أي وقت. تأثير GIL لا يُلاحظ لدى المطورين الذين ينفذون برامج أحادية الخيط، ولكنه قد يكون عنق زجاجة في الأداء في الشيفرات المرتبطة بوحدة المعالجة المركزية والمتعددة الخيوط.
منذ أن GIL يسمح فقط لخيط واحد بالتنفيذ في وقت واحد حتى في هيكل متعدد الخيوط مع أكثر من نواة وحدة معالجة مركزية واحدة، فقد اكتسبت 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 بسبب قدرته على تنفيذ عمليات إدخال/إخراج غير محظورة بشكل جيد للغاية. أنا أيضاً على دراية جيدة بالبرمجة في 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 أعطى خطأً، فإن البرنامج حالياً لا يعيد المحاولة لتوجيه النداء
أود أيضًا أن أرى ما إذا كان يمكن تحقيق نتائج أسرع باستخدام لغة أخرى مثل Golang أو Erlang/Elixir.
يرجى عدم التردد في تزويدي بأي ملاحظات أو اقتراحات لتوسيع هذا المشروع.