ساخت یک ربات جستجوی آپارتمان در کمتر از ۳۰ دقیقه — از صفر تا صد
جستجوی آپارتمان در اغلب موارد کاری خسته کننده است. در این راهنما شیوه ساخت یک ربات که در این مسیر به شما کمک میکند را آموزش میدهیم. بدین ترتیب دیگر مجبور نخواهید بود نتایج جستجوی خود را به طور مرتب رفرش کنید.
چارچوب
در شهر مونترال فصل آغاز کرایه آپارتمانها تاریخ مشخصی است و در ابتدای ماه جولای آغاز میشود و با طول 12 ماه در انتهای ژوئن سال بعد پایان میپذیرد. با این که ممکن است فکر کنید این وضعیت با افزایش دسترسیپذیری و انتظارات موجب سادهتر شدن امور میشود؛ اما در عمل باعث میشود که رقابت شدیدی پدید آید.
در این موارد باید در طی بازه محدودی هر روز صبح از خواب بیدار شوید و صفحه وبسایت موتور جستجوی املاکی مانند kijiji را باز کرده و ایمیلهایی در خصوص همه آگهیهای جدید ارسال کنید. همین فرایند در زمان ظهر و عصر و پیش از خواب نیز تکرار میشود. نرخ پاسخ دادن به ایمیلها بسیار پایین و شاید کمتر از 10 درصد است و پاسخهایی نیز که دریافت میشوند، در اغلب موارد نامطلوب هستند.
مرحله بعد این است که تلفن را بردارید و شانس خود را بیشتر امتحان کنید. صاحبان املاک پاسخدهی بیشتری دارند و این بار معمولاً 10 نفر پیش از فرد قرار دارند. در این موارد ما تلاش کردیم که برنامه کوچکی بنویسیم که جستجوهای Kijiji را برای مشاهده تغییرها زیر نظر بگیرد. زمانی که برنامه ما تغییرات را مشاهده میکند یک پیامک به گوشی ارسال میکند که حاوی اطلاعات مرتبط است. در ادامه این راهنما مراحل راهاندازی چنین برنامهای را توضیح میدهیم. اگر میخواهید سورس کد این برنامه را ببینید پیشنهاد میکنیم به این ریپوی گیتهاب (+) مراجعه کنید.
مقدمه
میدانیم که چنین برنامهای باید سبک باشد، چون قرار است به طور شبانهروزی یا دستکم تا زمانی که یک آپارتمان نیافته است کار کند. بدین ترتیب یک اسکریپت Node ساده مینویسیم که بتوانیم از ترمینال اجرا کنیم.
راهاندازی
با فرض این که شما npm و node را نصب کردهاید، اولین مرحله در هر پروژه کدنویسی مقداردهی اولیه npm درون دایرکتوری پروژه است. سپس باید یک دایرکتوری src بسازیم که کد ما درون آن قرار میگیرد. درون دایرکتوری src یک فایل index.js بسازید که اسکریپت از آن اجرا شود. این کار به صورت زیر ممکن است:
$ npm init // this will ask a few questions $ mkdir src $ cd src && touch index.js
نوشتن اسکریپت
زمانی که یک پروژه منفرد مینویسیم میتوانیم آزادانهتر عمل کنیم. در واقع به همان صورتی که فکر میکنیم کدنویسی خواهیم کرد. نخستین کاری که باید انجام دهیم، این است که مطمئن شویم درخواست موفقی به Kijiji ارائه میکنیم. برای اطمینان یافتن از این مسئله که میتوانیم پاسخ مناسبی از این وبسایت دریافت کنیم یک fetch کاملاً ابتدایی انجام میدهیم. به این منظور باید یک کتابخانه request نصب کنیم:
$ npm install request-promise
و سپس کدهای زیر را به فایل index.js اضافه کنیم:
const rp = require("request-promise"); const url = `https://www.kijiji.ca/b-appartement-condo-4-1-2/grand-montreal/plateau/k0c214l80002?price=__1400`; rp(url).then(response => console.log(response));
فایل را ذخیره کنید و آن را با دستور زیر اجرا نمایید:
$ node src/index.js
بدین ترتیب در کنسول مقداری کدهای HTML میبینید که نشان دهنده موفقیت درخواست است.
به دلیل این که تنها وقتی محتوای وبسایت تغییر مییابد برای ما مهم است، بنابراین یک هش (Hash) ساده از پاسخ دریافتی از وبسایت میسازیم. بدین ترتیب میتوانیم پاسخهای دریافتی را از طریق هشها مقایسه کنیم. در حالتی که نتایج خود را log بکنیم، این امر موجب میشود که دردسر کمتری نسبت به HTML خام داشته باشیم.
بدین منظور میتوانیم از ابزار هش کردن به نام checksum استفاده کنیم:
$ yarn add checksum
و سپس:
const rp = require("request-promise"); const checksum = require("checksum"); const url = `https://www.kijiji.ca/b-appartement-condo-4-1-2/grand-montreal/plateau/k0c214l80002?price=__1400`; // let's add the hashing in our response rp(url).then(response => console.log(checksum(response)));
میبینیم که این کد کار میکند. 1500 خط کد HTML به صورت یک رشته 32 رقمی درمیآید. اینک آن را در یک تابع قرار میدهیم تا قابلیت استفاده مجدد داشته باشد:
const rp = require("request-promise"); const checksum = require("checksum"); // instantiate an empty variable outside the function so we can save its value let hash = ""; function checkURL(siteToCheck) { return rp(siteToCheck).then(response => { if (hash === "") { hash = checksum(response); return; } return hash !== checksum(response); }); } const url = `https://www.kijiji.ca/b-appartement-condo-4-1-2/grand-montreal/plateau/k0c214l80002?price=__1400`; // check every 10 seconds // doing this asynchonously so our fetch is sure to resolve setInterval(async () => { console.log(await checkURl(url)); }, 10000);
کد فوق یک هش از مقادیر واکشی شده تهیه میکند. سپس در واکشی بعدی هش اولیه را با هش جدید مقایسه میکند. اگر این دو متفاوت باشند، مقدار true بازمیگرداند. میبینیم که این کد عالی عمل میکند. در واقع شاید بیش از حد عالی عمل میکند، چون در همه موارد مقدار true بازمیگرداند!
با کمی بررسی پاسخ واکشی شده میبینیم که Kijiji در هدر خود یک «مُهر زمانی» (timestamp) دارد. این بدان معنی است که هش در هر واکشی متفاوت خواهد بود. همچنین لازم به ذکر است که این امر میتواند به دلیل چرخش تبلیغها و دستهای از محتواهای دینامیک دیگر نیز رخ دهد.
نتیجه تأملات فوق این است که همواره هنگام بررسی پاسخ و هر زمان که با یک API سر و کار داریم که خود ننوشتهایم، باید مراقب باشیم.
بدین ترتیب باید به بخش اصلی کد وبسایت دسترسی پیدا کنیم و از این رو یک بسته شخص ثالث را نصب میکنیم تا به تحلیل پاسخ کمک کند. Cheerio (+) کتابخانهای است که میتواند کدهای نشانهگذاری HTML را تحلیل کرده و آن را به صورت یک API جاوا اسکریپت قابل دسترسی دربیاورد. هدف از ایجاد این کتابخانه کمک به توسعهدهندگان jQuery برای عدم استفاده از jQuery بوده است؛ اما در موارد دیگری نیز استفاده میشود.
هدف ما برای استفاده از این کتابخانه تقلید برخی ابزارهای بخش توسعهدهنده کروم (Chrome Developer Tools) است. بدین ترتیب به عنوان پیشنیاز برای استفاده از Cheerio باید بدانیم که در کد HTML خود به دنبال چه چیزی هستیم. بنابراین کروم را باز کنید و URL را وارد نمایید.
اگر آگهی را بررسی کنیم، میبینیم که همه پاسخهای جستجو دارای کلاسهای.search-item و.regular-ad هستند. ما میتوانیم آنها را با استفاده از Cheerio به صورت زیر انتخاب کنیم:
const rp = require("request-promise"); const checksum = require("checksum"); const co = require("cheerio"); // instantiate an empty variable outside the function so we can save its value let hash = ""; function checkURL(siteToCheck) { return rp(siteToCheck).then(response => { const $ = co.load(HTMLresponse); let apartmentString = ""; // use cheerio to parse HTML response and find all search results $(".search-item.regular-ad").each((i, element) => { console.log(element); }); }); } const url = `https://www.kijiji.ca/b-appartement-condo-4-1-2/grand-montreal/plateau/k0c214l80002?price=__1400`; // check every 10 seconds // doing this asynchonously so our fetch is sure to resolve setInterval(async () => { console.log(await checkURl(url)); }, 10000);
دقیقاً همان طور که انتظار داریم این کار یک آرایه از شیءهای سازمانیافته ایجاد میکند. بر اساس مستندات Cheerio همه خصوصیتهای یک عنصر در کلیدی به نام attribs جای گرفتهاند. اگر به بخش ابزار توسعهدهندگان گوگل کروم بازگردیم، میبینیم که یک خصوصیت دادهای یکتا به نام ID داریم. بدین ترتیب با هدفگیری این خصوصیت کد درون تابع checkURL خود را با کد زیر جایگزین میکنیم:
rp(siteToCheck).then(response => { const $ = co.load(HTMLresponse); let apartmentString = ""; // use cheerio to parse HTML response and find all search results $(".search-item.regular-ad").each((i, element) => { console.log(element.attribs["data-ad-id"]); }); });
بدین ترتیب فهرستی از اعداد ID یکتا به دست میآوریم. این ID ها تنها اطلاعاتی هستند که در مورد صفحه داریم.
بنابراین اگر به نقشه اولیه خود برای مقایسه هش ها بازگردیم، تنها کاری که باید بکنیم این است که این بار ID ها را هش میکنیم:
const rp = require("request-promise"); const checksum = require("checksum"); const co = require("cheerio"); // instantiate an empty variable outside the function so we can save its value let hash = ""; function checkURL(siteToCheck) { return rp(siteToCheck) .then(HTMLresponse => { const $ = co.load(HTMLresponse); let apartmentString = ""; // use cheerio to parse HTML response and find all search results // then find all apartmentlistingIDs and concatenate them $(".search-item.regular-ad").each((i, element) => { apartmentString += `${element.attribs["data-ad-id"]}`; }); return checksum(apartmentString); }) .catch(err => { console.log(`Could not complete fetch of ${url}: ${err}`); }); } const url = `https://www.kijiji.ca/b-appartement-condo-4-1-2/grand-montreal/plateau/k0c214l80002?price=__1400`; // check every 10 seconds // doing this asynchonously so our fetch is sure to resolve setInterval(async () => { console.log(await checkURl(url)); }, 10000);
این کد دقیقاً همان طور که انتظار داریم عمل میکند. وقتی فردی یک آگهی جدید ارسال میکند (و یا یک آگهی قدیمی را حذف میکند و ترتیب ID ها به هم میریزد) مقدار true در کنسول ما نمایش مییابد. تنها کاری که باقی مانده است این است که ابزار SMS را راهاندازی کنیم.
ارسال SMS از ترمینال
این کار از آن چه تصور میشود آسانتر است. به این منظور از یک نرمافزار شخص ثالث به نام Twilio استفاده میکنیم. این نرمافزار کارهای زیادی انجام میدهد؛ اما یکی از ویژگیهای اصلی آن ارسال پیامک است. همچنین یک API جاوا اسکریپت عالی دارد. برای تکمیل کردن این راهنما باید یک حساب در Twilio داشته باشید. برای امتحان کردن امکانات نرمافزار، یک حساب رایگان کاملاً کافی است و شاید حتی با همین حساب بتوانید یک آپارتمان بگیرید. در آغاز باید دستور زیر را اجرا کنید:
$ yarn add twilio
سپس در فایل index.js وابستگی Twilio را وارد کرده و تابع جدیدی به نام SMS مینویسیم:
const twilio = require(twilio); // you'll need to get your own credentials for this one const client = new Twilio("accountID", "authKey"); function SMS({ body, to, from }) { client.messages .create({ body, to, from }) .then(() => { console.log(`? Success! Message has been sent to ${to}`); }) .catch(err => { console.log(err); }); }
تابع ساده فوق دو شماره تلفن (to و from) و یک پیام (body) میگیرد. به جای ارائه گزارش در کنسول، نتیجه تابع checkURL با فراخوانی تابع SMS به صورت یک پیامک با متن ارسال خواهد شد:
require('dotenv/config') const rp = require("request-promise"); const checksum = require("checksum"); const co = require("cheerio"); // Instantiate Twilio - you'll need to get your own credentials for this one! const client = require('twilio')(your_accountSid, your_authToken); // instantiate an empty variable outside the function so we can save its value let hash = ""; const url = `https://www.kijiji.ca/b-appartement-condo-4-1-2/grand-montreal/plateau/k0c214l80002?price=__1400`; function checkURL(siteToCheck) { return rp(siteToCheck) .then(HTMLresponse => { const $ = co.load(HTMLresponse); let apartmentString = ""; // use cheerio to parse HTML response and find all search results // then find all apartmentlistingIDs and concatenate them $(".search-item.regular-ad").each((i, element) => { apartmentString += `${element.attribs["data-ad-id"]}`; }); if (hash === '') { console.log('Making initial fetch...') hash = checksum(apartmentString); } // When the hashes are not equal, there is a change in ad ID's if (checksum(apartmentString) !== hash) { hash = checksum(apartmentString) return true; } console.log('No change to report!') return false; }) .catch(err => { console.log(`Could not complete fetch of ${url}: ${err}`); }); } function SMS({ body, to, from }) { client.messages .create({ body, to, from }) .then(() => { console.log(`? Success! Message has been sent to ${to}`); }) .catch(err => { console.log(err); }); } // check every 10 seconds // doing this asynchonously so our fetch is sure to resolve setInterval(async () => { // if checkURL returns true, send a message! if (await checkURL(url)) { console.log('Found a change! Sending SMS...') // These are obviously fake phone numbers, replace the `to` with whatever number you want to message // and the from with the number from your Twilio account! SMS({ body: `There is a new add at ${url}!`, to: "+555555555", from: "+4444444444" }); } }, 10000);
بدین ترتیب هر زمان که اسکریپت ما مورد جدیدی را مشاهده کند، یعنی متوجه شود که بین هشهای متوالی سایت تغییری رخ داده است، یک پیام متنی با URL مربوطه به گوشی تلفن همراه ما ارسال خواهد کرد.
سخن پایانی
اسکریپت واقعی که ایجاد کردیم کمی پیچیدهتر از آن است که در اینجا ارائه شده و میتوانید آن را در این ریپوی گیتهاب (+) مشاهده کنید.
همچنین میتوان برخی بهبودها در آن ایجاد کرد، مثلاً آن را عمومیتر ساخت تا صرفاً به برسی یک وبسایت خاص نپردازد. از طرف دیگر این اسکریپت کاملاً ابتدایی است و میتواند به یک پروژه تماموقت برای توسعهدهندگان جدید تبدیل شود. اگر به توسعه آن علاقهمند هستید میتوانید همین حالا اقدام کنید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش جاوا اسکریپت (JavaScript)
- مجموعه آموزشهای مهندسی نرم افزار
- Node.js و وب هوک های گیت هاب — راهنمای به روز رسانی پروژه ها از راه دور
- 12 فریمورک وب که توسعه دهندگان باید در سال 2۰1۸ بیاموزند
- چگونه برنامه نویس وب شویم؟ — بخش دوم: بکاند (Backend)
==