Как написать бота для браузера

Предисловие.

Веб-бот — это программа, которая автоматизирует ваши действия в интернете.

В этой статье, я объясню общий принцип создания ботов на Python, применив полученные знания, вы сможете создать бота который:

  • Получает актуальную информацию о стоимости товара.
  • Автоматически участвует в раздачах.
  • Пишет сообщения от вашего имени (ссылка на урок).
  • Скачивает картинки с сайта (ссылка на урок).

Приступим.

Создаем первого бота на Selenium.

Selenium — это библиотека для автоматизации действий в браузере.

Данный способ подойдет для любого сайта, однако, за все нужно платить. Selenium запускает браузер, отъедая огромный запас оперативной памяти. Используйте его только тогда, когда нужно выполнить JS код на странице.

Первым делом нужно установить библиотеку, для этого введите в консоли:

pip install selenium

Далее, установите веб-драйвер под браузер Firefox отсюда. Также, необходимо установить браузер Mozilla Firefox, если еще не установлен.

Теперь напишем простейшего бота. Для этого, напишите следующий python скрипт.

from selenium import webdriver #Ипортируем библиотеку
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.firefox_binary import FirefoxBinary

binary = FirefoxBinary('C:/Program Files/Mozilla Firefox/firefox.exe') #Прописываем путь до firefox.exe
browser = webdriver.Firefox(firefox_binary=binary) #создаем объект Firefox браузера
browser.get('https://under-prog.ru') #посредством метода get, переходим по указаному URL

Код скрипта описан в комментариях.

Далее, переместите файл скрипта, в одну папку с веб-драйвером geckodriver.exe

И запустите python скрипт. У вас должен открыться браузер.

В адресной строке видна иконка робота, это значит, что браузером управляет программа.

Хорошо, бот создан, но он бесполезен. Единственное на что он способен, это заходить на сайт. Давайте добавим ему новых функций. Например, сделаем так, чтобы бот лайкал посты на сайте.

Бот лайкающий посты на сайте.

Последовательность действий у нас следующая.

  1. Зайти на сайт under-prog.ru (открыть браузер)
  2. Пройтись по каждому из постов.
  3. Нажать кнопку лайк, если она не нажата.
  4. Закрыть браузер.

Первый пункт мы уже сделали, перейдем ко второму.

Пройтись по каждому из постов.

На этом этапе, нужно понимать разметку HTML.

Зайдите на сайт, и нажмите кнопку F12.

У вас откроются инструменты разработчика. Изучив разметку, мы понимаем, что все посты находятся в теге article.

Сейчас нам нужно получить ссылку, на каждый пост. Для этого будем использовать этот css селектор.

article div div.blog-entry-content header.entry-header h2.entry-title a

Данный селектор указывает:

  • На элемент с тегом a
  • который находится находится внутри тега h2 с классом entry-title
  • тот, в свою очередь, находится внутри тега header с классом entry-header
  • тег header находится внутри тега div с классом blog-entry-content
  • тот, находится в теге div
  • тег div находится внутри тега article

Теперь, дополним бота.

link = browser.find_elements(By.CSS_SELECTOR, 'article div div.blog-entry-content header.entry-header h2.entry-title a')[0].get_attribute('href')
print(link)

Разберем новую функцию.

browser.find_elements(By.CSS_SELECTOR, css_селектор)

Данная функция ищет элементы по css селектору. В результате своей работы, она возвращает массив элементов.

link = browser.find_elements(By.CSS_SELECTOR, 'article div div.blog-entry-content header.entry-header h2.entry-title a')[0].get_attribute('href')

В-общем, мы из этого массива, достали первый элемент, и при помощи функции get_attribute(), получили значение атрибута href (ссылка на пост).

print(link)

И вывели его на экран.

Запустите скрипт, в консоли должна появится ссылка на первый пост.

Если закинуть массив элементов в цикл, то получится извлечь ссылки на все посты.

from selenium import webdriver #Ипортируем библиотеку
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.firefox_binary import FirefoxBinary

binary = FirefoxBinary('C:/Program Files/Mozilla Firefox/firefox.exe') #Прописываем путь до firefox.exe
browser = webdriver.Firefox(firefox_binary=binary) #создаем объект Firefox браузера
browser.get('https://under-prog.ru') #посредством метода get, переходим по указаному URL

elements = browser.find_elements(By.CSS_SELECTOR, 'article div div.blog-entry-content header.entry-header h2.entry-title a')
for elem in elements:
    print(elem.get_attribute('href'))

Отлично, ссылки на все посты получены, осталось всем этим постам, поставить лайк.

Нажать кнопку лайк, если она не нажата

Сначала перекопируем наши ссылки в отдельный массив. Замените это:

elements = browser.find_elements(By.CSS_SELECTOR, 'article div div.blog-entry-content header.entry-header h2.entry-title a')
for elem in elements:
    print(elem.get_attribute('href'))

На это:

elements = browser.find_elements(By.CSS_SELECTOR, 'article div div.blog-entry-content header.entry-header h2.entry-title a')
links_arr = []
for elem in elements:
    links_arr.append(elem.get_attribute('href'))

Далее напишем код, отвечающий за нажатие кнопки лайк.

for link in links_arr: #Проходим по каждому элементу массива ссылок
    browser.get(link) #Переходим по ссылке
    if browser.find_element(By.CSS_SELECTOR, 'button.wp_ulike_btn').get_attribute('class').find('wp_ulike_btn_is_active') == -1: #Если кнопка 'лайк' не нажата, тогда
        browser.find_element(By.CSS_SELECTOR, 'button.wp_ulike_btn').click() #кликаем по кнопке 'лайк'

Разберем данные строки.

if browser.find_elements(By.CSS_SELECTOR, 'button.wp_ulike_btn').get_attribute('class').find('wp_ulike_btn_is_active') == -1:

Данная строка ищет кнопку с помощью css_селектора, и получает строку с названиями классов нашей кнопки.

Далее, при помощи функции find (стандартная функция python), мы получаем индекс подстроки ‘wp_ulike_btn_is_active‘, если не удалось найти подстроку, функция find возвращает -1, этим мы и воспользовались в нашем условии. Т.е. если атрибут ‘class‘ не содержит подстроку ‘wp_ulike_btn_is_active‘, то.

browser.find_elements(By.CSS_SELECTOR, 'button.wp_ulike_btn').click()

Кликаем по кнопке лайк.

Осталось закрыть браузер, делается это с помощью функции quit().

browser.quit()

Бот завершен, запустите скрипт, и наслаждайтесь автоматизацией.

Делаем браузер невидимым

Бот работает и все-бы ничего, но своим окном бразуера, он перекрывает все остальные окна. К счастью, у Firefox есть headless режим, позволяющий пользоваться функциями бразура, не открывая окно браузера.

Добавьте следующий код перед инициализацией браузера:

option = webdriver.FirefoxOptions()
option.headless = True

Здесь, мы переопредили настройки браузера, осталось передать их, нашему браузеру.

browser = webdriver.Firefox(options=option)

Теперь, порядок.

Полный код бота:

from selenium import webdriver #Ипортируем библиотеку
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.firefox_binary import FirefoxBinary

binary = FirefoxBinary('C:/Program Files/Mozilla Firefox/firefox.exe') #Прописываем путь до firefox.exe
browser = webdriver.Firefox(firefox_binary=binary) #создаем объект Firefox браузера
browser.get('https://under-prog.ru') #посредством метода get, переходим по указаному URL

elements = browser.find_elements(By.CSS_SELECTOR, 'article div div.blog-entry-content header.entry-header h2.entry-title a')
links_arr = []
for elem in elements:
    links_arr.append(elem.get_attribute('href'))
    
for link in links_arr:
    browser.get(link)
    if browser.find_element(By.CSS_SELECTOR, 'button.wp_ulike_btn').get_attribute('class').find('wp_ulike_btn_is_active') == -1:
        browser.find_element(By.CSS_SELECTOR, 'button.wp_ulike_btn').click()
    
browser.quit()

Итоговый результат.

avatar

Время на прочтение
12 мин

Количество просмотров 14K

Selenium – один из самых популярных фреймворков автоматизации тестирования. Несмотря на то, что Selenium поддерживает шесть популярных языков программирования, его можно использовать для волшебной реализации с наименьшим количеством кода в связке с Python.

Помимо автоматизированного веб-тестирования, веб-скрейпинг с  Selenium на Python – крайне популярный вариант, который вы можете реализовать с меньшим количеством кода и ошибок на Python. Как QA-инженер, я по-прежнему использую другие языки программирования, такие как Java и JavaScript в связке с Selenium, но Python – мой основной инструмент для простых/комплексных задач веб-автоматизации.

Как я уже упоминал, Python – третий по популярности язык после HTML/CSS и JavaScript. Бот на Selenium с Python может широко использоваться для автоматизации различных сценариев и задач.

В этом руководстве я подробно расскажу, как создать автоматизированного бота на Selenium с Python. Знания окажутся для вас полезными, поскольку мы коснемся различных аспектов автоматизированного тестирования с Selenium.

Инструменты для создания бота

Одной из основных целей создания бота является использование возможностей, предлагаемых Selenium для автоматизации взаимодействия с web-элементами в DOM. Существует набор инструментов Selenium для автоматизации веб-браузеров, которые можно использовать для целого ряда задач автоматизации, такие как веб-скрейпинг, автоматизированное веб-тестирование, кросс-браузерное тестирование и т.д.

Итак, давайте посмотрим на основные инструменты, необходимые для создания бота с Selenium.

Selenium

Библиотека Python – обертка над Selenium WebDriver, обеспечивает соединение и простой API для написания функциональных тестов для автоматизации веб-задач, таких как нажатие на кнопки, навигация по страницам и заполнение форм.

Скрипт теста не взаимодействует напрямую с браузером. Selenium WebDriver — это ядро фреймворка Selenium. Как говорится в этом руководстве, Selenium WebDriver играет важную роль в соединении с браузером с помощью соответствующего драйвера браузера. Однако, вы можете еще глубже погрузиться в Selenium, если хотите узнать больше о его компонентах.

Если тесты должны выполняться в локальной Selenium Grid, на целевой машине должен быть установлен соответствующий драйвер браузера: 

Загружать и устанавливать драйверы браузера необходимо только если вы хотите выполнять тесты в локальном Selenium Grid. В случае с удаленным Selenium WebDriver тесты выполняются в облачном Selenium Grid (например, LambdaTest).

Фреймворки для автоматизации на Python

Python – самый популярный выбор для автоматизированного тестирования с Selenium, ведь сам по себе язык не такой многословный, поддерживает динамическую типизацию, дает беспроблемную отчетность и многое другое. Широкий спектр фреймворков Selenium поддерживает параллельное тестирование, что ускоряет время выхода на рынок.

В этом руководстве я затронул основы Selenium Python, так позвольте мне углубиться в популярные фреймворки Selenium для автоматизации тестирования:

PyUnit (Unittest)

Unittest (или PyUnit) – это фреймворк для тестирования на Python по умолчанию, который поставляется «из коробки». Этот фреймворк вдохновлен другим популярный фреймворком JUnit.

Unittest обеспечивает простое и гибкое выполнение тест-кейсов с быстрой генерацией отчетов о тестировании. Unittest использует camelCase вместо соглашения об именовании snake_case в Python.

Руководство по Selenium PyUnit может стать хорошей отправной точкой для тех, кто хочет использовать фреймворк для автоматизированного тестирования.

PyTest

PyTest – это открытый фреймворк для тестирования, который поддерживает модульное тестирование, функциональное и тестирование API. Он доступен для Python версии 3.5 и выше. 

Объектная модель страницы (POM) в Selenium Python играет важную роль в улучшении поддержки и переиспользовании кода. Я отдаю предпочтение PyTest, а не PyUnit из-за богатого набора плагинов – настоящего блага для QA-инженеров.

Вот несколько популярных плагинов PyTest для QA-инженеров:

  • pytest-randomly

  • pytest-cov

  • pytest-django

  • pytest-bdd

Если вы хотите рассмотреть все аспекты автоматизации тестирования с Selenium и PyTest, обратитесь к этому руководству. Оно окажется полезным как новичкам, так и профессионалам, и поможет узнать, как использовать фреймворк PyTest для автоматизации.

Robot

Robot – это открытый фреймворк для автоматизации, который позволяет писать тест-кейсы на Gherkin. Лучше всего работает в методологии Behaviour Driven Development (BDD) в связке с Selenium и Gherkin. Robot также используется для приемочного TDD и автоматизации роботизированных процессов (RPA). 

Ядро Robot написано на Python, но он также может работать с Jython и IronPython. Изначально он не поддерживает параллельное тестирование, но предоставляет множество простых в использовании расширений и библиотек.

Для демонстрации реализации бота Selenium с Python я собираюсь использовать фреймворк PyTest.

Behave

Behave – фреймворк для тестирования, работающий в связке с Behavior-Driven Development (BDD). У него обширная документация и он хорошо поддерживается сообществом. Еще он обеспечивает интеграцию с Django и Flask – двумя самыми популярными веб-фреймворками на Python. 

Behave также пригодится для blackbox – тестирования. Связка BDD, Behave и Selenium имеет свой собственный набор преимуществ и недостатков, но это все еще отличный фреймворк для автоматизации тестирования на Python.

Помимо тех, что я перечислил, есть еще ряд полезных фреймворков, которые можно изучить, чтобы на полную использовать возможности Selenium и Python.

Планировщики задач для Python

Планировщики задач – это программы, которые позволяют планировать задачи и сценарии через определенные промежутки времени. С другой стороны, сценарии также можно планировать или запускать при достижении определенных условий.

Планировщик задач можно использовать для запуска пакетного файла (или .bat), который запускает соответствующий скрипт на Python. 

Вот некоторые распространенные программы, обеспечивающие планирование задач на Python:

Cron

Cron – планировщик задач, который основывается на времени в Unix-подобных операционных системах, таких как Linux и macOS. Он позволяет планировать такие задачи, как создание резервных копий, логирование и запуск скриптов.

Планировщик задач Windows

Планировщик задач – компонент ОС Windows, который позволяет автоматически создавать и запускать практически любую задачу. Обычно, система и некоторые приложения поддерживают автоматизацию и обслуживание по расписанию. Такие задачи, как дефрагментация диска, очистка диска, обновления и т.д. могут быть запланированы автоматически с помощью планировщика задач Windows.

Планировщик задач Windows работает, отслеживая время и события на компьютерах, и выполняет задачу, если выполняются необходимые условия.

Для текущей задачи я собираюсь использовать планировщик задач Windows вместе с библиотекой Win32com – тонкой оберткой на Python, которая помогает автоматизировать приложения Windows. 

Теперь, когда мы коснулись неотъемлемых аспектов создания бота на Selenium и Python, давайте испачкаем руки реализацией.

Создание бота на Selenium с Python

Выполните указанные ниже шаги, чтобы создать среду разработки для нашей реализации:

Создание виртуальной среды и установка зависимостей

Виртуальная среда (или virtualenv) — инструмент для создания изолированных сред Python. Для этой цели в Python есть модуль venv, который поддерживает создание облегченных «виртуальных сред» с изолированными каталогами.

Вы можете ознакомиться с этим руководством для быстрого погружения в автоматизированное тестирование с помощью Selenium и Python.

python3 -m venv env
  1. Выполните одну из команд ниже, чтобы активировать эту виртуальную среду.

Windows

.envScriptsactivate

Linux

source env/bin/activate
  1. Выполните следующую команду, чтобы установить Selenium для Python:

pip install selenium
  1. Выполните следующую команду, чтобы установить фреймворк PyTest:

pip install pytest
  1. Установите пакет pywin32 для работы с планировщиком задач Windows:

	
pip install pywin32

Структура папок для тестовых скриптов

Проект состоит из двух файлов:

  • main.py – Содержит основной скрипт, который позволяет входить в платформу LambdaTest и получать подробную информацию о соответствующих тестах.

  • scheduler.py – Содержит реализацию для планирования запуска тестов.

Реализация бота

Для демонстрации создания бота Selenium с Python мы автоматизируем тестовый сценарий входа на платформу LambdaTest. Тестируемый URL-адрес — https://accounts.lambdatest.com/login

Поскольку тест демонстрируется в LambdaTest Grid, я использую удаленный WebDriver в Selenium вместо локального. Для начала мы импортируем необходимые пакеты.

from selenium import webdriver

Мы определим две переменные, в которые положим имя пользователя и ключ доступа, комбинация которых используется для доступа к облачной LambdaTest Grid. Имя пользователя и ключ доступа доступны со страницы профиля LambdaTest.

username = "<YOUR USERNAME>"
access_key = "<YOUR ACCESS KEY>"

Создадим класс TestClass со следующим функционалом:

  • Setup_method – функция настройки удаленного драйвера и его свойств.

# Generate capabilities from here: https://www.lambdatest.com/capabilities-generator/
# setUp runs before each test case and
def setup_method(self):
desired_caps = {
       "build": "your build name",
       "name": "your test name",
       "platform": "Windows 10",
       "browserName": "Chrome",
       "version": "92.0",
       "selenium_version": "3.13.0",
       "geoLocation": "IN",
       "chrome.driver": "91.0",
}
"""
Setup remote driver
       -------
       username and access_key can be found on the lambdatest platform
 
"""
self.driver = webdriver.Remote(
       command_executor="https://{}:{}@hub.lambdatest.com/wd/hub".format(
       username, access_key
       ),
       desired_capabilities=desired_caps )
  • Teardown_method – функция для разрыва соединения после каждого тест-кейса.

def teardown_method(self):
       self.driver.quit()
  • Test_login – Реализация этой функции используется для входа в дашборд LambdaTest. Здесь я использую SendKeys в Selenium для заполнения формы входа, чтобы войти на сайт LambdaTest. Вот синтаксис метода SendKeys в Selenium Python:

element.send_keys("text to enter")

Для определения местоположения соответствующих веб-элементов я использую локатор XPath в Selenium. Однако вы можете использовать другие популярные локаторы Selenium, такие как ID, Name, linktext и т.д. для определения местоположения веб-элементов.

Ниже показан синтаксис метода find_element_by_xpath() в Selenium Python. В качестве альтернативы вы также можете использовать метод driver.find_element(By.XPATH) для определения местоположения элемента с помощью локатора XPath.

element = driver.find_element_by_xpath("xpath_here")
 
driver.find_element(By.XPATH, "xpath")

Вот как выглядит весь метод входа в систему (test_login):

def test_login(self):
       """
       this function logins you in lambdatest dashboard
       """
       LOGIN_URL = "https://accounts.lambdatest.com/login"
       DASHBOARD_URL = "https://accounts.lambdatest.com/dashboard"
       self.driver.get(LOGIN_URL)
       login_email = self.driver.find_element_by_xpath(
           "//input[@id='email']"
       )
       login_password = self.driver.find_element_by_xpath(
           "//input[@id='password']"
       )
       login_button = self.driver.find_element_by_xpath(
           "//button[@id='login-button']"
       )
 
       login_email.send_keys(email)
       login_password.send_keys(password)
       login_button.click()
 
       # if we are on right page
       if self.driver.current_url == DASHBOARD_URL :
           assert True
           # time to get recent tests
           self.get_recent_tests()
       else:
           print(" something went wrong !!")
           assert False
  • Get_recent_tests – Как только я вошел в дашборд LambdaTest, извлекаю из него все последние тесты.

Как видно выше, .cut-text — общий класс для всех тегов, которые нам нужно прочитать. Далее я сделаю веб-скрейпинг для получения необходимой информации.

titles = self.driver.find_elements_by_class_name("cut-text")

Здесь я получаю текст из соответствующих тегов:

def get_recent_tests(self):
       titles = self.driver.find_elements_by_class_name("cut-text")
       titles_list = []
       for i in titles:
           titles_list.append(i.text)
       message = ' '.join(titles_list[0::2])
       print(message)
       send_email(message)

Как видно из реализации, мы используем Selenium 3 Grid для тестирования. С выпуском стабильной версии Selenium 4 многие QA-инженеры переносят тесты в Selenium 4 Grid. Прежде чем сделать это, я рекомендую сравнить Selenium 3 и Selenium 4, чтобы можно было беспрепятственно переносить тесты.

Отправка электронных писем через Selenium для Python

В Python есть встроенная библиотека smtplib для отправки электронных писем с защищенным подключением с использованием SMTP_SSL() и .starttls(). Библиотека smtplib использует протокол RFC 821 для SMTP.

Для отправки электронного письма через smtplib я создал функцию, которая принимает сообщение в виде строки и отправляет электронное письмо.

def send_email(message):
   context = ssl.create_default_context()
   try:
       server = smtplib.SMTP(smtp_server,port)
       server.ehlo() # Can be omitted
       server.starttls(context=context) # Secure the connection
       server.ehlo() # Can be omitted
       server.login(sender_email, sender_password)
       server.sendmail(sender_email, receiver_email, message)
       server.close()
   except:
       print(" failed to send email ")

Так выглядит итоговая реализация main.py.

from selenium import webdriver
import smtplib, ssl
 
port = 587
smtp_server = "smtp.gmail.com"
sender_password = "<YOUR_PASSWORD>"
sender_email = "<YOUR_EMAIL>"
receiver_email = "<EMAIL_WHERE_YOU_WANT_TO_SEND>"
 
def send_email(message):
   context = ssl.create_default_context()
   try:
       server = smtplib.SMTP(smtp_server,port)
       server.ehlo() # Can be omitted
       server.starttls(context=context) # Secure the connection
       server.ehlo() # Can be omitted
       server.login(sender_email, sender_password)
       server.sendmail(sender_email, receiver_email, message)
       server.close()
   except:
       print(" failed to send email ")
 
username = "<YOUR_LAMBDATEST_USERNAME>"
access_key = "<YOUR__LAMBDATEST_ACCESS_KEY>"
 
class TestClass():
   def setup_method(self):
       desired_caps = {
           "build": "your build name",
           "name": "your test name",
           "platform": "Windows 10",
           "browserName": "Chrome",
           "version": "92.0",
           "selenium_version": "3.13.0",
           "geoLocation": "IN",
           "chrome.driver": "91.0",
       }
       """
       Setup remote driver
               -------
               username and access_key can be found on lt platform
 
       """
       self.driver = webdriver.Remote(
           command_executor="https://{}:{}@hub.lambdatest.com/wd/hub".format(
               username, access_key
           ),
           desired_capabilities=desired_caps,
       )
 
   # tearDown runs after each test case
   def teardown_method(self):
       self.driver.quit()
 
   def test_login(self):
       """
       this function logins you in lambdatest dashboard
       """
       LOGIN_URL = "https://accounts.lambdatest.com/login"
       DASHBOARD_URL = "https://accounts.lambdatest.com/dashboard"
       self.driver.get(LOGIN_URL)
       login_email = self.driver.find_element_by_xpath(
           "//input[@id='email']"
       )
       login_password = self.driver.find_element_by_xpath(
           "//input[@id='password']"
       )
       login_button = self.driver.find_element_by_xpath(
           "//button[@id='login-button']"
       )
 
       login_email.send_keys(email)
       login_password.send_keys(password)
       login_button.click()
 
       # if we are on the right page
       if self.driver.current_url == DASHBOARD_URL :
           assert True
           # time to get recent tests
           self.get_recent_tests()
       else:
           print("something went wrong !!")
           assert False
 
   def get_recent_tests(self):
      
       titles = self.driver.find_elements_by_class_name("cut-text")
       titles_list = []
       for i in titles:
           titles_list.append(i.text)
       message = ' '.join(titles_list[0::2])
       send_email(message)

Планирование теста Selenium на Python

Планировщик позволяет запланировать выполнение тестовых скриптов в заданный момент времени или при заданных условиях. Для этой задачи я использую планировщик задач Windows, который представляет из себя приложение с графическим интерфейсом. Я использую библиотеку Win32com для программного планирования триггеров.

Win32com — библиотека API, тонкая оболочка Python, которая позволяет автоматизировать приложения Windows, как экземпляры планировщика задач Windows.

Планировщик задач Windows использует объекты COM (Component Object Model), что позволяет управлять приложениями Windows из другой программы или скрипта. COM-объекты определяются в реестре Windows. Поскольку COM-объекты сами по себе являются «объектами», их можно использовать для тестового скрипта и с ними можно взаимодействовать программно.

Краткий код на Python для запуска тестового скрипта в планировщике задач Windows:

  • Определение клиента win32com из библиотеки pywin32:

scheduler = win32com.client.Dispatch('Schedule.Service')
scheduler.Connect()
root_folder = scheduler.GetFolder('\')
task_def = scheduler.NewTask(0)
  • Настройка важных аргументов

start_time: Установите время запуска скрипта.

start_time = datetime.datetime.now() + datetime.timedelta(minutes=1)

Repetition.Duration: продолжительность времени (в днях), в течение которого необходимо повторять выполнение программы. В данном случае я хочу запустить этот скрипт на 10 дней.

#Repeat for 10 days
num_of_days = 10
trigger.Repetition.Duration = "P"+str(num_of_days)+"D"

Repetition.Interval: Переменная указывает, с каким интервалом должна выполняться программа. Здесь я настроил запуск скрипта через каждые 6 часов.

#For every 6 hour
trigger.Repetition.Interval = "PT6H"

action.Path: Путь к интерпретатору Python или бинарному файлу pytest.

action.Path = r'C:Usersvinayakselenium_testenvScriptspytest.exe'

action.Arguments: Путь к скрипту, который должен выполняться.

action.Arguments = r'C:Usersvinayakselenium_testmain.py'

Для кода планировщика давайте создадим файл scheduler.py.

import datetime
import win32com.client
 
scheduler = win32com.client.Dispatch('Schedule.Service')
scheduler.Connect()
root_folder = scheduler.GetFolder('\')
task_def = scheduler.NewTask(0)
 
# Start time of script
start_time = datetime.datetime.now() + datetime.timedelta(minutes=1)
 
# for running it one time
TASK_TRIGGER_DAILY = 1
trigger = task_def.Triggers.Create(TASK_TRIGGER_DAILY)
 
#Repeat for 10 day
num_of_days = 10
trigger.Repetition.Duration = "P"+str(num_of_days)+"D"
 
#For every 6 hour
trigger.Repetition.Interval = "PT6H" 
trigger.StartBoundary = start_time.isoformat()
 
# Create action
TASK_ACTION_EXEC = 0
action = task_def.Actions.Create(TASK_ACTION_EXEC)
action.ID = 'TRIGGER BATCH'
action.Path = r'C:Usersvinayakselenium_testenvScriptspytest.exe'
action.Arguments = r'C:Usersvinayakselenium_testmain.py'
 
# Set parameters
task_def.RegistrationInfo.Description = 'Test Task'
task_def.Settings.Enabled = True
task_def.Settings.StopIfGoingOnBatteries = False
 
# Register task
# If task already running, it will be updated
TASK_CREATE_OR_UPDATE = 6
TASK_LOGON_NONE = 0
root_folder.RegisterTaskDefinition(
   'Test Task',  # Task name
   task_def,
   TASK_CREATE_OR_UPDATE,
   '',  # No user
   '',  # No password
   TASK_LOGON_NONE

Обертка

Selenium Webdriver с Python используется для таких задач, как веб-скрейпинг или тестирование приложений. Но автоматизацию можно использовать и иначе, как мы использовали ее в нашем случае. В этом руководстве мы рассмотрели, как создать автоматизированного бота на Selenium с Python.

Аналогично планировщик задач Windows можно использовать многими способами, не только для планирования задач для приложений Windows. Также она позволяет запускать сценарии в фоновом режиме без необходимости помнить о них постоянно.


Приглашаем всех желающих на открытый урок «Написание автотестов для UI-части интернет-магазина и формирование отчётности».

Пишем своего бота для любого сайта на C#

DarkD Сливы

Всем привет. По просьюам трудящихся, решил показать как сделать своего бота практически для любого сайта на C# и библиотеки для управления браузером Selenium Web Driver.

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

В чем плюсы написания бота именно на C#

[*] С# (далее шарп) даёт очень понятный для понимания код, даже тем, кто в программировании полный ноль

[*] На выходе бот будет именно приложением EXE, а не каким-то скриптом

[*] Да и этого думаю хватит, а то больше голова не варит)

В качестве среды разработки я использую бесплатную официальную версию Visual Studio Community 2015

https://www.microsoft.com/ru-ru/SoftMicrosoft/vs2015Community.aspx

Так же понадобится сама библиотека Selenium Web Driver + Chrome Driver т.к. я буду показывать пример именно для браузера хром

В Visual Studio нажимаем «Проект — Управление пакетами NuGet»

На вкладке «Обзор» пишем Selenium и устанавливаем его

Чуть ниже качаем Selenium.Chrome.WebDriver

Теперь можно приступить к написанию самого бота, запускаем нашу Vusual Studio и нажимаем «Файл — создать — проект»

Язык выбираем C#, затем «Приложение Windows Forms (.NET Framework)», внизу пишем как будет называться наш проект и выбираем место сохранения проекта, затем жмём ОК

Теперь стоит вопрос, какой бот для примера написать? Так как в голову после вчерашней синьки ничего толкового не пришло, я решил показать пример написания бота для VLMI.su (реклама хули, а то всёравно копипастом разойдется статейка)

Теперь нужно подумать головой, что должен делать бот и составить ТЗ (техническое задание).

Так как мы только учимся, то нагружать бот большим функционалом мы не будем.

ТЗ будет выглядеть примерно так:

1. Зайти на сайт
2. Авторизоваться на форуме используя свой логин и пароль
3. Зайти в раздел "Флудилка"
4. Нажать там кнопку "Создать тему"
5. Ввести заголовок темы
6. Ввести текст темы
7. Нажать кнопку "Создать тему"

Думаю для понимания принципа работы бота этого будет достаточно.

Дальше будет на время написания статьи стоять большой хайд, т.к. время у меня не резиновое.

После написания всей статьи хайд будет снят.

Для того, чтобы окно нашей программы нельзя было растягивать по высоте и ширине, нужно кликнуть по окну программы и в свойствах установить Form Border Style в Fixed Dialog. Там же можно и изменить текст, который будет писаться в окне нашей программы

Там можно и другие настройки выставить, внизу будет писать, что делает тот или иной пункт

Какие элементы нужно добавить на форму я буду писать в процессе написания.

Сейчас нам нужно добавить

Два Label (Это обычный текст, чтобы было понятно куда писать логин, а куда пароль для авторизации)

Делаем это

Теперь нажимаем на каждый label и пишем ему нормальное имя всё в тех же свойствах, про которые я писал выше

Тепрь на форму нужно кинуть 2 текстбокса и всё это говницо более менее нормально выровнять, чтобы кровь из глаз не пошла

Теперь нужно нажать на каждом текстбоксе и изменить его имя, чтобы потом было легче писать код, я сделал текстбоксам имена LOGINBOX и PASSBOX

Теперь нужно по такому же принципу добавить на форму пару label и Textbox, в которые мы будем писать желаемый заголовок темы и сам текст темы. После добавления получаем такую картину

Добавленным текстбоксам я дал имена THREADBOX и TEXTBOX

Так же я поменял цвет фона приложения (BackColor), чтобы было лучше всё видно

Теперь еще хотелось бы добавить небольшой лог работы программы, чтобы в случае проблемы, мы видели где накосячили.

Дабавляем ниже еще label и Textbox, Textbox я назову LOG.

Кстати забыл сказать, текстбоксу с текстом темы и текстбоксу с логом, ножно добавить мультистрочность, чтобы всё в одну строку не писалось, для этого просто нажимаем по текстбоксу, потом нажимаем по стрелочке в правом верхнем углу и ставим галочку MultiLine

Получилось вот так:

Один хрен чего-то не хватает… Бля точно, нужно же сделать кнопку, которая всё это будет запускать

Перекидываем с панели элементов элемент Button на нашу форму и растягиваем кнопку как нам хочется, называем кнопку как угодно, а в свойствах Разработка — Name пишем START

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

Побухали, поматерили меня — приступим.

Клакаем 2 раза по кнопке СТАРТ и начинаем «кодить»…

Если вы попали сюда, то уже всё заебись)

Нужно теперь подключить необходимые директивы, поднимаемся в самый верх кода и пишем туда следующее

using System;
using System.Windows.Forms;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.PhantomJS;
using OpenQA.Selenium;
using System.Threading;

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

private void START_Click(object sender, EventArgs e)
        {
            IWebDriver web; //Это мы... Как бы проще сказать... Короче инициализируем наш веб драйвер

            string LOGIN = LOGINBOX.Text;   //Читаем в переменную LOGIN введенный логин
            string PASS = PASSBOX.Text;     //Читаем в переменную PASS введенный логин
            string THREAD = THREADBOX.Text; //Читаем в переменную THREAD введенное название темы
            string TEXT = TEXTBOX.Text;     //Читаем в переменную TEXT введенный текст для темы

        }

Selenium Web Driver выполняет следующее действие, только когда страница загрузится на 100% иногда загрузка может занять очень долгое время из-за плохого интернета или просто из-за глюка, а например нужная вам кнопка уже давно появилась.

Чтобы избавиться от глюков и максимально ускорить быстродействие программы, нужно добавить следующую строку в код:

  web.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(20);

Эта команда ждет в течении 20 секунд, пока не появится нужный вам элемент (кнопка на сайте, поле для ввода или что-то другое) и выполняет указанное действие.

Как только нужный вам элемент появляется на странице, программа сразу приступит к следующему действию и не будет ждать пока страница загрузится на 100%

Осталось совсем не много, всё основное мы сделали.

Обернём весь код в try, чтобы не вылетали исключения

private void START_Click(object sender, EventArgs e)
        {
            IWebDriver web; //Это мы... Как бы проще сказать... Короче инициализируем наш веб драйвер

            string LOGIN = LOGINBOX.Text;   //Читаем в переменную LOGIN введенный логин
            string PASS = PASSBOX.Text;     //Читаем в переменную PASS введенный логин
            string THREAD = THREADBOX.Text; //Читаем в переменную THREAD введенное название темы
            string TEXT = TEXTBOX.Text;     //Читаем в переменную TEXT введенный текст для темы

            web = new ChromeDriver();       //В каком браузере будет работать наш бот, если лиса,
                                            //то нужно будет в NuGet скачать FireFox.WebDriver
                                            //и заменить эту строку на web = new FireFoxDriver();

            web.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(20);

            try
            {
                //Теперь весь код пишем только тут



            }
            catch (Exception)
            {

  
            }

        }

Вспомним своё ТЗ, первым делом нам нужно зайти на сайт

web.Navigate().GoToUrl("https://vlmi.su");  //Переходим по URL

Дальше будет сложнее, наш бот должен понимать, на какой элемент в коде страницы нужно нажать, для этого нужно установить расширение в хром.

Топаем сюда

https://chrome.google.com/webstore/category/extensions?hl=ru

В поиске вбиваем XPath и устанавливаем первое приложение XPath Helper.

Когда установилось расщирение, пора узнать какой «код» соответствует кнопке «Войти» на сайте VLMI.su

Переходим на VLMI.su -> Нажимаем кнопки CTRL + SHIFT + X и откроется откно хелпера. Теперь зажимаем правый шифт и мышкой наводим на нашу кнопку «Войти»

Нам хелпер даёт такой код:

/html[@id='XenForo']/body/div[@class='xbOffCanvasContainer']/div[@class='xbBoxed']/div[@id='headerMover']/header/fieldset[@id='moderatorBar']/div[@class='pageWidth']/div[@class='pageContent']/div/div[@id='right_nav']/ul[@class='publicTabs loginBar']/li[@class='login Popup PopupControl PopupClosed PopupContainerControl']/a[@class='logbt']

Но это что-то дохуя длинный, давайте сократим его методом тыка:

Я оставлю только самую последнюю часть кода и напишу её в хелпер через двойной слеш

//a[@class='logbt']

Как видим мне повезло и найден только один элемент с этим кодом

Это и есть код кнопки войти, вот по этому коду и нужно нажать, делается в Selenium это так

web.FindElement(By.XPath("//a[@class='logbt']")).Click();   //Нажать по кнопке ВОЙТИ

Давайте немного разберем этот код:

FindElement — это командой мы говорим программе, что нужно найти элемент

ByXpath — это по какому локатору мы ищем элемент, в данном случаем мы ищем по XPath. Существует 10 разных локаторов. Например локатор By.LinkText ищет ссылку, в которой есть указанный текст. Еще очень популярен CssSelector, тут в помощь идёт гугл.

.Click() указывает на то, что по найденому элементу нужно нажать

Дальше всё делаем по логике. Нажали кнопку, смотри что появилось. Ага, появились поля для ввода логина и пароля. Нужно значит ввести логин и пароль. Делается это точно так же как предыдущая команда:

находим XPath поля логина

//input[@id='ctrl_pageLogin_login']

и XPath поля пароля

//input[@id='ctrl_pageLogin_password']

Вводим логин:

web.FindElement(By.XPath("//input[@id='ctrl_pageLogin_login']")).SendKeys(LOGIN);   //Вводим логин из переменной LOGIN, а переменная LOGIN считывает то, что мы вручную ввели в программе

Так же вводим пароль:

web.FindElement(By.XPath("//input[@id='ctrl_pageLogin_password']")).SendKeys(PASS); //По такому же принципу вводим пароль

А теперь немного пиздец, я забыл про то, что мы еще собирались вести логи, но ничего страшного, это всё исправляется очень быстро. Нужно каждую команду обернуть в try и если будет ошибка, то писать это в лог. В итоге должно получиться вот так:

private void START_Click(object sender, EventArgs e)
        {
            IWebDriver web; //Это мы... Как бы проще сказать... Короче инициализируем наш веб драйвер

            string LOGIN = LOGINBOX.Text;   //Читаем в переменную LOGIN введенный логин
            string PASS = PASSBOX.Text;     //Читаем в переменную PASS введенный логин
            string THREAD = THREADBOX.Text; //Читаем в переменную THREAD введенное название темы
            string TEXT = TEXTBOX.Text;     //Читаем в переменную TEXT введенный текст для темы

            web = new ChromeDriver();       //В каком браузере будет работать наш бот, если лиса,
                                            //то нужно будет в NuGet скачать FireFox.WebDriver
                                            //и заменить эту строку на web = new FireFoxDriver();

            web.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(20);

            try
            {
                //Теперь весь код пишем только тут
                // + Environment.NewLine это означает перевести строку,
                //чтобы не писать всё в одну строчку

                try
                {
                    web.Navigate().GoToUrl("https://vlmi.su");  //Переходим по URL
                }
                catch (Exception)
                {
                    //Эта команда сработает только в том случае, если будет ошибка в блоке try
                    //и запишет текст ошибки в окно программы
                    LOG.AppendText("Ошибка. Не удалось перейти по URL" + Environment.NewLine);
                }

                try
                {
                    web.FindElement(By.XPath("//a[@class='logbt']")).Click();   //Нажать по кнопке ВОЙТИ
                }
                catch (Exception)
                {
                    //Эта команда сработает только в том случае, если будет ошибка в блоке try
                    //и запишет текст ошибки в окно программы
                    LOG.AppendText("Ошибка. Не удалось нажать на кнопку ВОЙТИ" + Environment.NewLine);
                }

                try
                {
                    web.FindElement(By.XPath("//input[@id='ctrl_pageLogin_login']")).SendKeys(LOGIN);   //Вводим логин из переменной LOGIN, а переменная LOGIN считывает то, что мы вручную ввели в программе
                }
                catch (Exception)
                {
                    //Эта команда сработает только в том случае, если будет ошибка в блоке try
                    //и запишет текст ошибки в окно программы
                    LOG.AppendText("Ошибка. Не удалось ввести логин" + Environment.NewLine);
                }

                try
                {
                    web.FindElement(By.XPath("//input[@id='ctrl_pageLogin_password']")).SendKeys(PASS); //По такому же принципу вводим пароль
                }
                catch (Exception)
                {
                    //Эта команда сработает только в том случае, если будет ошибка в блоке try
                    //и запишет текст ошибки в окно программы
                    LOG.AppendText("Ошибка. Не удалось ввести пароль" + Environment.NewLine);
                }



  
            }
            catch (Exception)
            {

  
            }

Теперь ошибок постараемся не делать и будем каждую команду обёртывать в try.

После ввода логина и пароля, хорошо бы нажать кнопку ВХОД, как это сделать вы уже должны уметь

try
                {
                    web.FindElement(By.XPath("//input[@class='button primary']")).Click();
                }
                catch (Exception)
                {
                    //Эта команда сработает только в том случае, если будет ошибка в блоке try
                    //и запишет текст ошибки в окно программы
                    LOG.AppendText("Ошибка. Не удалось нажать кнопку ВОЙТИ" + Environment.NewLine);
                }
                }

После попадания на форум видим что XPath мягко говоря хуёво справляется со своей задачей и при помощи него нам вываливает аж 40 вариантов…

Хуйня вопрос, вместо XPath будем использовать By.LinkText (Текст ссылки) в этом случает это годится лучше всего

try
                {
                    web.FindElement(By.LinkText("Флудилка"));
                }
                catch (Exception)
                {
                    //Эта команда сработает только в том случае, если будет ошибка в блоке try
                    //и запишет текст ошибки в окно программы
                    LOG.AppendText("Ошибка. Не нашли ФЛУДИЛКУ" + Environment.NewLine);

Теперь «охотимся» за кнопкой СОЗДАТЬ ТЕМУ. Как видим XPath и LinkText находят несколько вариантов. Тут можно пробовать использовать другие селекторы или как я ленивая жопа просто удлинить XPath

try
                {
                    web.FindElement(By.XPath("//div[@class='nodeListNewDiscussionButton']/a[@class='callToAction']")).Click();
                }
                catch (Exception)
                {
                    //Эта команда сработает только в том случае, если будет ошибка в блоке try
                    //и запишет текст ошибки в окно программы
                    LOG.AppendText("Ошибка. Не нашли кнопку СОЗДАТЬ ТЕМУ" + Environment.NewLine);
                }

Вводим название темы:

try
                {
                    web.FindElement(By.XPath("//iframe[@class='redactor_textCtrl redactor_MessageEditor redactor_BbCodeWysiwygEditor redactor_']")).SendKeys(TEXT);
                }
                catch (Exception)
                {
                    //Эта команда сработает только в том случае, если будет ошибка в блоке try
                    //и запишет текст ошибки в окно программы
                    LOG.AppendText("Ошибка. Не удалось ввести текст темы" + Environment.NewLine);
                }

Нажимаем кнопку СОЗДАТЬ ТЕМУ:

try
                {
                    web.FindElement(By.XPath("//dl[@class='ctrlUnit submitUnit'][1]/dd/input[@class='button primary']")).Click();
                }
                catch (Exception)
                {
                    //Эта команда сработает только в том случае, если будет ошибка в блоке try
                    //и запишет текст ошибки в окно программы
                    LOG.AppendText("Ошибка. Не удалось нажать кнопку создания темы" + Environment.NewLine);
                }

Вот и всё, самый простой бот готов, дальше можно использовать полученные знания для создания чего-то более сложного и под свои нужды

Бот работает немного криво, видимо пивко дало о себе знать.

Ниже прикреплю весь исходник, но думаю основное вы поняли. Если есть вопросы, то задавайте, попробую на них ответить.

=====================

Подписывайтесь на канал DarkD Сливы : https://telete.in/private_manual

Базы данных ФСБ/МВД (Сливы) : https://telete.in/joinchat/AAAAAFGLZjknYfJKHJRewA

Наш проект по накрутке в соц. сетях : https://telete.in/fastnakrutkaru

Подписывайте на канал DarkD-Shop : https://telete.in/dark_shop39

Подготовка

Этот туториал, и код в нем, требует установки нескольких дополнительных библиотек для Python. Они обеспечивают обертку Python’а в кусок низкоуровневого C-кода, который значительно упрощает создание и скорость исполнения.

Некоторые библиотеки существуют только под Windows. У них могут быть эквиваленты под Mac или linux, но мы не будем их рассматривать.

Вам нужно скачать и установить следующие библиотеки:

  • Python Imaging Library (PYL)
  • Numpy
  • PyWin

Все представленные библиотеки комплектуются установщиками. Запуск их автоматически установит модуль в директорию libsite-packages и, теоретически, добавит соответствующий pythonPath. Однако, на практике это происходит не всегда. Если Вы получите сообщение об ошибке после установки, добавьте их вручную в переменные Path.

Последний инструмент это графический редактор. Я предлагаю использовать Paint.Net как лучший из бесплатных, но подойдет любая программа с линейками и измерениями в пикеслях.

Мы будем использовать несколько игр в качестве примеров.

Введение

Это руководство написано с целью дать базовое понимание основы разработки ботов для браузерных игр. Подход, который мы собираемся дать, вероятно немного отличается от того, что многие ожидают услышать говоря о ботах. Вместо того, чтобы сделать программу, вставляющую код между клиентом и сервером (как боты для Quake или CS), наш бот будет находиться чисто снаружи. Мы будем опираться на методы Компьютерного зрения и вызовы Windows API для сбора необходимой информации и выполнения движений.

С этим подходом мы теряем часть деталей и контроля, но сокращаем время разработки и получим простоту в использовании. Автоматизирование специфичных игровых функций может быть создано в несколько строк кода, и полноценный бот, от начала до конца (для простой игры) может быть собран за несколько часов.

Когда вы привыкните к тому, что компьютер может видеть, начнете смотреть на игры по-другому. Хороший пример это поиск в играх-пазлах. Обычное решение основывается на ограничении скорости игрока, что заставляет принимать не оптимальные решения. Интересно (и довольно легко) «взломать» их скриптами движений которые не повторить человеку.

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

Исходники примеров из курса, а также одного законченного бота можно найти здесь.

Шаг 1: Создание проекта

В папке с проектом создайте текстовый файл quickGrab, измените расширение на ‘py’ и откройте его в редакторе кода.

Шаг 2: Создаем приложение, которое делает скриншот экрана

Начнем работу с изучения базовой функции, которая делает скриншот экрана. Один раз создав и запустив, мы будем строчка за строчкой, как эту функцию, создавать каркас нашего кода.

Вставим в наш файл с проектом quickGrab.py следующий код:

ImageGrab
import os
import time
 
def screenGrab():
    box = ()
    im = ImageGrab.grab()
    im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) + '.png', 'PNG')
 
def main():
    screenGrab()
 
if __name__ == '__main__':
    main()

Запустив этот код, вы получите скриншот экрана:

screenshot

Данный код забирает всю ширину и высоту области экрана и сохраняет в PNG файл в директорию проекта.

Давайте пошагово разберем код, чтобы понять как это работает. Первые три строки:

import ImageGrab
import os
import time

…называются ‘import statements’. Они говорят Pytjon’у какие модули загружать во время выполнения. Это дает доступ к методам этих модулей через синтаксис module.attribute .

Первый модуль Python Image Library мы установили ранее. Как следует из названия, он дает нам функциональность взаимодействия с экраном на которую ссылается бот.

Вторая строка импортирует модуль операционной системы (OS — operating system). Он дает возможность простой навигации по директориям в операционной системе. Это пригодится, когда мы начинаем размещать файлы в разных папках.

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

Следующие четыре строки определяют функцию screenGrab().

def screenGrab():
      box = ()
      im = ImageGrab.grab()
      im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) + '.png', 'PNG')

Первая строка def screenGrab() определяет имя функции. Пустые скобки означают, что она не принимает аргументов.

Строка 2, box = () присваивает пустое значение переменной «box». Мы заполним это значение дальше.

Строка 3, im = ImageGrab.grab() создает полный скриншот экрана и возвращает RGB изображение в переменную im .

Строка 4, может быть немного сложнее если вы не очень хорошо знакомы с тем как работает Time module. Первая часть im.save( вызывает метод «save». Он принимает два аргумента. Первый это директория в которую нужно сохранить файл, а второй это формат файла.

Здесь мы устанавливаем директорию вызовом метода os.getcwd() . Функция получает текущую директорию в которой выполняется код и возвращает её как строку. Далее мы добавим «+». Сложение нужно использовать между каждым новым аргументом для соединения всех строк вместе.

Следующая часть '\full_snap__ дает нам простое описание в имени файла. Обратный слеш является экранирующим символом в Python, и мы добавили два, чтобы избежать отмены одного из символов.

Далее идет эта сложная конструкция: str(int(time.time())). Она использует встроенные функции Питона. Мы рассмотрим работу этого куска кода изнутри:

time.time() возвращает количество секунд с начала Эпохи, тип данных Float (число с плавающей точкой). Так как мы используем дату для именования файлов, мы не можем использовать десятичное число, поэтому мы обернем выражение в int(), чтобы конвертировать в целое число (Integer). Это делает нас ближе к решению, но Python не может соединить тип Integer с типом String, поэтому следующим шагом мы обернем все в функцию str(). Далее остается только добавить расширение как часть строки + '.png' и добавить вторым аргументом функции снова расширение: «PNG».

Последняя часть кода определяет функцию main(), которая вызывает функцию screenGrab(), когда исполняется.

И, наконец, условное обозначение, которое позволяет интерпретатору определить, какую функцию запускать в случае, если файл запущен как основной (а не просто импортирован в другой скрипт). В противном случае, если он загружен как модуль другого скрипта — он только передает доступ к этому методу, вместо выполнения.

def main():
      screenGrab()
   
  if __name__ == '__main__':
      main()

Шаг 3: Область видимости

Функция ImageGrab.grab() принимает один аргумент, который определяет область видимости. Это набор координат по шаблону (x,y,x,y), где

  1. Первая пара значение (x,y… определяет левый верхний угол рамки;
  2. Вторая пара …x,y) определяет правый нижний.

Это дает нам возможность скопировать только часть экрана, которая нам нужна.

Рассмотрим это на практике.

Для примера рассмотрим игру Sushi Go Round (Довольно увлекательная. Я Вас предупредил). Откройте игру в новой вкладке и сделайте скриншот использую существующий код screenGrab():

screenshot

Шаг 4: Задание координат

Пришло время задания координат для нашей области видимости.

Откройте скриншот в редакторе картинок.

Координаты (0,0) это всегда левый верхний угол изображения. Мы хотим заполнить X и Y таким образом, чтобы нашему новому скриншоту функция установила координаты (0,0) в крайний левый угол игровой области.

Этому есть две причины. Во-первых, это упрощает нахождение координат, когда мы должны определять координаты относительно игровой области, по сравнению со всем экраном монитора. Во-вторых, захват меньшей части экрана уменьшает нагрузку на процессор. Полноэкранные скриншоты производят довольно много данных, чтобы их можно было циклично повторять несколько раз в секунду.

координаты экрана

Если вы это еще не сделали, включите линейки в вашем графическом редакторе и приблизьте верхний угол игровой области до той степени, пока не увидите рамки пикселей.

Наведите курсор на первый пиксель игровой области и запишите координаты на линейках. Это будут первые два значения для нашей функции. У меня получились значения (305, 243).

Затем следуйте к нижнему краю и запишите вторую пару координат. У меня получилось (945, 723). Вместе эти пары дают область с координатами (305,243,945,723).

Давайте добавим координаты в код:

import ImageGrab
  import os
  import time
   
  def screenGrab():
      box = (305,243,945,723)
      im = ImageGrab.grab(box)
      im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) +
  '.png', 'PNG')
   
  def main():
      screenGrab()
   
  if __name__ == '__main__':
      main()

На строке 6 мы обновили массив для хранения координат игровой области.

Сохраните и запустите код. Откройте новое сохраненное изображение и вы увидите следующее:

Отлично! Это идеальный снимок игровой области. Нам не всегда будет требоваться эта напряженная охота за координатами. После того, как мы узнаем о win32api, мы рассмотрим более быстрые методы для установки координат, когда нам нужна идеальная точность.

Шаг 5: Перспективное планирование для гибкости

На данный момент, мы жестко прописали координаты по отношению к текущей настройке, исходя из нашего браузера и нашего разрешения монитора. Вообще, это плохая идея — жестко прописывать координаты таким образом. Если, например, мы хотим запустить код на другом компьютере, или, скажем, что-то на сайте немного сдвинет позицию игровой области — нам придется вручную скрупулезно фиксировать все наши координаты заново.

Давайте создадим две новые переменные: x_pad и y_pad. В них будет храниться расстояние между игровой областью и остальным экраном. Это поможет легко портировать код с места на место, так как каждая новая координата будет задаваться относительно двух глобальных переменных, которые мы создадим. Чтобы настроить изменения экрана нужно сбросить эти две переменные.

Так как мы уже сделали измерения, установить отступы для нашей текущей системы достаточно просто. Мы собираемся установить отступы, чтобы хранить положение первого пикселя за пределами игровой площадки. От первой пары координат нашего кортежа вычесть по 1. Получается 304 и 242.

Давайте добавим это в наш код:

# Globals
# ------------------
 
x_pad = 304
y_pad = 242

Теперь, когда они установлены, мы скорректируем координаты игровой области относительно них.

def screenGrab():
    box = (x_pad+1, y_pad+1,945,723)
    im = ImageGrab.grab(box)
    im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) + '.png', 'PNG')

Для второй пары значений мы собираемся сначала найти разницу между первой и второй парой координат, чтобы получить размер игрового окна, а затем использовать эти значения с нашими переменными.

def screenGrab():
    box = (x_pad+1, y_pad+1, x_pad+641, y_pad+481)
    im = ImageGrab.grab(box)
    im.save(os.getcwd() + '\full_snap__' + str(int(time.time())) + '.png', 'PNG')

Для координаты x значение стало 945 — 304 = 641, а для y стало 723 — 242 = 481.

Сперва это может показаться излишним, но это дополнительный шаг к более легкому обслуживанию в будущем.

Шаг 6: Создание документации

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

Напоследок, нужно следить за постоянно меняющимся рекламным пространством на популярных игровых сайтах. Если функция захвата экрана перестает себя вести, как ожидалось, стоит добавить координатам немного смещения.

Для примера, я обычно добавляю подобный комментарий в начало моего кода:

"""
 
All coordinates assume a screen resolution of 1280x1024, and Chrome 
maximized with the Bookmarks Toolbar enabled.
Down key has been hit 4 times to center play area in browser.
x_pad = 156
y_pad = 345
Play area =  x_pad+1, y_pad+1, 796, 825
"""

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

Шаг 7: Делаем quickGrab.py удобным инструментом

Мы собираемся клонировать наш проект в этой точке, создавая два файла: один, чтобы писать код нашего бота, другой чтобы хранить функцию снимка экрана. Мы будем делать много снимков, поэтому с отдельным модулем работа пойдет быстрее.

Сохраните и закройте текущий проект.

Создайте копию проекта в этой же папке и переименуйте файл в code.py. Теперь добавление и редактирование всех изменений мы будем производить в code.py, а quickGrab.py оставим исключительно для скриншотов. Только добавим одно финальное изменение: изменим расширение на .pyw

Это расширение сообщает Питону, что нужно запускать скрипт без открытия консоли. Двойной клик по файлу и он быстро выполнится в фоне и сохранит скриншот в рабочую директорию.

Держите игру открытой в фоне (не забудьте отключить зацикленную музыку, иначе она сведет вас с ума); мы скоро к ней вернемся. У нас еще есть несколько инструментов для внедрения, прежде чем начать контролировать вещи на экране.

Шаг 8: win32api — краткий обзор

Работать с win32api может быть немного сложно на начальном этапе. Это обертка в низкоуровневый Windows C, которых хорошо задокументирован здесь, но навигация похожа на лабиринт, так что пару раз придется пройти по кругу.

Если при выполнении Вы видите ошибку «ImportError: No module named win32api», значит не установлен этот модуль. Выполните в консоли команду pip install pypiwin32

Прежде чем мы начнем писать код, давайте поближе познакомимся с некоторыми функциями API на которые далее мы будем опираться. После того как у нас появится четкое понимание каждого параметра, мы сможем легко настроить их для наших целей в игре.

win32api.mouse_event():

win32api.mouse_event(
    dwFlags,
    dx,
    dy,
    dwData  
    )

Первый параметр dwFlags определяет «действия» мыши. Такие как перемещение, клик, скроллинг и т.п. Следующий список показывает распространенные параметры, используемые для программирования движений.

  • win32con.MOUSEEVENTF_LEFTDOWN
  • win32con.MOUSEEVENTF_LEFTUP
  • win32con.MOUSEEVENTF_MIDDLEDOWN
  • win32con.MOUSEEVENTF_MIDDLEUP
  • win32con.MOUSEEVENTF_RIGHTDOWN
  • win32con.MOUSEEVENTF_RIGHTUP
  • win32con.MOUSEEVENTF_WHEEL

Имена говорят сами за себя. Если вы хотите выполнить виртуальный правый клик, нужно отправить параметр win32con.MOUSEEVENTF_RIGHTDOWN в dwFlags.

Следующие два параметра, dx и dy, описывают абсолютную позицию вдоль осей x и y. Пока мы будем использовать эти параметры для программирования движения мыши, они будут использовать систему координат отличную от той, которую мы использовали до этого. Мы зададим нули и будем опираться на другую часть API для движения мыши.

Четвертый параметр это dwData. Эта функция используется тогда и только тогда, когда dwFlags содержит MOUSEEVENTF_WHEEL. В других случаях она может быть опущена или установлена в 0. dwData скорость прокрутки колеса мыши.

Простой пример для закрепления

Если мы представим игру с переключением оружия как в Half-Life 2 (где оружие может быть выбрано вращением колеса) — мы можем использовать эту функцию для выбора оружия из списка:

def browseWeapons():
    weaponList = ['crowbar','gravity gun','pistol'...]
    for i in weaponList:    
        win32api.mouse_event(win32con.MOUSEEVENTF_MOUSEEVENTF_WHEEL,0,0,120)

Здесь мы хотим симулировать скроллинг колеса мыши для навигации по нашему теоретическому списку оружия, поэтому мы вносим ...MOUSEEVENTF_WHEEL в dwFlag. Не нужно указывать позиционирование dx и dy, оставим эти значения 0, и нам нужен один скрол вперед для каждого оружия в списке, поэтому устанавливаем значение 120 для dwData, что соответствует одному скролу мыши.

Как вы видите, работа с mouse_event это просто вопрос подключения правильных аргументов в правильном порядке. Давайте перейдем к более полезным функциям.

Шаг 9: Клики мыши

Мы переходим к созданию трех новых функций. Одна общая функция нажатия левой кнопки мыши, и два обработчика состояний нажатия и отпускания.

Откройте code.py в редакторе и добавьте следующее выражение к списку импортов

import win32api, win32con

Как и ранее, это дает нам доступ к содержимому модуля через синтаксис module.attribute

Далее создадим первую функцию клика мыши

def leftClick():
      win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,0,0)
      time.sleep(.1)
      win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,0,0)
      print "Click."          #completely optional. But nice for debugging purposes.

Напомню, что все, что мы делаем здесь это назначаем действие первому аргументу mouse_event. Мы не должны указывать никакую информацию о позиционировании, поэтому мы опускаем параметры координат (0,0), и мы не должны указывать дополнительную информацию, такую как dwData. Функция time.sleep(.1) говорит Питону приостановить выполнение на время указанное в скобках. Добавим это в наш код. Обычно это очень короткий промежуток времени. Без этого клик может получиться до того, как меню обновится.

Мы создали функцию для левого клика. Один раз нажать, один раз отпустить. Мы потратили много времени на нее, но давайте создадим еще две вариации. Это тоже самое, но теперь каждый шаг мы разобьем на отдельную функцию. Это можно использовать когда нам надо удерживать нажатой мышь в течение продолжительного времени (например, для перетаскивания предметов или стрельбы.)

def leftDown():
    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,0,0)
    time.sleep(.1)
    print 'left Down'
         
def leftUp():
    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,0,0)
    time.sleep(.1)
    print 'left release'

Шаг 10: Простые движения мышью

Все, что остается это движение мыши по экрану. Добавим следующие функции в файл code.py

def mousePos(cord):
    win32api.SetCursorPos((x_pad + cord[0], y_pad + cord[1])
     
def get_cords():
    x,y = win32api.GetCursorPos()
    x = x - x_pad
    y = y - y_pad
    print x,y

Эти две функции служат совершенно разным целям. Первая будет использоваться для задания движения в программе. Благодаря соглашению об именовании, тело функции делает именно то, что обозначает название SetCursorPos(). Вызов этой функции устанавливает координаты мыши по заданным (x,y). Обратите внимание, что мы добавили поправки x_pad и y_pad к координатам; это важно делать там, где координаты объявляются.

Вторая функция это простой инструмент который мы будем использовать в интерактивном режиме. Он выводит в консоль координаты текущей позиции мыши. Это сильно ускоряет процесс навигации в меню без необходимости делать скриншот и пользоваться линейками. Мы не хотим постоянно использовать эту функцию, так как некоторая активность мыши будет требовать точного позиционирования, но там где это возможно, это хорошо сэкономит нам время.

На следующем шаге мы применим эти техники для начала навигации в игровом меню. Но сначала надо удалить текущий контент у main() в сode.py и заменить его на тот, который мы написали. На следующем шаге мы будем работать с интерактивным режимом , поэтому функция screenGrab() нам пока не понадобиться.

Шаг 11: Навигация в меню

В этом и в следующих нескольких этапах мы попытаемся собрать координаты для разных событий используя метод get_cords(). Используя его мы сможем быстро построить код для таких вещей как навигация по меню, очистка столов, приготовление еды. После сбора мы встроим их в логику бота.

Давайте начнем. Сохраните и запустите код в Python Shell. С тех пор как на прошлом шаге мы заменили тело функции main()на pass, вы должны увидеть пустое окно после запуска Shell.

Теперь прежде чем перейти к игровой части, нужно пройти 4 начальных меню.

  1. Кнопка «Play»
  2. Кнопка «continue»
  3. Пропустить обучение «skip»
  4. Кнопка «continue»

Мы должны получить координаты каждой кнопки и добавить их в новую функцию startGame(). Расположите на экране Shell так, чтобы была видна игровая область. Нужно поставить мышь на кнопку, координаты которой нужно получить и выполнить в Shell функцию get_cords(). Убедитесь, что активным окном является Shell. Мы получим в Shell’е координаты текущей позиции мыши. Повторите это для остальных трех окон.

Оставьте Shell открытым и настройте экран так, чтобы видеть IDLE редактор. Добавим функцию startGame() и заполним новыми координатами.

def startGame():
    #location of first menu
    mousePos((182, 225))
    leftClick()
    time.sleep(.1)
     
    #location of second menu
    mousePos((193, 410))
    leftClick()
    time.sleep(.1)
     
    #location of third menu
    mousePos((435, 470))
    leftClick()
    time.sleep(.1)
     
    #location of fourth menu
    mousePos((167, 403))
    leftClick()
    time.sleep(.1)

Теперь у нас есть компактная функция, которую можно вызывать на старте каждой игры. Она устанавливает курсор на каждую позицию в меню, которую мы заранее определили и кликает. time.sleep(.1) говорит Питону остановить выполнение на 1/10 секунды между каждым кликом, чтобы меню успевало обновляться между кликами. Сохраните и запустите код.

У меня, как у медленного человека, прохождение меню вручную занимает больше секунды, тогда как наш бот может сделать это в течение примерно 0,4 секунд. Совсем неплохо!

Шаг 12: Зададим координаты еды

Давайте повторим процесс для каждой кнопки.

меню с едой

С помощью get_cords(), соберите координаты еды из меню. Еще раз в Python Shell напишите get_cords(), наведите мышь на еду и выполните команду.

Как вариант, для ускорения работы, если у вас есть второй монитор или вы можете организовать расположение таким образом, чтобы видеть браузер и окно Shell, можно не вводить каждый раз get_cords(), а сделать простой цикл for. Используйте метод time.sleep() чтобы успевать перемещать мышь между итерациями цикла.

for i in range(6):
    time.sleep(1.2)
    print get_cords()

Нам нужно создать новый класс Cord, чтобы хранить в нем собранные координаты. Возможность вызова через Cord.f_rice дает большие преимущества, так как можно передававть координаты прямо в mousePos(). Как вариант, можно хранить всё в словарях, но я нахожу синтаксис классов более удобным.

class Cord:
     
    f_shrimp = (40,340)
    f_rice = (90, 340)
    f_nori = (40, 387)
    f_roe = (90, 387)
    f_salmon = (40, 440)
    f_unagi = (90, 440)

Мы будем хранить много наших координат в этом классе, там будет некоторое дублирование, поэтому добавив префикс ‘f_’ мы будем знать, что это ссылка на еду, а не, скажем, на заказ еды по телефону.

Продолжим добавлять координаты.

Шаг 13: Координаты пустых мест

Каждый раз после еды посетители оставляют пустые тарелки, на которые нужно кликать, чтобы убрать. Поэтому нам нужно знать расположение тарелок.

Повторите действия из предыдущих шагов, чтобы получить координаты тарелок. Сохраните их пока в закомментированной строке.

"""
 
Plate cords:
 
    87, 208
    194, 208
    288, 208
    395, 208
    491, 208
    591, 208

Осталось всего несколько шагов до действительно интересных штук.

Шаг 14: Координаты телефона

Итак, это будет последняя часть координат. Здесь понадобиться намного больше действий, поэтому вы можете сделать это вручную функцией get_cords(), вместо цикла. Мы собираемся прокликать всё меню телефона, чтобы получить координаты каждого пункта.

Здесь есть сложности, так как нам нужно иметь достаточно денег, чтобы купить что-то. Поэтому нам нужно сделать несколько порций суши, прежде чем вбивать координаты телефонных сделок. Нам придется сделать два суши рола , чтобы купить немного риса.

Есть шесть меню, через которые нам надо пройти.

  1. Телефон

  2. Начальное меню

  3. Начинки

  4. Рис

  5. Доставка

Нам нужно получить координаты всех, кроме Саке (можете её тоже добавить, если хотите. На мой взгляд бот отлично работает и без нее).

class Cord:
     
    f_shrimp = (40,340)
    f_rice = (90, 340)
    f_nori = (40, 387)
    f_roe = (90, 387)
    f_salmon = (40, 440)
    f_unagi = (90, 440)
     
#-----------------------------------    
     
    phone = (574, 358)
 
    menu_toppings = (528, 273)
     
    t_shrimp = (495, 224)
    t_nori = (490, 287)
    t_roe = (574, 279)
    t_salmon = (494, 340)
    t_unagi = (576, 218)
    t_exit = (591, 342)
 
    menu_rice = (532, 293)
    buy_rice = (548, 281)
     
    delivery_norm = (490, 292)

Окей! Мы наконец собрали все необходимые координаты. Давайте создадим что-нибудь полезное!

Шаг 15: Убираем со стола

Мы используем координаты собранные ранее для создания функции clear_tables().

def clear_tables():
    mousePos((87, 208))
    leftClick()
 
    mousePos((194, 208))
    leftClick()
 
    mousePos((288, 208))
    leftClick()
 
    mousePos((395, 208))
    leftClick()
 
    mousePos((491, 208))
    leftClick()
 
    mousePos((591, 208))
    leftClick()
    time.sleep(1)

Как вы можете видеть, это выглядит более менее похоже на нашу прошлую функцию startGame(). С одним небольшим отличием: нет функции time.sleep() между кликами. Нам не нужно ждать обновления меню, поэтому не нужно ждать задержку между кликами.

Однако, у нас есть функция time.sleep() в самом конце. Хотя это и не обязательно, лучше добавить паузы в выполнение кода, чтобы была возможность вручную завершить цикл, если это потребуется. В противном случае скрипт будет менять позицию мыши снова и снова и вы не будете в состоянии переместить фокус на Shell, чтобы остановить сценарий. Это прикольно первые два или три раза, но быстро теряет свое очарование.

Шаг 16: Создание суши

Сначала нужно понять как сделать суши. Кликните на книгу рецептов, чтобы открыть инструкции. Все виды суши, встречающиеся в игре, могут быть найдены на страницах этой книги. Оставлю первые три ниже, остальные вы можете найти в книге по ходу игры.

'''
Recipes:
 
    onigiri
        2 rice, 1 nori
     
    caliroll:
        1 rice, 1 nori, 1 roe
         
    gunkan:
        1 rice, 1 nori, 2 roe
'''

Теперь давайте создадим функцию, которая будет принимать в виде аргумента тип суши и затем собирать требуемые ингредиенты для переданного значения.

def makeFood(food):
    if food == 'caliroll':
        print 'Making a caliroll'
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_nori)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_roe)
        leftClick()
        time.sleep(.1)
        foldMat()
        time.sleep(1.5)
     
    elif food == 'onigiri':
        print 'Making a onigiri'
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_nori)
        leftClick()
        time.sleep(.1)
        foldMat()
        time.sleep(.05)
         
        time.sleep(1.5)
 
    elif food == 'gunkan':
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_nori)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_roe)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_roe)
        leftClick()
        time.sleep(.1)
        foldMat()
        time.sleep(1.5)

Эта функция такая же как и все остальные, но с одним небольшим отличием: мы не передаем координаты непосредственно, а вызываем их в качестве атрибутов класса Cord.

Функция foldMat() вызывается в конце каждого приготовления. Она кликает по циновке, чтобы завернуть суши, которое мы приготовили. Давайте зададим её:

def foldMat():
    mousePos((Cord.f_rice[0]+40,Cord.f_rice[1])) 
    leftClick()
    time.sleep(.1)

Давайте кратко пройдемся по функции mousePos(). Мы обращаемся к первому значению f_rice через добавление [0] в конце атрибута. Напомню, что это координата x. Для клика по циновке нам необходимо добавить небольшое смещение по оси х, и мы прибавим 40 к значению координаты x и передадим f_price[1] в y. Это сдвинет нашу позицию по x достаточно, чтобы активировать циновку.

Обратите внимание, что после вызова foldMat() у нас идет длинный time.sleep(). Циновка будет закручиваться и ингредиенты будут не доступны для клика, поэтому придется подождать конца анимации.

Шаг 17: Навигация в телефонном меню

В этом шаге мы зададим все точки mousePos() для подходящий пунктов меню, координаты которых были оставлены для этого момента. Эта часть программы которая будет обернута и контролируема логикой бота. Мы вернемся к этой функции, после того как обзаведемся несколькими новыми техниками.

def buyFood(food):
     
    mousePos(Cord.phone)
     
    mousePos(Cord.menu_toppings)
     
    mousePos(Cord.t_shrimp)
    mousePos(Cord.t_nori)
    mousePos(Cord.t_roe)
    mousePos(Cord.t_salmon)
    mousePos(Cord.t_unagi)
    mousePos(Cord.t_exit)
     
    mousePos(Cord.menu_rice)
    mousePos(Cord.buy_rice)
     
    mousePos(Cord.delivery_norm)

Краткое введение в компьютерное зрение

Сейчас в нашем распоряжении имеются очень интересные куски кода. Давайте рассмотрим как научить компьютер «видеть» события. Это очень увлекательная часть процесса.

Еще одна сторона построения бота заключается в том, что в конечном итоге бот может предоставить нам достаточное количество информации, что дальнейшем облегчит работу. Например, в случае суши-бота, как только мы пройдем первый уровень, бот будет предоставлять нам достаточное количество данных о том, что происходит на экране, и все что нам останется сделать это объяснить ему как реагировать на эти новые данные.

Другая большая часть в написании бота это обучение игре. Понимание какие значения нужно отслеживать в игре, а какие можно игнорировать. Например, можно не отслеживать деньги в кассе. Это то, что в конечном счете не имеет отношения к боту. Все, что ему надо знать это достаточно ли еды, чтобы продолжать работать. Таким образом, вместо того, чтобы мониторить количество денег, он просто проверяет, может ли бот что-то купить независимо от цены, потому что в игре это вопрос лишь нескольких секунд. Поэтому если бот не может что-то купить, он просто ждет несколько секунд.

Это подводит нас к финальной точке. Это брутфорс против элегантного решения. Алгоритмы зрения требуют значительного процессорного времени. Проверка нескольких точек во многих разных областях игровой области могут сильно снижать производительность. Таким образом все сводится к вопросу «должен бот узнать о том что что-то случилось или нет?»

Например, посетитель может проходить через четыре состояния: отсутствует, ожидает, ест и закончил есть. Когда закончил есть, он оставляет пустую тарелку. Мы могли бы затратить ресурсы на проверку всех мест просмотрев их, а затем кликнуть напротив ожидаемой тарелки (что может приводить к ошибкам, так как тарелки мигают, давая ложный сигнал). Или можно убирать их простым перебором, прокликивая все места через каждые несколько секунд. На практике прокликивание оказывается таким же эффективным, как и элегантное решение, позволяющее определить состояние клиента. Прокликать шесть мест занимает доли секунды, в то время как захват и обработка шести изображений сравнительно медленный способ. Мы можем использовать сэкономленное время для других более важных задач, связанных с обработкой изображений.

Шаг 18: Импорт библиотек Numpy и ImageOps

Добавим следующие выражения импорта

import ImageOps
from numpy import *

ImageOps это еще одна библиотека для работы с изображениями Python’а. Она используется для выполнения операций над изображениями (таких как перевод в черно-белый формат).

Я кратко поясню вторую строку для тех, кто близко не знаком с Питоном. Стандартное выражение импорта загружает пространство имен модуля (коллекцию имен переменных и функций). Таким образом, для доступа к элементам из области видимости мы используем синтаксис module.attribute. Однако, используя выражение from _ import мы наследуем имена в локальную область видимости. То есть синтаксис module.attribute больше не нужен. Это не верхний уровень, поэтому мы можем использовать их как встроенные функции Питона, как str() или list(). Импорт Numpy таким способом дает нам возможность просто вызвать array() вместо numpy.array().

Символ * означает импорт всего из модуля.

Шаг 19: Создаем компьютерное зрение

Первый метод сравнивает значения RGB у пикселей с ожидаемым значением. Этот метод отлично подходит для статичных объектов, таких как меню. Так как нужно иметь дело с конкретными пикселями, то метод не слишком надежен для движущихся объектов. Однако, это варьируется от случая к случаю. Иногда это отличная техника, а в другой раз приходится искать другой метод.

Запустите Sushi Go Round в браузере и начните новую игру. Откройте телефонное меню. Можете пока не обращать внимание на посетителей. Вы начинаете игру без денег, поэтому все пункты меню будут серыми как показано ниже. Это и будут те значения RGB, которые мы будем проверять.

В code.py перейдите к функции screenGrub(). Нужно внести следующие изменения:

def screenGrab():
    b1 = (x_pad + 1,y_pad+1,x_pad+640,y_pad+480)
    im = ImageGrab.grab(b1)
 
    ##im.save(os.getcwd() + '\Snap__' + str(int(time.time())) +'.png', 'PNG')
    return im

Мы сделали два небольших изменения. Во—первых, мы закомментировали строку, которая сохраняет скриншот. Во—вторых, на шестой строке мы возвращаем объект с изображением после выполнения функции.

Сохраните и запустите код.

Во время того как открыто телефонное меню и все пункты серые, выполните следующий код:

>>>im = screenGrab()
>>>

Это сохранит скриншот, который мы сделали с помощью функции screenGrub() в переменную im. Теперь мы можем вызвать функцию getpixel(xy) чтобы получить данные о заданных пикселях.

Теперь нам надо получить RGB значения для каждого серого элемента меню. Они нужны для сравнения со значениями, которые бот будет сравнивать с теми, которые он будет получать вызывая getpixel().

Мы же имеем необходимые координаты после выполнения предыдущих шагов, поэтому просто передадим их в качестве аргументов в функцию getpixel() и запишем результаты.

>>> im = screenGrab()
  >>> im.getpixel(Cord.t_nori)
  (109, 123, 127)
  >>> im.getpixel(Cord.t_roe)
  (127, 61, 0)
  >>> im.getpixel(Cord.t_salmon)
  (127, 71, 47)
  >>> im.getpixel(Cord.t_shrimp)
  (127, 102, 90)
  >>> im.getpixel(Cord.t_unagi)
  (94, 49, 8)
  >>> im = screenGrab()
  >>> im.getpixel(Cord.buy_rice)
  (127, 127, 127)
  >>>

Если мы добавим эти значения в функцию buyFood(), чтобы знать доступна покупка в данный момент или нет.

def buyFood(food):
     
    if food == 'rice':
        mousePos(Cord.phone)
        time.sleep(.1)
        leftClick()
        mousePos(Cord.menu_rice)
        time.sleep(.05)
        leftClick()
        s = screenGrab()
        if s.getpixel(Cord.buy_rice) != (109, 123, 127):
            print 'rice is available'
            mousePos(Cord.buy_rice)
            time.sleep(.1)
            leftClick()
            mousePos(Cord.delivery_norm)
            time.sleep(.1)
            leftClick()
            time.sleep(2.5)
        else:
            print 'rice is NOT available'
            mousePos(Cord.t_exit)
            leftClick()
            time.sleep(1)
            buyFood(food)
             
 
             
    if food == 'nori':
        mousePos(Cord.phone)
        time.sleep(.1)
        leftClick()
        mousePos(Cord.menu_toppings)
        time.sleep(.05)
        leftClick()
        s = screenGrab()
        print 'test'
        time.sleep(.1)
        if s.getpixel(Cord.t_nori) != (109, 123, 127):
            print 'nori is available'
            mousePos(Cord.t_nori)
            time.sleep(.1)
            leftClick()
            mousePos(Cord.delivery_norm)
            time.sleep(.1)
            leftClick()
            time.sleep(2.5)
        else:
            print 'nori is NOT available'
            mousePos(Cord.t_exit)
            leftClick()
            time.sleep(1)
            buyFood(food)
 
    if food == 'roe':
        mousePos(Cord.phone)
        time.sleep(.1)
        leftClick()
        mousePos(Cord.menu_toppings)
        time.sleep(.05)
        leftClick()
        s = screenGrab()
         
        time.sleep(.1)
        if s.getpixel(Cord.t_roe) != (127, 61, 0):
            print 'roe is available'
            mousePos(Cord.t_roe)
            time.sleep(.1)
            leftClick()
            mousePos(Cord.delivery_norm)
            time.sleep(.1)
            leftClick()
            time.sleep(2.5)
        else:
            print 'roe is NOT available'
            mousePos(Cord.t_exit)
            leftClick()
            time.sleep(1)
            buyFood(food)

Мы передаем имя ингредиента в функцию buyFood(). Серия выражений if/else получает переданный параметр и дает соответствующий ответ. Каждая ветка следует одинаковой логике, поэтому мы рассмотрим только одну.

if food == 'rice':
     mousePos(Cord.phone)
     time.sleep(.1)
     leftClick()
     mousePos(Cord.menu_rice)
     time.sleep(.05)
     leftClick()
     s = screenGrab()
     time.sleep(.1)

Первое, что мы должны сделать это кликнуть на телефон и открыть нужное меню. В этом случае меню с рисом.

s = screenGrab()
if s.getpixel(Cord.buy_rice) != (109, 123, 127):

Далее мы делаем скриншот области и вызываем getpixel(), чтобы получить RGB-значение пикселя у координат Cord.buy_rice. Мы сравниваем их с RGB-значением пикселя, полученного до этого на неактивном элементе. Если мы получаем в результате сравнения значение True, значит кнопка больше не серая и у нас достаточно денег для покупки. Соответственно, если получаем False, значит не можем себе это позволить.

print 'rice is available'
mousePos(Cord.buy_rice)
time.sleep(.1)
leftClick()
mousePos(Cord.delivery_norm)
time.sleep(.1)
leftClick()
time.sleep(2.5)

Если у нас достаточно денег, мы просто проходим через оставшиеся этапы, необходимые для покупки.

else:
    print 'rice is NOT available'
    mousePos(Cord.t_exit)
    leftClick()
    time.sleep(1)
    buyFood(food)

Наконец, если нам не хватает денег, то мы говорим боту закрыть меню, подождать секунду и повторить все сначала. Обычно это вопрос секунд, когда нам снова становится доступна покупка. Довольно просто добавить дополнительную логику, чтобы бот мог решить, нужно ли продолжать ждать или заняться в это время чем-то полезным и вернуться позже.

Шаг 20: Следим за ингредиентами

Окей, до этого момента мы медленно продвигались шаг за шагом. Перепишем часть нашего кода, точнее внешний объект, обеспечивающий входящие данные и принятие решений посредством логики, которая может запускаться сама.

Мы должны придумать способ хранить информацию как много ингредиентов у нас сейчас на руках. Мы будем запрашивать экран в определенной области, или усредняя каждую ячейку ингредиента (к этой технике мы вернемся позже), но в дальнейшем, простой и наиболее быстрый способ просто хранить все элементы в словаре.

Количество каждого ингредиента остается постоянным на старте каждого уровня. Всегда начинаем с 10 обычных ингредиентов (рис, нори, икра), и по 5 дефицитных ингредиентов (креветки, лосось, угорь).

Начальные ингредиенты

Давайте добавим информацию о них в массив.

foodOnHand = {'shrimp':5,
              'rice':10,
              'nori':10,
              'roe':10,
              'salmon':5,
              'unagi':5}

Ключи массива содержат имена ингредиентов, по ним мы получаем доступ к значениям.

Шаг 21: Добавим отслеживание продуктов в код

Каждый раз, когда мы готовим, мы расходуем ингредиенты. И также пополняем их, когда делаем покупки. Давайте немного расширим нашу функцию makeFood()

def makeFood(food):
    if food == 'caliroll':
        print 'Making a caliroll'
        foodOnHand['rice'] -= 1
        foodOnHand['nori'] -= 1
        foodOnHand['roe'] -= 1 
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_nori)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_roe)
        leftClick()
        time.sleep(.1)
        foldMat()
        time.sleep(1.5)
     
    elif food == 'onigiri':
        print 'Making a onigiri'
        foodOnHand['rice'] -= 2 
        foodOnHand['nori'] -= 1 
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_nori)
        leftClick()
        time.sleep(.1)
        foldMat()
        time.sleep(.05)
         
        time.sleep(1.5)
 
    elif food == 'gunkan':
        print 'Making a gunkan'
        foodOnHand['rice'] -= 1 
        foodOnHand['nori'] -= 1 
        foodOnHand['roe'] -= 2 
        mousePos(Cord.f_rice)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_nori)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_roe)
        leftClick()
        time.sleep(.05)
        mousePos(Cord.f_roe)
        leftClick()
        time.sleep(.1)
        foldMat()
        time.sleep(1.5)

Теперь каждый раз при приготовлении Суши, мы уменьшаем значения наших ингредиентов на соответствующие значения. Теперь дополним код в функции buyFood()

def buyFood(food):
     
    if food == 'rice':
        mousePos(Cord.phone)
        time.sleep(.1)
        leftClick()
        mousePos(Cord.menu_rice)
        time.sleep(.05)
        leftClick()
        s = screenGrab()
        print 'test'
        time.sleep(.1)
        if s.getpixel(Cord.buy_rice) != (127, 127, 127):
            print 'rice is available'
            mousePos(Cord.buy_rice)
            time.sleep(.1)
            leftClick()
            mousePos(Cord.delivery_norm)
            foodOnHand['rice'] += 10     
            time.sleep(.1)
            leftClick()
            time.sleep(2.5)
        else:
            print 'rice is NOT available'
            mousePos(Cord.t_exit)
            leftClick()
            time.sleep(1)
            buyFood(food)
             
    if food == 'nori':
        mousePos(Cord.phone)
        time.sleep(.1)
        leftClick()
        mousePos(Cord.menu_toppings)
        time.sleep(.05)
        leftClick()
        s = screenGrab()
        print 'test'
        time.sleep(.1)
        if s.getpixel(Cord.t_nori) != (33, 30, 11):
            print 'nori is available'
            mousePos(Cord.t_nori)
            time.sleep(.1)
            leftClick()
            mousePos(Cord.delivery_norm)
            foodOnHand['nori'] += 10         
            time.sleep(.1)
            leftClick()
            time.sleep(2.5)
        else:
            print 'nori is NOT available'
            mousePos(Cord.t_exit)
            leftClick()
            time.sleep(1)
            buyFood(food)
 
    if food == 'roe':
        mousePos(Cord.phone)
        time.sleep(.1)
        leftClick()
        mousePos(Cord.menu_toppings)
        time.sleep(.05)
        leftClick()
        s = screenGrab()
         
        time.sleep(.1)
        if s.getpixel(Cord.t_roe) != (127, 61, 0):
            print 'roe is available'
            mousePos(Cord.t_roe)
            time.sleep(.1)
            leftClick()
            mousePos(Cord.delivery_norm)
            foodOnHand['roe'] += 10                
            time.sleep(.1)
            leftClick()
            time.sleep(2.5)
        else:
            print 'roe is NOT available'
            mousePos(Cord.t_exit)
            leftClick()
            time.sleep(1)
            buyFood(food)

Шаг 22: Проверка запасов еды

Теперь когда функции makeFood() и buyFood() могут менять количество ингредиентов, нам нужно создать функцию, которая будет отслеживать что количество какого-то ингредиента стало ниже критического уровня.

def checkFood():
    for i, j in foodOnHand.items():
        if i == 'nori' or i == 'rice' or i == 'roe':
            if j <= 3:
                print '%s is low and needs to be replenished' % i
                buyFood(i)

Мы будем циклом обходить пары ключ:значение в массиве с запасами ингредиентов. Если нори, риса или икры останется меньше 4, вызывается функция buyFood(), параметром в которую передается имя ингредиента.

Шаг 23: Перевод RGB значений — Установка

Для того, чтобы двигаться дальше, мы должны получать информацию о том, какой тип суши запрашивает клиент. Сделать это с помощью функции getpixel() было бы достаточно тяжело, так как нам пришлось бы искать область с уникальным значением RGB для каждого вида суши для каждого посетителя. Кроме того, для каждого нового типа суши, вам придется вручную осмотреть его, чтобы увидеть, есть ли у него уникальный RGB, который не найден ни в одном из других типов суши. Это значит, что нам пришлось бы хранить 6 мест, по 8 типов суши. Это 48 уникальных координат.

Очевидно, нам нужен метод попроще.

Метод номер два: усреднение изображения. Этот способ работает с набором RGB-значений, вместо конкретного пикселя. Для каждого скриншота, переведенного в оттенки серого, и загруженного в массив, мы просуммируем все пиксели. Эта сумма обрабатывается также как значение RGB в методе getpixel().

Гибкость этого метода в том, что, когда он настроен, от нас больше ничего не требуется. По мере ввода новых суши, их значения RGB суммируются и выводятся для нашего использования. Нет необходимости искать конкретные координаты с помощью getpixel().

Тем не менее, для этого метода требуется настройка. Нам нужно получать скрин только той области, в которой отображается желаемое суши конкретного клиента, а не всего игрового окна.

Найдите уже написанную функцию screenGrab() и скопируйте ее рядом. Переименуйте копию в grab(), и внесите следующие изменения:

def grab():
    box = (x_pad + 1,y_pad+1,x_pad+641,y_pad+481)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    return a

На второй строке мы переводим скриншот в оттенки серого и записываем в переменную im. Такой скриншот позволяет работать с ним намного быстрее, так как вместо 3 значений Красного, Зеленого и Голубого, каждый пиксель имеет только одно значение от 0 до 255.

На третьей строчке мы создаем массив значений цветов использую PIL метод getcolors() и записываем в переменную im.

На четвертой строке мы суммируем все точки и выводим их в консоль. Это число по которому мы будем сравнивать изображения.

Шаг 24: Зададим области для скринов заказов

Начните новую игру и дождитесь когда все посетители рассядутся по своим местам. Сделайте скрин двойным кликом по quickGrab.py.

Области с заказами

Нам нужно задать ограничивающие области внутри каждого из этих спич-баблов (белые облака с рисунком суши). Приблизьте из в редакторе так, чтобы было видно пиксели:

Области с заказами

Для каждого спич-бабла мы должны быть уверены что верхний левый край начинается в одном и том же месте. Для этого отступим два пикселя от внутреннего края спич-бабла. Первый белый пиксель на второй ступеньке будет началом отсчета.

Области с заказами

Для создания пары координат, отсчитайте 63 по оси x и 16 по оси y. Это даст подобный прямоугольник:

Области с заказами

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

Приступим к созданию 6 новых функций, каждая будет уточнением функции grab() и будет передавать аргументом координаты спич-баблов. Как только закончим, создадим функцию, запускающую их все сразу для тестирования.

def get_seat_one():
    box = (45,427,45+63,427+16)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    im.save(os.getcwd() + '\seat_one__' + str(int(time.time())) + '.png', 'PNG')    
    return a
 
def get_seat_two():
    box = (146,427,146+63,427+16)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    im.save(os.getcwd() + '\seat_two__' + str(int(time.time())) + '.png', 'PNG')    
    return a
 
def get_seat_three():
    box = (247,427,247+63,427+16)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    im.save(os.getcwd() + '\seat_three__' + str(int(time.time())) + '.png', 'PNG')    
    return a
 
def get_seat_four():
    box = (348,427,348+63,427+16)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    im.save(os.getcwd() + '\seat_four__' + str(int(time.time())) + '.png', 'PNG')    
    return a
 
def get_seat_five():
    box = (449,427,449+63,427+16)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    im.save(os.getcwd() + '\seat_five__' + str(int(time.time())) + '.png', 'PNG')    
    return a
 
def get_seat_six():
    box = (550,427,550+63,427+16)
    im = ImageOps.grayscale(ImageGrab.grab(box))
    a = array(im.getcolors())
    a = a.sum()
    print a
    im.save(os.getcwd() + '\seat_six__' + str(int(time.time())) + '.png', 'PNG')    
    return a
 
def get_all_seats():
    get_seat_one()
    get_seat_two()
    get_seat_three()
    get_seat_four()
    get_seat_five()
    get_seat_six()

Отлично! Много кода, но это только вариации функции ImageGrab.Grab. Теперь нужно перейти в игру и протестировать. Нужно убедиться, что не зависимо от того в каком спич-бабле находится суши, один и тот же заказ отображает одну и ту же сумму.

Шаг 25: Создаем массив с типами Суши

Как только Вы убедились, что каждый тип суши дает одинаковое значение суммы пикселей, запишите эти суммы в массив:

sushiTypes = {1893:'onigiri', 
    2537:'caliroll',
    1900:'gunkan',}

Здесь значение стоит на месте ключа потому, что по нему будет осуществляться поиск.

Шаг 26: Создаем массив мест без спич-баблов

Для работы нам нужны значения сумм скриншотов мест под спич-баблами, чтобы понимать что посетитель отсутствует в данном месте.

Нужно начать игру и выполнить функцию get_all_seats() до того как придет первый посетитель. Полученные значения запишем в массив Blank.

class Blank:
    seat_1 = 8041
    seat_2 = 5908
    seat_3 = 10984
    seat_4 = 10304
    seat_5 = 6519
    seat_6 = 8963

Шаг 27: Соединяем все вместе

Время окончательно передать контроль нашему боту. Напишем скрипт, который позволит ему реагировать на клиентов, готовить еду и пополнять запасы.

Основа логики будет следующая: Проверка мест > Проверка заказов > Если не хватает ингредиентов, то купить > почистить столы > Повторить сначала

def check_bubs():

  checkFood()
  s1 = get_seat_one()
  if s1 != Blank.seat_1:
      if sushiTypes.has_key(s1):
          print 'table 1 is occupied and needs %s' % sushiTypes[s1]
          makeFood(sushiTypes[s1])
      else:
          print 'sushi not found!n sushiType = %i' % s1

  else:
      print 'Table 1 unoccupied'

  clear_tables()
  checkFood()
  s2 = get_seat_two()
  if s2 != Blank.seat_2:
      if sushiTypes.has_key(s2):
          print 'table 2 is occupied and needs %s' % sushiTypes[s2]
          makeFood(sushiTypes[s2])
      else:
          print 'sushi not found!n sushiType = %i' % s2

  else:
      print 'Table 2 unoccupied'

  checkFood()
  s3 = get_seat_three()
  if s3 != Blank.seat_3:
      if sushiTypes.has_key(s3):
          print 'table 3 is occupied and needs %s' % sushiTypes[s3]
          makeFood(sushiTypes[s3])
      else:
          print 'sushi not found!n sushiType = %i' % s3

  else:
      print 'Table 3 unoccupied'

  checkFood()
  s4 = get_seat_four()
  if s4 != Blank.seat_4:
      if sushiTypes.has_key(s4):
          print 'table 4 is occupied and needs %s' % sushiTypes[s4]
          makeFood(sushiTypes[s4])
      else:
          print 'sushi not found!n sushiType = %i' % s4

  else:
      print 'Table 4 unoccupied'

  clear_tables()
  checkFood()
  s5 = get_seat_five()
  if s5 != Blank.seat_5:
      if sushiTypes.has_key(s5):
          print 'table 5 is occupied and needs %s' % sushiTypes[s5]
          makeFood(sushiTypes[s5])
      else:
          print 'sushi not found!n sushiType = %i' % s5

  else:
      print 'Table 5 unoccupied'

  checkFood()
  s6 = get_seat_six()
  if s6 != Blank.seat_6:
      if sushiTypes.has_key(s6):
          print 'table 1 is occupied and needs %s' % sushiTypes[s6]
          makeFood(sushiTypes[s6])
      else:
          print 'sushi not found!n sushiType = %i' % s6

  else:
      print 'Table 6 unoccupied'

  clear_tables()

Первое что мы делаем — это проверяем запасы. Далее делаем скрин позиции заказа первого посетителя и записываем в s1. После этого проверяем скрин на неравенство пустому месту. Если они не равны, значит у нас есть посетитель. Далее проверяем массив sushiTypes чтобы определить какой заказ сделал посетитель и передаем этот аргумент в функцию makeFood().

Clear_tables() выполняется через проверку каждых двух мест.

Теперь надо это зациклить.

Шаг 28: Главный цикл

Мы создаем цикл. Так как мы не задаем никакого условия выхода из цикла, то чтобы завершить игру, нужно в консоли нажать CTRL + C.

def main():
    startGame()
    while True:
        check_bubs()

Вот и все! Обновите страницу, дождитесь загрузки игры и запустите бота!

Бот немного неуклюжий и нуждается в доработках. Но это отличный скелет для того чтобы продолжить экспериментировать!

Уровень: начинающий

Материал рассчитан на тех, кто в жизни не написал ни строчки кода. Если вы уже в курсе основ программирования, прочитайте лучше о чистых функциях.

Многие слышали про чат-ботов и роботов для общения: им пишешь, они отвечают, получается диалог с машиной. Чат-бот может рассказать анекдот, поискать за вас в интернете, забронировать столик в ресторане и что угодно ещё, чему его обучат создатели.

Иногда такое общение выглядит как общение с человеком. Может даже показаться, что там работает искусственный интеллект — и иногда так действительно бывает. Но часто всё проще: это алгоритм, который умеет распознавать некоторые ваши слова и давать ответы по заранее заготовленным шаблонам. Чем алгоритм более разветвлённый, тем естественнее и полезнее бот.

Давайте сделаем собственного чат-бота с очень простым алгоритмом. Позже вы сможете усложнить его, как захотите. Но сначала — самая база для тех, кто никогда не писал код.

Обычно, чтобы создать какую-то программу, нужно выполнить несколько действий: например, скачать программу-обработчик языка, завести проект, написать задуманную программу, скомпилировать. И только потом ей можно пользоваться. Но мы пойдём по более простому пути: напишем программу, работающую прямо в браузере, через который вы читаете эту статью. Сделать это можно лишь на компьютере, на телефоне придётся пользоваться ботом.

Мы будем решать задачу на языке JavaScript — это язык программирования, который встроен в ваш браузер и на котором написать код можно прямо сейчас, ничего не устанавливая.

Чтобы сделать что-то на JavaScript, нужно открыть консоль. Почти во всех современных браузерах это делается сочетанием клавиш Shift + Ctrl + J или Cmd + Alt + J. Справа или снизу появится дополнительное окно, в котором уже будет что-то происходить:

Чат-бот будет создан через консоль

Если у вас не открылась консоль, зайдите в верхнее меню и поищите слово «Консоль». Обычно этот пункт прячется в разделе «Инструменты разработчика».

Когда вы открываете консоль, она сразу готова выполнять ваши команды. Если в неё вставить программу, написанную на JavaScript, и нажать Enter, ваш браузер её реализует. Если в коде есть ошибки, консоль сама подсветит их. Можно отправлять в неё программу кусками или даже построчно: браузер будет помнить всё, что происходило в вашей программе, пока вы не перезагрузите страницу.

Первая строка

В консоли можно не только писать код, но и выводить туда результаты. Давайте для начала сделаем самую простую программу, которая отобразит в консоли слово «Привет!». Для этого используем команду console.log('Привет!');

Вставим её в консоль и нажмём Enter:

Чат-бот: пишем первую строку кода

Поздравляем, вы только что написали свою первую программу для компьютера! Она очень простая: компьютер всего лишь говорит «Привет!». Но оцените момент: это вы его научили так говорить. Попробуйте научить его и другим словам.

Если написать несколько команд, получим сообщение из нескольких строк:

console.log('Привет!');
console.log('Я — ваш компьютер.');

Вот мы и начали создавать своего чат-бота, который нас уже поприветствовал в консоли. Теперь сделаем так, чтобы мы тоже могли ему что-нибудь ответить. Для этого нам понадобятся переменные.

Переменные

Переменная — это ячейка в памяти компьютера, где можно что-то хранить и менять. Дело в том, что компьютеру для вычислений нужно сказать: «Вот тут данные у нас меняться не будут, а вот тут будут, выдели память». И система выделит достаточно памяти, чтобы хранить всё, что будет лежать внутри переменной. В последнюю можно записать новое значение, а можно узнать, что уже лежит внутри неё.

Чтобы дать понять компьютеру, что у нас сейчас будет переменная, нужно сказать ему слово var, после которого вписать название переменной — так нам проще к ней обращаться. Например, следующая строка создаст переменную name и положит в неё слово «Код»:

var name = 'Код';

Название тут может быть практически любым, главное, чтобы оно начиналось с буквы. По-русски переменные называть нельзя, только буквами латинского алфавита. Можно было бы использовать вариант imya или zovut, но программисты считают, что чем проще название переменной, тем лучше.

Теперь посмотрим содержимое элемента. Следующая команда выведет то, что сейчас записано в переменной name:

console.log(name);

Можно посмотреть, какое сегодня число. Это внутренняя системная переменная. Строго говоря, это не совсем переменная, но для начала давайте считать так:

console.log(Date());

Но это мы всё смотрим во внутренности компьютера. А нам нужно спросить что-то у пользователя. Чтобы мы могли ввести новые данные в нашу программу, используем команду prompt()

var name = prompt('Как вас зовут?');

Вставьте в консоль команду var name = prompt('Как вас зовут?'); и посмотрите, что произойдёт. Компьютер выведет окно и будет ждать, пока вы внесёте туда своё имя. Интерфейс выглядит красиво: давайте в диалоге общаться с компьютером не через консоль, а через такие появляющиеся окошки. Для этого напишем новые команды:

alert('Привет! Я — ваш компьютер.');
var name = prompt('Как вас зовут?');

Пусть компьютер проявит вежливость и скажет, что ему приятно с нами познакомиться. Чтобы он смог обратиться к нам по имени, используем переменную name — в ней как раз хранится то, что мы ответили компьютеру:

alert('Привет, ' + name + ', приятно познакомиться!');

Чтобы вывести осмысленную фразу, мы взяли начало ‘Привет, ‘, затем с помощью плюсика соединили со значением переменной name, которая хранит наше имя, а потом ещё одним плюсиком добавили к фразе концовку. Чтобы компьютер знал, что мы хотим вывести на экран текст, а не числа, используются кавычки. Компьютер воспринимает как текст то, что внутри кавычек. И выводит точно в том виде, в котором написано.

Расчёт дня рождения

Давайте соединим все наши команды в одну программу и допишем несколько новых фраз:

alert('Привет! Я — ваш компьютер.');
var name = prompt('Как вас зовут?');
alert('Отлично, ' + name + ', приятно познакомиться!');
var hobby = prompt('Скажите, ' + name + ', а какое у вас хобби?');
alert('Действительно, ' + hobby + ' — интересное занятие! А я больше всего люблю вычисления и алгоритмы.');
var d = prompt(name + ', у меня для вас сюрприз! Напишите свой день рождения в формате месяц-день (например, 05-23)');
//Началась магия, где мы берём текущую дату и сравниваем с тем, что вы ввели.
//Кстати, это — комментарий, он не влияет на ход программы. 🙂
var current_date = new Date();
var my_date = new Date((d + '-' + (current_date.getFullYear() + 1)));
var dayZ = Math.abs(((Date.parse(my_date) - Date.parse(current_date)) / (1000 * 3600 * 24)) % 365);
var result = dayZ.toFixed(0);
//Магия закончилась, в переменной result у нас теперь хранится количество дней.
alert('Знаете, сколько дней осталось до вашего дня рождения? ' + result + '!');
alert('Спасибо за общение, ' + name + ', ещё увидимся!');

Обратите внимание: у нас появился новый вопрос и новая переменная hobby, в которой хранится информация об увлечении. А ещё — комментарии, которых можно добавлять сколько угодно. JavaScript не обращает внимания на то, что написано после двух косых черт:

//это комментарий

Теперь у вас есть всё, что нужно, чтобы написать свою версию чат-бота для общения. Если продолжите решать наши задачки, то сможете научить компьютер по-разному реагировать на ваши ответы и даже вести осмысленный диалог.

Что ещё посмотреть

Вот кое-что, что может вам пригодиться при создании первого чат-бота.

performance.now() — эта команда возвращает время в миллисекундах с момента открытия текущей страницы. Можно поделить на 1 000, и вы узнаете, сколько секунд вы сидите на какой-то странице. Если поделить на 60 000 — сколько минут.

setTimeout() — позволяет выполнить любой код через определённое время. Например, вы можете задать вопрос и предоставить ровно минуту на размышление, после чего появится окно для ответа.

setInterval() —  то же самое, что и предыдущее, но выполнение кода повторяется с равномерным интервалом, например раз в 5 минут. Если вы хотите научить чат-бота, чтобы он раз в час напоминал попить воды, эта команда — то, что нужно.

Как пользоваться этими штуками, мы расскажем в одной из будущих статей, но вы всегда можете самостоятельно поискать в интернете, как они работают. Пользуясь этими тремя возможностями JavaScript, получится создать неплохого бота, который будет следить за вашей продуктивностью и интервалами работы. Подписывайтесь на «Код», чтобы не пропустить новые разборы.

45K

19 апреля 2009 года

qpokyc

21 / / 27.03.2009

Гуишка совсем лишнее — представляешь, alekciy, скрипты можно и в нотпаде писать! (офигеть, да?) Нам DZенхам не до «интуитивно-понятных интерфейсов» :D

Теоритически, такое и на ЯвеСкрипт возможно, но такой трюк легко нейтрализуется, поэтому надежней ПХП, ибо не даром его деверы сказали, что он «Может всё!».

Впрочем, я бы и сам в надежности их слов сомневался, еслибы не узрил своими глазами бота на ПХП, играющего в Diablo!!! Говорят, бот ожил, но в последний раз, кода я его увидел он не работал из-за противной капчи (кстати, alekciy, а по этому поводу у Мелкоягких тоже есть «готовые решения», впрочем, не отвечай — сам знаю ;) куда ты меня пошлешь, ибо у таких «разработчиков» как ты все сводится если не к «Ы», то к «для этого используй то»).

Дык на странице по формочкам/ссылочкам бегать — это что проблемму что ли составит? Или уже имеется «готовое решение»? Если имеется — вопросов 0, а нет — гони на бочку свое, ибо мне даже интересно стало.

Зачем тебе десктопное прикладухо? Уж не через API ли ты собрался лезть в браузер и нажимать там сцылки :D? Браузер здесь вообще косвенный элемент. Тот бот не десктопный и работал через апач нормально и всех устраивал, что не так?
(Твой вариант?)

ЗЫ: И да простят меня модеры, весна — не удержался :rolleyes:
————————
А вот оно чё (узрил)! Ну можно и так, конечно. Чтож, гнедой кобыле — верста не крюк =), но я бы между ПХП и аутойдом выбрал ПХП. Аутойд — это всетаки чтобы кнопочки автоматически нажимать, а не ботов на нем делать…

В этой статье разберём, как добавить на сайт простой чат-бот SimpleChatbot и создать для него необходимый сценарий диалога.

О чат-боте SimpleChatbot

SimpleChatbot — это простой чат-бот для сайта, сценарий диалога которого определяется JSON-файлом или данными, расположенными непосредственно в скрипте. Состоит он из 2 частей: клиентской и северной. Первая написана на чистом JavaScript, вторая — на PHP.

SimpleChatbot очень прост в настройке и установке на сайт. Он не зависит от сторонних сервисов и библиотек. Для его работы (серверной стороны) нужен веб-сервер Nginx, Apache или любой другой, умеющий выполнять php-скрипты.

Демо чат-бота

На клиентской стороне кроме самого скрипта чат-бота («chatbot.js») используется ещё FingerPrint JS. Он применяется для создания отпечатка браузера (выполнение анонимной идентификации клиента). Сам чат-бот непосредственно от него не зависит. Его действие заключается в формировании некоторого идентификатора, который затем сохраняется в LocalStorage и используется чат-ботом для передачи его на сервер. А серверу он нужен только для создания файла с соответствующим именем, в который он будет сохранять всю переписку бота с этим пользователем. По умолчанию файлы сохраняются в папку chats.

Пример содержимого каталога chats:

Пример содержимого папки chats

В этом примере имеются 2 файла. Каждый из них – это некий диалог бота с определённым клиентом.

Если его открыть, то мы увидим весь контент «разговора».

Например:

Пример разговора чат-бота с клиентом

Если обратно вернуться к FingerPrint JS и немного углубиться в эту тему, то увидим, что значение, которое он генерирует сохраняется в LocalStorage. При этом в качестве имени ключа используется значение configChatbot.key. По сути, здесь не важно, что использовать, FingerPrint JS , или какую-то другую библиотеку, или свой код, главное, чтобы он создавал некий идентификатор клиента и помещал его в хранилище.

Исходный код этого чат-бота является открытым и находится на GitHub. Текущая версия 1.3.2. В качестве лицензии, используется MIT. Эта лицензия позволяет данное программное обеспечение использовать как в бесплатных, так и в коммерческих проектах (при условии, что текст лицензии поставляется вместе с этим программным обеспечением).

Установка чат-бота на сайт

Порядок действий:

1. Скачать последний релиз SimpleCatbot (v. 1.3.2) с GitHub и распаковать его.

2. Загрузить папку «chatbot» в корень вашего сайта или любое другое место. Если вы выбрали последнее, то в этом случае необходимо будет ещё в определённых местах изменить отредактировать пути к файлам.

3. Добавить стили (файл «chatbot.css») к странице:

<link rel="stylesheet" href="/chatbot/chatbot.css">

4. Подключить скрипты («fp2.js» и «chatbot.js»):

<!-- FingerPrint JS -->
<script src="/chatbot/fp2.js"></script>
<!-- ChatBot JS -->
<script src="/chatbot/chatbot.js"></script>

Код «chatbot.js» написан на синтаксисе, который поддерживается всеми используемые в данное время браузерами включая IE11.

<!-- FingerPrint JS -->
<script src="/chatbot/fp2.js"></script>
<!-- ChatBot JS -->
<script src="/chatbot/chatbot.js"></script>

5. Поместить в HTML документ кнопку, посредством которой будем вызывать окно чат-бота:

<!-- Кнопка для вызова окна чат-бота -->
<div class="chatbot__btn">
  <div class="chatbot__tooltip d-none">Есть вопрос?</div>
</div>

Кнопка для открытия окна чат-бота

Далее необходимо поместить на страницу скрипт после подключения этих файлов.

1 вариант скрипта – с загрузкой сценария диалога из JSON файла («/data/data-1.json») посредством AJAX.

// конфигурация чат-бота
const configChatbot = {};
// CSS-селектор кнопки, посредством которой будем вызывать окно диалога с чат-ботом
configChatbot.btn = '.chatbot__btn';
// ключ для хранения отпечатка браузера
configChatbot.key = 'fingerprint';
// реплики чат-бота
configChatbot.replicas = '/data/data-1.json';
// корневой элемент
configChatbot.root = SimpleChatbot.createTemplate();
// URL chatbot.php
configChatbot.url = '/chatbot/chatbot.php';
// переменная для хранения экземпляра
let chatbot = null;
// добавление ключа для хранения отпечатка браузера в LocalStorage
let fingerprint = localStorage.getItem(configChatbot.key);
if (!fingerprint) {
  Fingerprint2.get(function (components) {
    fingerprint = Fingerprint2.x64hash128(components.map(function (pair) {
      return pair.value
    }).join(), 31)
    localStorage.setItem(configChatbot.key, fingerprint)
  });
}
// при клике по кнопке configChatbot.btn
document.querySelector(configChatbot.btn).onclick = function (e) {
  this.classList.add('d-none');
  const $tooltip = this.querySelector('.chatbot__tooltip');
  if ($tooltip) {
    $tooltip.classList.add('d-none');
  }
  configChatbot.root.classList.toggle('chatbot_hidden');
  if (chatbot) {
    return;
  }
  // получение json-файла, содержащего сценарий диалога для чат-бота через AJAX
  const request = new XMLHttpRequest();
  request.open('GET', configChatbot.replicas, true);
  request.responseType = 'json';
  request.onload = function () {
    const status = request.status;
    if (status === 200) {
      const data = request.response;
      // для поддержки IE11
      if (typeof data === 'string') {
        configChatbot.replicas = JSON.parse(data);
      } else {
        configChatbot.replicas = data;
      }
      // инициализация SimpleChatbot
      chatbot = new SimpleChatbot(configChatbot);
      chatbot.init();
    } else {
      console.log(status, request.response);
    }
  };
  request.send();
};

Настройка чат-бота выполняется посредством установки определённых ключей configChatbot:

  • configChatbot.btn — селектор кнопки, посредством которой будем вызывать окно диалога с чат-ботом;
  • configChatbot.key — имя ключа LocalStorage, в котором будет храниться отпечаток браузера;
  • configChatbot.replicas — URL json-файла, содержащего сценарий диалога для чат-бота;
  • configChatbot.root — ссылка, на корневой элемент чат-бота;
  • configChatbot.url — путь к файлу chatbot.php.

Инициализация чат-бота в коде выполняется так:

let chatbot = new SimpleChatbot(configChatbot);
chatbot.init();

2 вариант скрипта – с помещением данных для чат-бота непосредственно в сценарий.

// конфигурация чат-бота
const configChatbot = {};
// CSS-селектор кнопки, посредством которой будем вызывать окно диалога с чат-ботом
configChatbot.btn = '.chatbot__btn';
// ключ для хранения отпечатка браузера
configChatbot.key = 'fingerprint';
// реплики чат-бота
configChatbot.replicas = { /*...*/ };
// корневой элемент
configChatbot.root = SimpleChatbot.createTemplate();
// URL chatbot.php
configChatbot.url = '/chatbot/chatbot.php';
// создание SimpleChatbot
let chatbot = new SimpleChatbot(configChatbot);
// при клике по кнопке configChatbot.btn
document.querySelector(configChatbot.btn).onclick = function (e) {
  this.classList.add('d-none');
  const $tooltip = this.querySelector('.chatbot__tooltip');
  if ($tooltip) {
    $tooltip.classList.add('d-none');
  }
  configChatbot.root.classList.toggle('chatbot_hidden');
  chatbot.init();
};
// добавление ключа для хранения отпечатка браузера в LocalStorage
let fingerprint = localStorage.getItem(configChatbot.key);
if (!fingerprint) {
  Fingerprint2.get(function (components) {
    fingerprint = Fingerprint2.x64hash128(components.map(function (pair) {
      return pair.value
    }).join(), 31)
    localStorage.setItem(configChatbot.key, fingerprint)
  });
}

Настройка чат-бота в этом варианте практически ничем отличается от способа с JSON за исключением того, что данные здесь уже находятся непосредственно в скрипте (в configChatbot.replicas), и их не нужно дополнительно загружать через AJAX.

Составление диалога для чат-бота

Написание реплик для чат-бота выполняется в формате объекта. При создании объекта необходимо определить 2 ключа. Первый ключ (bot) отвечает за сообщения бота, а второй (human) — пользователя.

configChatbot.replicas = {
  bot: { /* ... */ },
  human: { /* ... */ }
}

Далее в bot необходимо поместить сами реплики. Описание их также выполняется в формате объекта. В качестве ключа используется число, а в роли значения – объект, состоящий из свойств content и human. В content помещаем сам контент, а в human – массив ответов для пользователя.

configChatbot.replicas = {
  bot: {
    0: { content: 'Привет! Я Инфинити - бот поддержки сайта <a href="https://itchief.ru" target="_blank">itchief.ru</a>', human: [0, 1, 2] },
    /* ... */
  },
  human: { /* ... */ }
}

Кстати, работа чат-бота всегда стартует с приветственного сообщения бота, заданного в 0. После показа этой реплики и некоторой паузы будет выведены варианты ответов для пользователя, указанные в human.

Приветственная реплика бота

Создание ответов для пользователя выполняется аналогичным образом. Т.е. в content помещаем ответ пользователя, который будет выведен в виде кнопки, а в bot – номер следующей реплики бота при выборе этого ответа пользователем.

configChatbot.replicas = {
  bot: {
    0: { content: 'Привет! Я Инфинити - бот поддержки сайта <a href="https://itchief.ru" target="_blank">itchief.ru</a>', human: [0, 1, 2] },
    /* ... */
  },
  human: {
    0: { content: 'Привет! Я рад с тобой познакомиться', bot: 1 },
    1: { content: 'Салют!', bot: 2 },
    2: { content: 'Приветик, Инфинити!', bot: 2 },
    /* ... */
  }
}

Ответы для пользователя в чат-боте

Но если нужно, чтобы пользователь не просто выбрал один из предоставленных ему ответов, а ввёл некоторые данные. То в этом случае в качестве значения content нужно указать пустую строку. В этом сценарии пользователю станет доступно поле для ввода (input) информации и кнопка, с помощью которой её можно будет отправить.

Если данные, которые ввёл пользователь вы хотите использовать в репликах, то им необходимо задать имя. Осуществляется это с помощью свойства name.

configChatbot.replicas = {
  bot: {
    0: { content: 'Привет! Я Инфинити - бот поддержки сайта <a href="https://itchief.ru" target="_blank">itchief.ru</a>', human: [0, 1, 2] },
    1: { content: 'Я тоже рад, как мне к Вам обращаться', human: [3] },
    2: { content: 'Как мне к Вам обращаться?', human: [3] },
    /* ... */
  },
  human: {
    0: { content: 'Привет! Я рад с тобой познакомиться', bot: 1 },
    1: { content: 'Салют!', bot: 2 },
    2: { content: 'Приветик, Инфинити!', bot: 2 },
    3: { content: '', bot: 3, name: 'name' },
    /* ... */
  }
}

Поле чат-бота для ввода запрашиваемых данных

Например, в этом примере, мы установили данным, которые введёт пользователь, имя name. Далее эти данные мы можем использовать в репликах бота используя это имя, заключенное в двойные фигурные скобки (например, {{name}}).

configChatbot.replicas = {
  bot: {
    0: { content: 'Привет! Я Инфинити - бот поддержки сайта <a href="https://itchief.ru" target="_blank">itchief.ru</a>', human: [0, 1, 2] },
    1: { content: 'Я тоже рад, как мне к Вам обращаться', human: [3] },
    2: { content: 'Как мне к Вам обращаться?', human: [3] },
    3: { content: '{{name}}, что Вас интересует?', human: [4, 5] },
    /* ... */
  },
  human: {
    0: { content: 'Привет! Я рад с тобой познакомиться', bot: 1 },
    1: { content: 'Салют!', bot: 2 },
    2: { content: 'Приветик, Инфинити!', bot: 2 },
    3: { content: '', bot: 3, name: 'name' },
    4: { content: 'Меня интересует, как я могу использовать этот чат-бот у себя на сайте', bot: 4 },
    5: { content: 'Хочу оставить запрос разработчику чат-бота', bot: 5 },
    /* ... */
  }
}

Использование имени пользователя в реплики чат-бота

Далее, придерживаясь этих правил при написании реплик составляем нужный диалоговый сценарий чат-бота для своего сайта.

Пример сценария:

configChatbot.replicas = {
  bot: {
    0: {
      content: 'Привет! Я Инфинити - бот поддержки сайта <a href="https://itchief.ru" target="_blank">itchief.ru</a>', human: [0, 1, 2]
    },
    1: { content: 'Я тоже рад, как мне к Вам обращаться?', human: [3] },
    2: { content: 'Как мне к Вам обращаться?', human: [3] },
    3: { content: '{{name}}, что Вас интересует?', human: [4, 5] },
    4: { content: '{{name}}, для этого перейдите на <a href="https://itchief.ru/javascript/chatbot-for-website" target="_blank">эту страницу</a>. Она содержит подробную инструкцию по использованию этого чат-бота.', human: [6] },
    5: { content: "{{name}}, какой у Вас вопрос?", human: [7] },
    6: { content: '{{name}}, мы получили Ваш вопрос! Скажите, как с Вами удобнее будет связаться?', human: [8, 9] },
    7: { content: '{{name}}, укажите пожалуйста ваш телефон', human: [10] },
    8: { content: '{{name}}, укажите пожалуйста ваш Email ниже', human: [10] },
    9: { content: 'Готово! {{name}}, мы свяжемся с вами в ближайшее время по {{contact}}. Всего хорошего!', human: [6] },
  },
  human: {
    0: { content: 'Привет! Я рад с тобой познакомиться', bot: 1 },
    1: { content: 'Салют!', bot: 2 },
    2: { content: 'Приветик, Инфинити!', bot: 2 },
    3: { content: '', bot: 3, name: 'name' },
    4: { content: 'Меня интересует, как я могу использовать этот чат-бот у себя на сайте', bot: 4 },
    5: { content: 'Хочу оставить запрос разработчику чат-бота', bot: 5 },
    6: { content: 'В начало', bot: 3 },
    7: { content: '', bot: 6, name: '' },
    8: { content: 'по телефону', bot: 7 },
    9: { content: 'по email', bot: 8 },
    10: { content: '', bot: 9, name: 'contact' },
  }
}

Когда сценарий у вас готов, его следует, если вы используете 2 вариант скрипта, поместить вместо этой строчки:

configChatbot.replicas = { /*...*/ };

При использовании 1 варианта скрипта, его необходимо перевести в строку JSON. Для этого можно воспользоваться каким-нибудь онлайн сервисом или стандартным JavaScript методом stringify:

configChatbot.replicas = { /* ... */ };
const json = JSON.stringify(configChatbot.replicas);
console.log(json);

Если вы воспользовались последним советом, то после открытия страницы из консоли браузера нужно скопировать полученную строку (без обрамляющих её кавычек). После этого вставить её в файл с расширением json и загрузить его на сервер.

Новые возможности чат-бота

В SimpleChatbot начиная с версии 1.2.0 для бота можно указывать сразу несколько сообщений. Для этого их необходимо передать в формате массива. Такие сообщения будут выведены последовательно друга за другом в виде отдельных реплик.

Например:

bot: {
  0: { content: ['Привет!', 'Я Инфинити - бот поддержки сайта <a href="https://itchief.ru" target="_blank">itchief.ru</a>'], human: [0, 1, 2] },
  ...

Задание нескольких сообщений в чат-боте одно за другим

В версии 1.3.0 добавлена возможность отправки сообщения клавишей Enter, метод reset для очистки (сброса) чата и кнопка для вызова этого метода.

Понравилась статья? Поделить с друзьями:
  • Как написать бота для биржи криптовалют
  • Как написать бота для бинарных опционов
  • Как написать бота для бинанс
  • Как написать бота для starcraft 2
  • Как написать бота для binance python