یه آدم معمولی
چت بات کرونا در WhatsApp. آموزش ساخت بات واتس اپ با پایتون
مشغول کار روی پایان نامم بود که متاسفانه ویروس کرونا تو ایران همه گیر شد. تو این اوضاع و شرایط احساس کردم که به عنوان یک وظیفه شهروندی باید یک سیستمی رو ایجاد کنم که مردم بهتر با خطرات این اپیدمی آشنا بشن.
سایت ها و اپلیکشن های زیادی در این مورد ساخته شده و در حال استفاده است که انصافا برنامه های خیلی خوبی هم هستند. ولی چون اکثر کسایی که گوشی های هوشمند دارن از پیام رسانهایی مثل واتس آپ و تلگرام استفاده میکنند به نظر من یک بات روی این پیام رسانها برای اطلاع رسانی و اگاهی و اموزش کاربرد بیشتری میتونه داشته باشه. نمونه کامل و پیاده سازی و اجرا شده این بات از اینجا یا ارسال سلام به شماره 09160459678 قابل دسترسی است.
ساخت بات برای تلگرام راحت و کتابخونه های زیادی در انواع زبانها براش وجود داره. ولی مشکلی که داره اینه که تو ایران فیلتر و شاید خیلی ها نتونن بهش دسترسی داشته باشن ولی خب واتس اپ تقریبا روی اکثر گوشی ها نصب و در حال استفاده است. پس تصمیم گرفت که برای واتس اپ درست کنم.
متاسفانه برای واتس اپ api که در دسترس عموم باشه وجود نداره. توی گیتهاب یکسری پروژه هست که از آخرین اپدیتشون خیلی وقت میگذره و از کار افتادن و قابل استفاده نیستن.پس تصمیم گرفتم خودم یکی درست کنم. و چون اموزشی در سطح نت ندیدم و سوال میپرسیدن که چطور بات رو درست کردی، به نظرم رسید که این اموزش رو اینجا قرار بدم. امیدوارم که مفید واقع بشه.
نحوه کار این بات به این صورت که منتظر میمونه تا پیام جدیدی برسه و بعد از دریافت پیام پاسخ متناسب با اون پیام رو ارسال میکنه. همونطور که میبینید یک روال خیلی ساده است.
برای پیاده سازیش ما از نسخه تحت وب واتس اپ و برای کنترلش از کتابخونه selenium استفاده میکنیم. پس قبل از شروع کار سلنیوم رو نصب میکنیم.سلنیوم یک کتابخونه برای اتوماتیک کردن کارها در مرورگرهاست. پس در ابتدای کار کتابخونه سلنیوم رو با دستور زیر نصب میکنم:
pip install -U selenium
در مرحله بعد باید تصمیم بگیریم که میخواییم از چه مرورگری استفاده کنیم. اگر از کروم استفاده میکنید باید اخرین نسخه chrome driver رو از اینجا و اگر از فایرفاکس استفاده میکند باید اخرین نسخه Geckodriver رو از اینجا دانلود کنید. برای مرورگرهای ادج و سافاری هم هست که میتونید لینکشون از اینجا پیدا کنید. من تو این پروژه از فایرفاکس استفاده کردم ولی روال کار با مرورگرهای دیگه تفاوت آنچنانی نداره.
from selenium import webdriver
driver=webdriver.Firefox(executable_path=dir_path+'/geckodriver')
driver.get("https://web.whatsapp.com")
کد بالا باعث اجرای مرروگر فایرفاکس و بارگزاری آدرس https://web.whatsapp.com میشود.خط دوم باعث اجرای فایرفاکس میشه. ورودی این تابع، آدرس geckodriver که در بالا دانلود کردیم رو وارد میکنیم. در خط سوم هم سایت https://web.whatsapp.com در مرورگر باز میشه. بعد از اجرای این ۳ خط چیزی که مشاهده میشه تصویر زیر:
در ادامه با اسکن کد بالا با گوشی وارد محیط چت میشیم. توی محیط چت باید منتظر بمونیم تا یک پیامی ارسال بشه و بعد از اینکه پیامی اومد باید پاسخ متناسب با اون پیام ارسال بشه. حالا چطور تشخیص بدیم که پیامی اومده. به تصویر زیر دقت کنید.
همانطور که مشاهده میکنید زمانی که پیام جدیدی میرسه یک دایره سبز رنگ کنار نام مخاطب ایجاد میشه. به بیان دقیق تر یک المان جدید به صفحه اظافه میشه و بعد از خواندن پیام این دایره پاک میشه. پس ما باید این المان رو شناسایی کنیم. حالا چطور این کار کنیم. برای این کار ابتدا در مرورگر فایرفاکس رو دایره سبز رنگ کلیک راست و در ادامه روی Inspect element کلیک میکنیم و وارد محیطی شبیه به محیط زیر میشویم.
در اینجا برای اینکه بتونیم دایره سبز رنگ رو پیدا کنیم نیاز به نام کلاس داریم که در اینجا "_15G96" است. حالا باید کدش رو پیاده سازی کنیم.
from selenium.webdriver.support.ui import WebDriverWait
wait = WebDriverWait(driver,3600)
wait.until(ec.presence_of_element_located((By.CLASS_NAME,"_15G96")))
newMessages=driver.find_elements_by_class_name("_15G96")
در کد بالا خط سوم به این معنی که تا زمانی که المانی با کلاس _15G96 در ساختار DOM سایت ظاهر نشده به خط بعدی نرو. مدت زمانی هم که میتونه منتظر بمونه ۳۶۰۰ ثانیه است که در خط قبلش مشخص شده و اگه از این مقدار بگذره خطا میده که باید به نحوی مدیریت بشه. بعد از اینکه این کلاس در ساختار سایت ظاهر شد وارد خط چهارم میشه و همه اونها رو در متغیر newMessages ذخیره میکنه.
خب تا اینجای کار ما تونستیم کدی بنویسیم که میتونه تشخیص بده پیام جدیدی رسیده یا نه . حالا وقتش که این پیام رو تحلیل کنیم و پاسخ مناسب رو ارسال کنیم.
for newMessage in newMessages:
....parent=newMessage.find_element_by_xpath("../../..")
....messageText=parent.find_element_by_class_name('_1wjpf').text
بالاتر گفتیم که متغیر newMessages حاوی پیامهای جدیدی که رسیده. حالا با استفاده از یک حلقه، یکی یکی پیامهای جدید رو میخونیم. ایده ای که برای خوندن پیامها هست اینه که اصلا نیازی نیست وارد محیط چت بشیم و فقط کافیه که متنی که زیر شماره مخاطب نوشته شده رو بخونیم. مانند پیام سلام در شکل زیر:
به محل قرارگیری دایره سبز رنگ در ساختار DOM سایت توجه کنید چیزی شبیه به ساختار زیر رو مشاهده میکنید
حالا ما بخواهیم از طریق المانی با نام کلاس _15G96به متن دست پیدا کنیم ابتدا نیاز که جَدِ این المان که کلاس _1AwDx هست رو پیدا کنیم. خط دوم از کد بالا این کار رو انجام میده در واقع برای پیدا کردن پدر یک المان از .. استفاده میکنیم. در خط سوم هم محتوا متنی کلاس _1wjpf رو که همون پیام جدید هست رو بدست میاریم. تا اینجای کار ما تونستیم پیام جدید که رسیده رو شناسایی و بخونیم . حالا باید تصمیم بگیریم که با این پیام جدید که رسیده چکار کنیم.
ساده ترین کار تعریف یک دیکشنری است که کلیدهاش پیام رسیده و مقدارش جواب پیام ها باشه. چیزی شبیه به مثال زیر:
responses={
....'hello':'hi',
....'name':'morteza',
....'goodbye': 'by'
}
در کد بالا یک دیکشنری تعریف کردیم که مشخص کنیم متناسب با هر پیام چه جوابی رو ارسال کنیم. حالا میخواییم جواب ارسال کنیم. از کدی شبیه به کد زیر استفاده میکنیم.
if messageText in responses.keys():
....response=responses[messageText]
....parent.click()
....wait.until(ec.element_to_be_clickable((By.XPATH,'//div[@spellcheck="true"]')))
....txtBox=driver.find_element_by_xpath('//div[@spellcheck="true"]')
....txtBox.click()
تو کد بالا ما هنوز جواب رو ارسال نکردیم و فقط تا تا مرحله شبیه سازی کلیک کردن رو نام مخاطبی که پیام رو ارسال کرده پیش رفتیم. در ادامه در خط۴ بعد از کلیک کردن منتظر میمونیم تا باکس وارد کردن متن پیام بالا بیاد تا همه چی برای وارد کردن متن پاسخ و ارسال اون اماده بشه. کد بالا ک اجرا بشه چیزی شبیه به تصویر زیر داریم که باکس پیام هم منتظر وارد کردن متن است.
حالا باید پیام رو وارد باکس پیام کنیم. میتونیم از کد زیر استفاده کنیم
txtBox.send_keys(response)
اما این کد دو تا ایراد داره. اولی اینکه این کد دقیقه تایپ کردن رو شبیه سازی میکنه و اگه متن طولانی باشه زمان میبره. دومین ایرادش اینه اگه در متن ارسالی \n داشته باشیم معادل با اینتر در نظر میگیره و یک پیام چند خطی رو در چند پیام یک خطی ارسال میکنه. حالا باید چکار کنیم که اینطوری نشه. ایده من کپی کردن متن در کلیپ بورد و پیست کردنش توی باکس متن بود. به این صورت که ابتدا pyperclip رو با دستور زیر نصب میکنیم. و کد ما به شکل زیر میشود
pip install pyperclip
import pyperclip
pyperclip.copy(response)
txtBox.send_keys(Keys.CONTROL+'V')
txtBox.send_keys(Keys.ENTER)
در خط اول متن رو در کلیپبرد ذخیره در ادامه پیست و در انتها دکمه اینتر رو میفرستیم تا پیام ارسال بشه. کدی در انتها به بدست میاد چیزی شبیه به کد زیر است:
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
import pyperclip
driver=webdriver.Firefox(executable_path=dir_path+'/geckodriver')
driver.get("https://web.whatsapp.com")
wait = WebDriverWait(driver,3600)
wait.until(ec.presence_of_element_located((By.CLASS_NAME,"_15G96")))
newMessages=driver.find_elements_by_class_name("_15G96")
responses={
....'hello':'hi',
....'name':'morteza',
....'goodbye': 'by'
}
for newMessage in newMessages:
....parent=newMessage.find_element_by_xpath("../../..")
....messageText=parent.find_element_by_class_name('_1wjpf').text
....if messageText in responses.keys():
........response=responses[messageText]
........parent.click()
........wait.until(ec.element_to_be_clickable((By.XPATH,'//div[@spellcheck="true"]')))
........txtBox=driver.find_element_by_xpath('//div[@spellcheck="true"]')
........txtBox.click()
........pyperclip.copy(response)
........txtBox.send_keys(Keys.CONTROL+'V')
........txtBox.send_keys(Keys.ENTER)
امکان دیگه ای که بات داره ارسال عکس که دقیقا شبیه به کد بالاست با این تفاوت که به جای کپی متن در کلیپبرد، عکس رو کپی میکنه. برای این کار در لینوکس از برنامه xcopy استفاده میکنیم. کدی که برای ارسال عکس بکار بردیم چیزی شبیه به کد زیر است
import subprocess
subprocess.Popen(['xclip', '-selection', 'clipboard', '-t', 'image/png', '-i', imagePath)
txtBox.send_keys(Keys.CONTROL+'V')
wait.until(ec.element_to_be_clickable((By.CLASS_NAME,'_2gZno')))
driver.find_element_by_class_name('_3hV1n').click()
مورد دیگه چگونگی ساخت تصاویر زیر است
به این گونه تصاویر در اصطلاح word cloud میگن که یک کتابخونه در پایتون به همین نام وجود داره. ابتدا باید این کتابخونه رو نصب کنیم
pip install wordcloud
کد ما برای ساخت این تصاویر چیزی شبیه به زیر بود:
from wordcloud import WordCloud
import matplotlib.pyplot as plt
import json
with open(fileName,'r',encoding='utf-8') as f:
data=json.load(f)
myDict={}
for t in data:
....myDict[t['name']]=float(t['count'])
wordcloud = WordCloud().generate_from_frequencies(myDict)
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.savefig(imgFile,bbox_inches='tight')
مورد بعدی به بدست اوردن آمار جهانی که اطلاعاتش رو با کدی شبیه به کد زیر از سایت worldometers بدست میاریم.ابتدا باید کتابخونه pandas رو نصب کنیم.
import pandas as pd
pd.options.mode.chained_assignment = None
data = pd.read_html('https://www.worldometers.info/coronavirus/')
data_cases=data[-1]
print(data_cases.to_string())
امکان دیگه ای که توی بات هست لیست کردن مقالات مرتبط با کرونا است که این مقالات رو با کد زیر از سایت corona.ir بدست میاریم. ابتدا باید کتابخونه BeautifulSoup رو نصب کنیم
import requests
from bs4 import BeautifulSoup
req=requests.get('https://corona.ir/')
if req.status_code==200:
....soup = BeautifulSoup(req.content, 'html.parser')
....soup=soup.select_one('section.home:nth-child(6)')
....articles=soup.findAll('h4')
....for a in articles:
........print(a.get_text()+'\nhttps://corona.ir'+a.find('a')['href'])
یه مقدار خلاصه توضیح دادم ولی امیدواردم که براتون مفید باشه. کد کامل رو بعدا توی گیتهاب میزارم و همین پست رو اپدیت میکنم.
نکته ای که هست اینکه اسامی کلاس ها در برنامه های فیسبوک مثل اینستاگرام و واتس اپ به صورت رندوم تولید میشه و احتمالا هر روز یا دو روزی یکبار عوض بشه که باید تغییرات رو اعمال کنید.
نکته بعدی اینکه برای اجرای اپلیکیشن واتس اپ روی سرور از امولاتور genymotion روی ابونتو سرور استفاده کردیم. مشکلی که داشتم اسکن کردن qrcode نسخه تحت وب با استفاده از اپلیکیشن روی شبیه ساز بود. راه حل من برای این مشکل نصب یک fake camera روی امولاتور و گرفتن عکس از صفحه نمایش و ارسال اون به شبیه ساز بود.
امیدوارم که مفید باشه.
مطلبی دیگر از این انتشارات
مقدمه ای بر JUnit
مطلبی دیگر از این انتشارات
آموزش برنامه نویسی پی ال سی
مطلبی دیگر از این انتشارات
پالپ فیکشن یک داستان عامه پسند!