Шалом, Хабр! Сегодня разберём такую тему, как автоматическая рассылка сообщений WhatsApp при помощи библиотеки Selenium на языке программирования Python и поговорим о том, почему же это не так просто, как запарсить Википедию.

P.S. На самом деле это не намного сложнее, чем запарсить Википедию, просто нужно знать об одной очень полезной фиче Selenium'а)

Инструмент Selenium

Очевидно, когда речь заходит об автоматизации работы браузера (а речь пойдёт именно о ней, ведь мы будем использовать web версию WhatsApp) нам необходимо определиться с тем, какой именно библиотекой мы будем пользоваться. Конечно это Selenium! Несмотря на то, что данная библиотека является огромной неповоротливой машиной, вроде замка Хаула, тем то она нам и выгодна, что в нашем распоряжении будет целый браузер с его настройками.

Нюансы WhatsApp и их решения

Я бы и рад был рассказать вам о какой-то библиотеке для работы с Whatsapp, но... Её нет. Нет, конечно есть какой-нибудь pywhatkit, который как раз и существует для отправки сообщений, но он крайне тривиален и слабо подходит для постоянной бесперебойной работы, ведь основан на pyautogui, а значит любой внезапный клик может заруинить скрипт. Поэтому, за неимением у Whatsapp'а нормального API, придётся работать посредством GET запросов и манипуляциями с HTML элементами.

Переходя на страницу WhatsApp Web мы видим печальную картину...

web.whatsapp.com

Почему печальную? Потому что ни тебе полей для ввода данных, ни (спойлер) тебе возможности сохранить тупа cookie для последующей авторизации, ни других способов входа. Да, господа, мобилки в руках - не избежать... Но! Не стоит огорчаться, сегодня я здесь, чтобы рассказать, как сделать так, чтобы было достаточно авторизоваться через QR-код всего один раз, а дальше всё будет происходить само.

В Selenium существует такая штука, как профили. Что это такое? А это возможность сохранить все настройки вашего браузера. Как я и сказал, в отличии от какого-нибудь Вконтакте, тут нам не достаточно сохранить лишь cookie браузера и подгружать их при каждом последующем запуске. Тут нужно сохранять ВСЁ. Таким образом, план действий:

  1. Запустить скрипт с переходом по главной ссылке(web.whatsapp.com).

  2. Когда откроется страница, произвести вход в аккаунт через QR-код.

  3. Завершить скрипт, сохранив профиль.

Вот тогда то, браузер и запомнит наш аккаунт и при последующих запусках и переходах на страницу WhatsApp не будет встречать нас этим штрих кодом из Дикси.

Кодим! Кодим! Кодим!

Забыл сказать, что я буду писать скрипт, подразумевая, что вы пользуетесь Хромом, однако всё легко исправить, заменяя в коде все Chrome на ваш браузер, если что гугланите, там всё супер легко. Просто не хочу засорять статью однотипными строками, описывая каждый браузер.

Первым делом, чтобы вы думали мы должны сделать? Правильно. Установить Selenium.

1) Пишем в консоль pip install selenium

Установил? Молодец. Не поверишь, но пол дела сделано!

2) Теперь надо импортировать его и все необходимые зависимости.

from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.support.ui import WebDriverWait

Да, селеня штука прихотливая. Но поверь, всё что тут есть, в будущем только сократит твой код. Взять хотя бы ChromeDriverManager . Благодаря нему, тебе не придётся искать актуальный WebDriver и докачивать его, достаточно будет указать кое-что в настройках и он скачается сам!

3) А вот и то, ради чего мы тут собрались! Настройка браузера... А точнее реализация той самой фичи с профилем, о которой я говорил.

options = webdriver.ChromeOptions()
options.add_argument('--allow-profiles-outside-user-dir')
options.add_argument('--enable-profile-shortcut-manager')
options.add_argument(r'user-data-dir=<Путь>') # Пример: r'user-data-dir=\Users\user\Desktop\test'
options.add_argument('--profile-directory=Profile 1')
options.add_argument('--profiling-flush=n')
options.add_argument('--enable-aggressive-domstorage-flushing')

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
wait = WebDriverWait(driver, 30)

Итак, разберём построчно мною написанное:
Строка 1: тут мы тупа передаём в переменную options настройки для Хрома. Напоминаю, что если у вас другой браузер, например FireFox, мы просто пишем options = webdriver.FirefoxOptions() .
Строка 2-7: самый сок! Всё это нужно нам, чтобы профили работали.

  1. options.add_argument('--allow-profiles-outside-user-dir') Эта строка добавляет аргумент --allow-profiles-outside-user-dir к настройкам браузера. Он указывает браузеру разрешить использование профилей пользователей вне рабочей директории пользователя.

  2. options.add_argument('--enable-profile-shortcut-manager') Здесь добавляется аргумент --enable-profile-shortcut-manager к настройкам браузера. Он включает менеджер ярлыков профилей, который обеспечивает удобное управление профилями пользователей.

  3. options.add_argument(r'user-data-dir=<Путь>') Эта строка добавляет аргумент user-data-dir, который задает путь к директории данных пользователя браузера. Вместо <Путь> вам нужно указать путь к желаемой директории. Это позволяет использовать предварительно настроенные профили или сохранять состояние браузера между запусками.

  4. options.add_argument('--profile-directory=Profile 1') Здесь указывается аргумент --profile-directory, определяющий имя профиля, который будет использоваться в браузере. В данном случае, имя профиля установлено как "Profile 1".

  5. options.add_argument('--profiling-flush=n') Данная строка добавляет аргумент --profiling-flush к настройкам браузера. Он определяет, как часто происходит сброс данных профилирования. Здесь n представляет числовое значение, указывающее интервал сброса данных.

  6. options.add_argument('--enable-aggressive-domstorage-flushing') В данной строке добавляется аргумент --enable-aggressive-domstorage-flushing к настройкам браузера. Он включает агрессивную очистку хранилища DOM после каждого тестового случая, что может быть полезным при автоматизации тестирования.

В целом, если вы занимаетесь парсингом данных, очень советую вам углубится в тему профилей в Selenuim. Иногда бывают крайне прихотливые и сложные сайты, справится с которыми можно только таким образом. Да, это лишний раз нагружает и без того не лёгкую машину, но лично мне данная фича неимоверно облегчила жизнь, так что если не стоит задачи супер быстродейственного скрипта, вперёд!

Строчка 9: объявления driver. Это по сути и есть наш браузер. Именно через него будут происходить все взаимодействия. Кстати, помните я сказал, что облегчу вам жизнь? Service(ChromeDriverManager().install()) именно этот параметр избавит вас от необходимости лазить по интернету и искать свою версию WebDriver.

Ну и в строке 10: настраиваем функцию wait, чтобы наш браузер знал сколько ему ждать, какого-то события. В данном случае, наш браузер ждёт 30 секунд.

В целом, настройка окончена, теперь переходим к самому WhatsApp.

4) Вот мы попали на наш аккаунт. Что теперь? Теперь начинается обыкновенный парсинг. нажимаем f12, ищем кнопочки, пути к ним, указываем на них скрипту и работаем с ними. Однако даже тут я спасу твоё время. Помнишь я говорил про GET запросы? Дабы не париться и не искать поле с контактами, поле с вводом текста, и т.д, мы просто сформируем готовую URL ссылку, перейдя по которой достаточно будет только нажать Enter.

url = f"https://web.whatsapp.com/send?phone={"Номер"}&text={"Тут+пишем+текст"}"
driver.get(url)
wait.until(EC.element_to_be_clickable((By.XPATH, '/html/body/div[1]/div/div/div[5]/div/footer/div[1]/div/span[2]/div/div[2]/div[2]/button')))
driver.find_element(By.XPATH, '/html/body/div[1]/div/div/div[5]/div/footer/div[1]/div/span[2]/div/div[2]/div[2]/button').click()

Строка 1: Указываем URL адрес страницы. После send? мы как раз и начинаем формировать наш GET запрос. phone={"Номер"} отвечает за номер, на который будет отправлено сообщение. Например: phone={"+70000000000"} . Далее идёт параметр text. Очень ВАЖНО! Вместо пробелов необходимо использовать "+", т.к в URL ссылке не может быть пробелов!
Строка 2: Переходим по заданному URL.
Строка 3: Ждём, пока появится кнопка отправить. Советую не пренебрегать данной функцией, ведь иногда ваш интернет может не поспеть за браузером и не прогрузить кнопку и тогда следующая функция нажатия уйдёт в никуда.
Строка 4: Нажимаем на кнопку. При помощи метода .click()

Поздравляю, сообщение отправлено!

5) Не забудьте произвести первую авторизацию.

from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from time import sleep

options = webdriver.ChromeOptions()
options.add_argument('--allow-profiles-outside-user-dir')
options.add_argument('--enable-profile-shortcut-manager')
options.add_argument(r'user-data-dir=<Путь>') # УКАЖИТЕ ПУТЬ ГДЕ ЛЕЖИТ ВАШ ФАЙЛ. Советую создать отдельную папку.
options.add_argument('--profile-directory=Profile 1')
options.add_argument('--profiling-flush=n')
options.add_argument('--enable-aggressive-domstorage-flushing')

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)

url = "https://web.whatsapp.com/"
driver.get(url)
sleep(60)

Перейдя на страницу, у вас будет 60 секунд, чтобы войти в профиль.

По сути, на этом всё. Далее уже адаптируете код под свои цели. Например для рассылки на множество номеров, можно создать файл с номерами и пустить скрипт через цикл for, создав для номера переменную и вставив её вместо "Номер".

Готовый код:

from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.support.ui import WebDriverWait
from time import sleep

options = webdriver.ChromeOptions()
options.add_argument('--allow-profiles-outside-user-dir')
options.add_argument('--enable-profile-shortcut-manager')
options.add_argument(r'user-data-dir=<Путь>') # УКАЖИТЕ ПУТЬ ГДЕ ЛЕЖИТ ВАШ ФАЙЛ. Советую создать отдельную папку.
options.add_argument('--profile-directory=Profile 1')
options.add_argument('--profiling-flush=n')
options.add_argument('--enable-aggressive-domstorage-flushing')

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
wait = WebDriverWait(driver, 30)


numbers = ["+70000000000", "+70000000001"]
text = "Привет+мир!"

for number in numbers:

    url = f"https://web.whatsapp.com/send?phone={number}&text={text}"
    driver.get(url)
    wait.until(EC.element_to_be_clickable((By.XPATH, '/html/body/div[1]/div/div/div[5]/div/footer/div[1]/div/span[2]/div/div[2]/div[2]/button')))
    driver.find_element(By.XPATH, '/html/body/div[1]/div/div/div[5]/div/footer/div[1]/div/span[2]/div/div[2]/div[2]/button').click()
    sleep(5)

В заключении хочу сказать, что данная статья не создана с целью научить вас спамить, флудить и портить жизнь людям иными способами. Этот материал несёт исключительно ознакомительный характер. Думайте, перед тем, как что-то делать.

Автор статьи: NeGoy
"Дорогу осилит идущий"
Удачи!

Комментарии (10)


  1. craxti
    16.05.2023 09:34
    +1

    Интересно, возможно кому-то даже полезно
    Но смотрю на ваши локаторы и очень уж они не красивые и могу очень легко ломаться как пример

    /html/body/div[1]/div/div/div[5]/div/footer/div[1]/div/span[2]/div/div[2]/div[2]/button 

    Очень высока вероятность поломки локатора если немного обновят страницу и придется заного заходить на страницу и искать новый xpath, я бы порекомендовал использовать другие локаторы или писать их самому


    1. NeGoy Автор
      16.05.2023 09:34
      -2

      Локатор указывает на кнопку отправки сообщения. Она не изменна.


      1. NeGoy Автор
        16.05.2023 09:34

        Неизменна*

        Так же можно использовать css селектор, так надёжнее.


        1. Free_ze
          16.05.2023 09:34

          Если селектор будет такой же глубокий и на :nth-child, то надежней он не будет.


    1. guitar4for
      16.05.2023 09:34

      соглашусь, семантически бессмысленный div[5] запросто завтра станет div[6]


    1. stas_grishaev
      16.05.2023 09:34

      поддержу, указание "абсолютного" xpath в автоматизации считается bad practice, конкретно в данном случае локатор можно заменить на //button[@ data-testid='compose-btn-send'] ( пробел после символа собаки нужен чтобы хабровский редактор не воспринимал это как меншен юзера ).


  1. dnbolt
    16.05.2023 09:34

    Эхх вто бы чтобы zabbix слал уведомления в группу/групповой чат WA а Youtrack принимал заявки из чата и контактов, но суть ясна что в теории можно.


    1. aborouhin
      16.05.2023 09:34
      +1

      В данном случае непонятно, зачем забивать гвозди микроскопом. И гвозди забьются так себе (возможности WA для таких задач всё-таки зачаточные), и микроскоп можем поломать (получить бан в WA). Если круг пользователей заранее определён - используем Телеграм, у которого и боты, и API, и куча готовых интеграций.

      WA нужен когда непонятно, есть ли у получателя что-то другое. Ну, скажем, какой-нибудь магазин, чтобы не тратиться на дорогущие СМСки, пробует рассылать уведомления через все мессенджеры/соцсети по очереди. Но для таких случаев у WA, вроде как, Business API есть. Так что остаётся только спам...


      1. NeGoy Автор
        16.05.2023 09:34

        Вопрос не в том, что лучше. Вопрос в том, как это сделать в WA. Есть спрос, в том числе на фрилансах, я рассказал как реализовать.

        По поводу WA business, там высокий порог входа и ещё более тёмный лес.


  1. longvalery
    16.05.2023 09:34

    Вы забыли, что нужно еще выполнить пункт 1.1. pip install webdriver_manager

    Тогда всё будет без ошибок

    Но код рабочий, круто.

    Спасибо