Простой бот для сайта
от aNNiMON
Наверняка, многие уже успели заметить некоторое нововведение на нашем сайте. Сейчас я расскажу, откуда пошли корни, как работает бот и как наполнялась база фраз. Сделать точно такого же бота не составит труда, уж поверьте.
Корни
Идею добавить на сайт бота подкинул благородный дон Virus-ON, так что все лавры ему.
Что же нужно для создания бота?
Во-первых, база данных фраз — память бота, чтобы он мог отвечать на сообщения.
Во-вторых, некоторый алгоритм выбора фраз — мозг бота, чтобы он отвечал более-менее в тему.
База данных. Часть 1
По воле случая, довелось познакомиться с одним человеком, программирующем под Android и жутко ненавидящем продукцию Apple. Одной из его разработок является VK iHA bot, исходники которого есть на GitHub.
Само приложение нам не интересно, а вот база данных на первых порах была бы весьма кстати. Её и возьмём.
Формат базы таков:
сообщение\ответ\релевантность
Пример
привет\Привет, рад тебя видеть!\3
как дела\Дела отлично!\2
я человек\А я робот))\2
Нам нужно распарсить этот файл, убрать лишние ссылки, имена, матерные слова (коих тут очень много) и сохранить всё в другом формате, понятном БД MySQL.
Скрипт на языке Scala:
В методе main мы перебираем все .bin файлы в директории и вызываем для каждого такого файла метод open. Метод open читает файл построчно и преобразует строку в класс Phrase. В классе Phrase мы разбиваем строку на сообщение — ответ.
По-сути спецсимволы для нашего бота — мусор. Ему необязательно знать, где во входящем сообщении стоит запятая, он должен ориентироваться по словам и цифрам. Строка как раз это и делает.
К слову, наш бот не разбирается, что перед ним: вопрос или утверждение. Но он может понять это по совместным словам: что, где, как и т.д.
После выполнения скрипта, мы получим такой вывод в SQL-формате:
Осталось только создать таблицу в БД и залить дамп.
Алгоритм выбора фраз
Алгоритм прост. Пользователь пишет сообщение, которое затем передаётся боту на обработку. Там мы убираем все лишние символы из сообщения и ищем в БД похожую запись. Нашли одну — выдаём в качестве ответа. Нашли несколько — выбираем случайную из найденных. Не нашли ничего похожего — выводим случайный ответ.
Теперь по порядку и с кодом.
1. Поступило сообщение, отдаём его на фильтрацию:
2. Выбираем фразу по явному совпадению:
3. Если такой не нашли, тогда воспользуемся более умным вариантом:
4. Если и здесь пусто — выбираем любой ответ из доступных:
Наиболее интересен, вероятно, второй запрос. Он ищет совпадения основываясь на естественном языке. Вот отличия:
Вот, собственно, и весь мозг.
PHP класс Bot.class.php
Использовать вот так (forum/say.php):
База данных. Часть 2
Четырёх тысяч сообщений явно мало, нужно пополнить базу чем-нибудь таким, что соответствует тематике сайта. Парсить каждое сообщение на форуме не лучший вариант — не ясно где вопрос, где ответ. Но вот сообщения, содержащие цитату, нам вполне подходят. В них уже есть вопрос и прям в том же сообщении ответ.
Сперва достаём из базы форума все сообщения, содержащие цитату:
Затем сохраняем дамп. У меня получился файл в 12 Мб, содержащий 38 тысяч записей.
Теперь всё это дело нужно распарсить. Снова зовём на помощь Scala.
В итоге мы получили около 16000 пар фраза-ответ, которые можно залить в БД.
На всякий случай скажу с какой проблемой столкнулся в регулярках Scala (и Java). Дело в том, что обычное .* не хочет работать для символов юникода. Пришлось использовать специальные выражения \p{?}. В результате маленькое выражение:
растаращивает до:
В итоге имеем такого монстра:
\p{L} — Letter — буква в юникоде
\p{N} — Number — любое представление числа в юникоде (целое, вещественное и т.д.)
\p{P} — Punctuation — знаки пунктуации
\p{C} — Colobok — прочие символы в юникоде
Подробнее об этом можно почитать здесь
Кстати, если вы, как и я, валенок в регулярных выражениях, вам поможет сайт https://regex101.com/ (за ссылку благодарим Ксакепа)
Всем спасибо за прочтение. Теперь интерес к боту пропадёт.
Корни
Идею добавить на сайт бота подкинул благородный дон Virus-ON, так что все лавры ему.
Что же нужно для создания бота?
Во-первых, база данных фраз — память бота, чтобы он мог отвечать на сообщения.
Во-вторых, некоторый алгоритм выбора фраз — мозг бота, чтобы он отвечал более-менее в тему.
База данных. Часть 1
По воле случая, довелось познакомиться с одним человеком, программирующем под Android и жутко ненавидящем продукцию Apple. Одной из его разработок является VK iHA bot, исходники которого есть на GitHub.
Само приложение нам не интересно, а вот база данных на первых порах была бы весьма кстати. Её и возьмём.
Формат базы таков:
сообщение\ответ\релевантность
Пример
привет\Привет, рад тебя видеть!\3
как дела\Дела отлично!\2
я человек\А я робот))\2
Нам нужно распарсить этот файл, убрать лишние ссылки, имена, матерные слова (коих тут очень много) и сохранить всё в другом формате, понятном БД MySQL.
Скрипт на языке Scala:
- import scala.collection.mutable.HashSet
- import scala.io.Source
- import java.io.File
- object VkIHAbotDbExtractor {
- val filterWords = Array(// тут перечисление известных матерных слов для
- "fuck", "suck", "http://", "www", // фильтрации (список намного больше ;)
- )
- def main(args: Array[String]) {
- var set = new HashSet[Phrase]
- new File(args(0))
- .list
- .filter(_.endsWith(".bin"))
- .foreach(f => set = set ++ open(f))
- set.toList.sorted.foreach(println)
- }
- def open(file: String) = {
- Source.fromFile(file, "UTF-8").getLines
- .filter(_.split('\\').length == 3)
- .map[Phrase](line => Phrase(line.trim))
- .filter(_.censor)
- .toSet
- }
- case class Phrase(line: String) extends Ordered[Phrase] {
- val arr = line.split('\\') // разделяем строку на массив по символу \
- val message = messageFilter(arr(0).trim)
- val answer = arr(1).trim.replace("'", "''")
- def messageFilter(str: String) = {
- str.toLowerCase // фильтрация строки от лишних символов
- .replaceAll("[^\\p{L}\\p{Nd} ]+", "")
- .trim.replace("'", "''")
- }
- def censor : Boolean = { // соответствует ли сообщение нормам
- if (message.isEmpty || answer.isEmpty) return false
- for (word <- filterWords)
- if (line.toLowerCase.contains(word)) return false
- true
- }
- def compare(that: Phrase) = message.compareToIgnoreCase(that.message)
- override def hashCode = message.hashCode ^ answer.hashCode
- override def equals(obj: Any) = obj match {
- case that: Phrase => this.message == that.message && this.answer == that.answer
- case _ => false
- }
- override def toString() = s"('$message', '$answer'),"
- }
- }
В методе main мы перебираем все .bin файлы в директории и вызываем для каждого такого файла метод open. Метод open читает файл построчно и преобразует строку в класс Phrase. В классе Phrase мы разбиваем строку на сообщение — ответ.
По-сути спецсимволы для нашего бота — мусор. Ему необязательно знать, где во входящем сообщении стоит запятая, он должен ориентироваться по словам и цифрам. Строка
- .replaceAll("[^\\p{L}\\p{Nd} ]+", "")
К слову, наш бот не разбирается, что перед ним: вопрос или утверждение. Но он может понять это по совместным словам: что, где, как и т.д.
После выполнения скрипта, мы получим такой вывод в SQL-формате:
- ('0', 'Хорошее число ты написал.'),
- ('1', 'У меня с цифрами проблемы.'),
- ('1 декабря', 'С наступающим Новым Годом'),
Осталось только создать таблицу в БД и залить дамп.
- CREATE TABLE IF NOT EXISTS `bot` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `message` text NOT NULL,
- `answer` text NOT NULL,
- PRIMARY KEY (`id`),
- FULLTEXT KEY `message` (`message`)
- ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Алгоритм выбора фраз
Алгоритм прост. Пользователь пишет сообщение, которое затем передаётся боту на обработку. Там мы убираем все лишние символы из сообщения и ищем в БД похожую запись. Нашли одну — выдаём в качестве ответа. Нашли несколько — выбираем случайную из найденных. Не нашли ничего похожего — выводим случайный ответ.
Теперь по порядку и с кодом.
1. Поступило сообщение, отдаём его на фильтрацию:
- private function filter($message) {
- $msg = mb_strtolower($message, 'UTF-8'); // переводим текст в нижний регистр
- $msg = str_replace(self::NAME . ',', '', $msg); // убираем обращение к боту
- $msg = preg_replace('#\[c(.+?)\[/c\]#sui', '', $msg); // убираем цитату на случай, если Magatino лень нажать Ответить
- $msg = preg_replace('/[^a-zа-я0-9 ]/u', '', $msg); // убираем все спецсимволы
- $msg = preg_replace('/\s+/', ' ', $msg); // несколько подряд идущих пробелов заменяем на один
- return trim($msg);
- }
2. Выбираем фразу по явному совпадению:
- SELECT `answer` FROM `bot` WHERE `message`="привет" ORDER BY RAND() LIMIT 1
3. Если такой не нашли, тогда воспользуемся более умным вариантом:
- SELECT `answer` FROM `bot`
- WHERE MATCH (`message`) AGAINST ("привет" IN NATURAL LANGUAGE MODE) > 0
- ORDER BY RAND() LIMIT 1
4. Если и здесь пусто — выбираем любой ответ из доступных:
- SELECT `answer` FROM `bot` ORDER BY RAND() LIMIT 1
Наиболее интересен, вероятно, второй запрос. Он ищет совпадения основываясь на естественном языке. Вот отличия:
Вот, собственно, и весь мозг.
PHP класс Bot.class.php
Использовать вот так (forum/say.php):
- if ($id == 226621) { // id темы, где будет отвечать бот
- require_once '../modules/Bot.class.php';
- $answer = Bot::byMessage($msg)->getAnswer();
- $realtime++;
- mysql_query("INSERT INTO `forum` SET
- `refid` = '$id',
- `type` = 'm' ,
- `time` = '$realtime',
- `user_id` = '" . Bot::ID . "',
- `from` = '" . Bot::NAME . "',
- `ip` = '0',
- `soft` = '',
- `text` = '" . mysql_real_escape_string($llogin . ', '. $answer) . "'");
- mysql_query("UPDATE `users` SET `lastdate` = '$realtime' WHERE `id` = '" . Bot::ID . "'");
- }
База данных. Часть 2
Четырёх тысяч сообщений явно мало, нужно пополнить базу чем-нибудь таким, что соответствует тематике сайта. Парсить каждое сообщение на форуме не лучший вариант — не ясно где вопрос, где ответ. Но вот сообщения, содержащие цитату, нам вполне подходят. В них уже есть вопрос и прям в том же сообщении ответ.
Сперва достаём из базы форума все сообщения, содержащие цитату:
- SELECT `text` FROM `forum` WHERE `text` REGEXP '^\\[c(=|\])*'
Затем сохраняем дамп. У меня получился файл в 12 Мб, содержащий 38 тысяч записей.
Теперь всё это дело нужно распарсить. Снова зовём на помощь Scala.
- import scala.collection.mutable.HashSet
- import scala.io.Source
- import java.io.File
- object PhrasesExtractor {
- val filterWords = Array( // снова фильтр с матами
- "samobot", "http"
- )
- def main(args: Array[String]) {
- var set = Source.fromFile(args(0), "UTF-8").getLines
- .filter(_.startsWith("('"))
- .map[Phrase](line => Phrase(line.trim))
- .filter(_.censor)
- .toSet
- set.toList.sorted.foreach(println)
- }
- case class Phrase(line: String) extends Ordered[Phrase] {
- val pattern = """^\('\[c[^\u0666]+\\r\\n([^\u0666]+)\[\/c\]([^\u0666]+)'\)[,|;]$""".r
- val t = line match {
- case pattern(msg, ans) => (msg, ans)
- case _ => ("", "")
- }
- val message = messageFilter(t._1.trim)
- val answer = answerFilter(t._2.trim)
- def messageFilter(str: String) = { // Фильтр сообщений
- var s = str.toLowerCase
- // Убираем ники в начале
- s = s.replaceAll("^[\\p{L}\\p{Nd}\\\\_]+, ", "")
- // Убираем спецсимволы и цифры, кроме пробела
- s = s.replaceAll("[^\\p{L} ]+", " ")
- // Убираем одиночные латинские символы, оставшиеся после смайлов
- s = s.replaceAll("\\b\\w\\b", " ")
- s = s.replaceAll("\\s+", " ");
- s = s.trim
- s.replace("'", "''")
- }
- def answerFilter(str: String) = { // Фильтр ответов
- // Убираем ники в начале
- var s = str.replaceAll("^[\\p{L}\\p{Nd}\\\\]+, ", "")
- s = s.trim
- s.replace("'", "''")
- }
- def censor : Boolean = {
- if (message.isEmpty || answer.isEmpty) return false
- if (5 > message.length || message.length > 150) return false
- if (3 > answer.length || answer.length > 200) return false
- for (word <- filterWords)
- if (line.toLowerCase.contains(word)) return false
- true
- }
- // дальше всё как и в первом примере
- }
В итоге мы получили около 16000 пар фраза-ответ, которые можно залить в БД.
На всякий случай скажу с какой проблемой столкнулся в регулярках Scala (и Java). Дело в том, что обычное .* не хочет работать для символов юникода. Пришлось использовать специальные выражения \p{?}. В результате маленькое выражение:
- [.*]+
- [\p{L}\p{Nd}\p{C}\p{P} ]+
В итоге имеем такого монстра:
- ^\('\[c[]+\\r\\n([\p{L}\p{Nd}\p{C}\p{P} ]+)\[\/c\]([\p{L}\p{Nd}\p{C}\p{P} ]+)'\),$
\p{N} — Number — любое представление числа в юникоде (целое, вещественное и т.д.)
\p{P} — Punctuation — знаки пунктуации
\p{C} — Colobok — прочие символы в юникоде
Подробнее об этом можно почитать здесь
Кстати, если вы, как и я, валенок в регулярных выражениях, вам поможет сайт https://regex101.com/ (за ссылку благодарим Ксакепа)
Всем спасибо за прочтение. Теперь интерес к боту пропадёт.