Практикум. Створюємо телеграм бота.
Основи Python и Django. -> Створюємо телеграм бота, керованого з файлової системи.'
Пишемо бота Telegram з керуванням через файлову систему.
Завдання.
Нам дають картинки та опис товарів, і з них потрібно зробити каталог.
Цей каталог буде використовуватися телеграм-ботом, який блукає каталогами і пропонуватиме користувачеві товари, відображаючи картинки та опції для навігації по каталогу.
Реалізація.
Картинки та текстові файли складатимемо в каталоги.
У нас буде загальний каталог, у якому кожен бот матиме свій власний, названий його ім’ям.
У каталозі бота буде багато підкаталогів, у кожному з яких буде картинка товару та текстовий файл message.txt з інструкціями для робота зі створення повідомлення при попаданні в цей каталог.
Цей файл містить вміст кожного повідомлення бота оформленого у вигляді xml документа.
Наприклад.
<message>Hello! Welcome</message>
<button value="path_to_one_step">
Do you want to see the catalog?
</button>
<button value="path_to_second_step">
No thank you
</button>
З тегів button будуть створені кнопки, за якими бот стрибатиме в інші директорії, вказані в їхньому атрибуті value.
Для роботи нам знадобляться такі бібліотеки:
python-telegram-bot - для використання API Telegram;
bs4 - для парсингу xml текстового файлу.
Створимо робочий каталог.
mkdir tbot
cd tbot
У ньому файл requirements.txt де перерахуємо необхідні залежності.
python-telegram-bot==12.0.0b1
bs4
Встановимо віртуальне оточення і встановимо пакети.
virtualenv -p python3 venv
source ./venv/bin/activate
pip install -r requirements.txt
Напишемо скрипт інсталяції робота install.py.
На початку створимо нову порожню директорію storage, де зберігатимемо каталоги ботів, тому їх може бути кілька.
Потім, поставимо користувачеві 2 питання з проханням вказати:
-
Ім’я робота.
-
Секретний ключ.
На ім’я бота будемо створювати підкаталоги в папці storage.
Код інсталятора.
#!/usr/bin/env python
import os
print('Устанавливаем бот!')
DATA_DIR = 'storage'
if __name__ == '__main__': # если запуск из консоли
# Створюємо каталог storage якщо його немає
if not os.path.exists(DATA_DIR):
print('Створюю директорію %s' % DATA_DIR)
os.makedirs(DATA_DIR
nname = input('Вкажіть ім'я бота:')
Формуємо шлях до каталогу бота
bot_path = %s/%s % (DATA_DIR, name)
if not os.path.exists(bot_path):
os.makedirs(bot_path)
if not os.path.exists(bot_path+'/index'):
print('Створюю директорію для бота %s' % name)
os.makedirs(bot_path+'/index')
else:
print("Такой бот с именем %s уже есть!" % bot_path)
key = input('Укажите ключ: ')
# записуємо секретний ключ у файл
f = open(%s/key % bot_path, w+)
f.write(key)
f.close()
print("Все готово. Для активації бота запустіть команду ./run.py %s" % name)
Результат роботи із введеним не валідним ключем.
Як зареєструвати бота та отримати секретний ключ (токен).
Потрібно знайти контакт BotFather.
Написати команду /newbot
Підібрати унікальне ім’я, після чого вам надішле повідомлення такого виду.
Done! Congratulations on your new bot. You will find it at t.me/zdimon77_bot. You can now add a description,
about section and profile picture for your bot, see /help for a list of commands.
By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it.
Just make sure the bot is fully operational before you do this.
Use this token to access the HTTP API:
829228816:AAGSjgoh0hfj-fwyMBo4UlGvGxHtnp6Z_сs
Keep your token secure and store it safely, it can be used by anyone to control your bot.
For a description of the Bot API, see this page: https://core.telegram.org/bots/api
Почнемо писати робота у файлі run.py.
Він запускатиметься з одним параметром - ім’ям бота.
Спочатку імпортуємо все, що нам потрібно.
import sys
import os
from telegram.ext import Updater
from telegram.ext import CommandHandler, CallbackContext, CallbackQueryHandler
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram import Bot
from bs4 import BeautifulSoup
from install import DATA_DIR
Забираємо ім’я бота з першого аргументу командного рядка, а якщо його немає, то повідомимо про це і вивалимося з програми.
try:
botname = sys.argv[1]
except:
print("Введите в качестве аргумента имя бота например ./run.py my_bot")
sys.exit()
Знайдемо секретний ключ та використовуючи його, створимо об’єкт бота.
print("Запускаю бота %s" % botname)
bot_path = '%s/%s' % (DATA_DIR,botname)
print("Читаю настройки из %s" % bot_path)
f = open(bot_path+'/key','r')
key = f.read()
f.close()
print("Ключ: %s" % key)
bot = Bot(token=key)
Визначимо функцію start, яка спрацьовуватиме коли користувач натисне кнопку Start у програмі Telergam.
def start(update: Updater, context: CallbackContext):
print("Start command!")
У цю функцію буде передано два об’єкти update та context.
Ми будемо використовувати update для отримання інформації про користувача.
Наприклад, отримати його логін та ідентифікатор кімнати можна так:
username = update.message.from_user['username']
room_id = update.message.chat_id
Ми будемо використовувати цей ідентифікатор для надсилання повідомлень у канал користувача.
Для того, щоб прив’язати цю функцію до обробника події натискання на кнопку “Старт”, потрібно створити об’єкт-обробник класу CommandHandler та “годувати” йому нашу функцію start.
start_handler = CommandHandler('start', start)
Перший параметр це ім’я команди, у разі це зарезервоване ім’я, але може бути довільним і визначено програмістом.
Контейнер для обробників буде об’єкт updater.
updater = Updater(token=key, use_context=True)
Додаємо обробник у контейнер та запускаємо нескінченний процес опитування telegram кімнати бота.
updater.dispatcher.add_handler(start_handler)
updater.start_polling()
Повний код програми.
#!/usr/bin/env python
import sys
import os
from telegram.ext import Updater
from telegram.ext import CommandHandler, CallbackContext, CallbackQueryHandler
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram import Bot
from bs4 import BeautifulSoup
from install import DATA_DIR
try:
botname = sys.argv[1]
except:
print("Введіть як аргумент ім'я бота наприклад ./run.py my_bot")
sys.exit()
print("Запускаю бота %s" % botname)
bot_path = %s/%s % (DATA_DIR,botname)
print("Читаю налаштування з %s" % bot_path)
f = open(bot_path+'/key','r')
key = f.read()
f.close()
bot = Bot(token=key)
def start(update: Updater, context: CallbackContext):
print("Start command!")
username = update.message.from_user['username']
room_id = update.message.chat_id
print("Новий користувач %s room_id: %s" % (username, room_id))
start_handler = CommandHandler('start', start)
updater = Updater(token=key, use_context=True)
updater.dispatcher.add_handler(start_handler)
updater.start_polling()
Результат праці.
Пробуємо надіслати ботом повідомлення привітання.
Це робиться функцією bot.send_message(), в яку, крім повідомлення, передається ідентифікатор кімнати.
def start(update: Updater, context: CallbackContext):
print("Start command!")
username = update.message.from_user['username']
room_id = update.message.chat_id
print("Новый пользователь %s room_id: %s" % (username, room_id))
bot.send_message(room_id, 'Привет!')
Якщо треба надіслати картинку, то зробити це можна іншою функцією.
bot.send_photo(chat_id=room_id, photo=open(img_path, 'rb'))
Тепер ми знаємо як надсилати повідомлення та картинки.
Давайте розбиратися з кнопками і як їх посилати?
Спочатку необхідно визначитися з їх компонуванням.
Допустимо, ми закинули кнопки в список, наприклад:
lst = ['btn1', 'btn2', 'btn3', 'btn4']
Щоб розташувати їх у два ряди по дві, необхідно з цього списку сформувати таку структуру:
[['btn1', 'btn2'], ['btn3', 'btn4']]
Ось невелика функція, яка це робить.
def build_menu(buttons,n_cols):
menu = [buttons[i:i + n_cols] for i in range(0, len(buttons), n_cols)]
return menu
Тут я використовую генератор списку, який формує список із рядом вкладених списків за кількістю другого параметра.
Сама кнопка створюється такою конструкцією.
from telegram import InlineKeyboardButton
btn = InlineKeyboardButton('Натисни мене',callback_data='button_pressed')
При створенні ми визначаємо назву кнопки та корисне навантаження, яке отримаємо після натискання і зможемо визначити, яка саме кнопка була натиснута.
Відправимо 4 тестові кнопки по 2 до ряду.
список із кнопками
btn_list = []
# закидаю 4 штучки
for cnt in range(0,4):
btn_list.append(InlineKeyboardButton('Натисніть мене %s' % cnt,callback_data='button_%s_pressed' % cnt))
# формую об'єкт розмітки із класу InlineKeyboardMarkup
markup_list = InlineKeyboardMarkup(build_menu(btn_list,n_cols=2))
# посилаю кнопки
bot.send_message(room_id, 'Кнопки', reply_markup=markup_list)
Сигнатура функції-обробника для кнопок виглядає за аналогією зі start
def press_button(update: Updater, context: CallbackContext):
print("Pressing button %s" % update.callback_query.data)
Тільки зв’язування обробника відрізняється використанням класу CallbackQueryHandler, який прийме нашу функцію.
updater.dispatcher.add_handler(CallbackQueryHandler(press_button))
Тепер залишилося зв’язати це все разом, і змусити бота переміщатися каталогами і підбирати їх вміст.
У новій функції navigate, яка буде викликатись при натисканні на кнопку, я робитиму наступне:
-
Переходити у потрібний каталог.
-
Посилати картинку image.png якщо вона там є.
-
Прочитати файл message.txt.
-
Посилати вміст тега message та кнопки в тегах button.
Код функції.
def navigate(command, chat_id):
path = '%s/%s' % (bot_path,command)
img_path = '%s/image.png' % path
print(img_path)
# шлю картинку если нашел
if os.path.isfile(img_path):
bot.send_photo(chat_id=chat_id, photo=open(img_path, 'rb'))
message_path = path+'/message.txt'
# находим файл message.txt
if os.path.isfile(message_path):
with open(message_path) as f:
but_txt = f.read()
# парсим xml
soup = BeautifulSoup(but_txt, 'html.parser')
msg = soup.find('message')
btns = soup.findAll('button')
btn_lst = []
# формуємо кнопки
for bt in btns:
btn_lst.append(InlineKeyboardButton(bt.text,callback_data=bt['value']))
button_list = InlineKeyboardMarkup(build_menu(btn_lst,n_cols=1))
# посилаємо кнопки та повідомлення
bot.send_message(chat_id, msg.text, reply_markup=button_list)
Останній пункт використовує бібліотеку BeautifulSoup і реалізовано в такий спосіб.
with open(message_path) as f:
but_txt = f.read()
soup = BeautifulSoup(but_txt, 'html.parser')
msg = soup.find('message') # один элемент
btns = soup.findAll('button') # несколько
Повний код робота:
#!/usr/bin/env python3
import sys
import os
from telegram.ext import Updater
from telegram.ext import CommandHandler, CallbackContext, CallbackQueryHandler
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram import Bot
from bs4 import BeautifulSoup
from install import DATA_DIR
try:
botname = sys.argv[1]
except:
print("Введіть як аргумент ім'я бота наприклад./run.py my_bot")
sys.exit()
print(“Запускаю бота %s” % botname) bot_path = %s/%s % (DATA_DIR,botname) print(“Читаю налаштування з %s” % bot_path) f = open(bot_path+’/key’,’r’) key = f.read() f.close() bot = Bot(token=key)
def navigate(command, chat_id):
path = '%s/%s' % (bot_path,command)
img_path = '%s/image.png' % path
print(img_path)
# шлю картинку якщо знайшов
if os.path.isfile(img_path):
bot.send_photo(chat_id=chat_id, photo=open(img_path, 'rb'))
message_path = path+'/message.txt'
# знаходимо файл message.txt'
# знаходимо файл message.txt
if os.path.isfile(message_path):
with open(message_path) as f:
but_txt = f.read()
# парсим xml
soup = BeautifulSoup(but_txt, 'html.parser')
msg = soup.find('message')
btns = soup.findAll('button')
btn_lst = []
# формируем кнопки
for bt in btns:
btn_lst.append(InlineKeyboardButton(bt.text,callback_data=bt['value']))
button_list = InlineKeyboardMarkup(build_menu(btn_lst,n_cols=1))
# посилаємо кнопки та повідомлення
bot.send_message(chat_id, msg.text, reply_markup=button_list)
def build_menu(buttons,n_cols):
menu = [buttons[i:i + n_cols] for i in range(0, len(buttons), n_cols)]
return menu
def press_button(update: Updater, context: CallbackContext):
print("Pressing button %s" % update.callback_query.data)
navigate(update.callback_query.data,update.callback_query.message.chat_id)
def start(update: Updater, context: CallbackContext):
print("Start command!")
username = update.message.from_user['username']
room_id = update.message.chat_id
navigate('index',update.message.chat_id)
start_handler = CommandHandler('start', start)
updater = Updater(token=key, use_context=True)
updater.dispatcher.add_handler(start_handler)
updater.dispatcher.add_handler(CallbackQueryHandler(press_button))
updater.start_polling()
Результат праці.
Висновки
У статті розглянуто процес створення телеграм-бота мовою Python. В його основу покладено принцип навігації по каталогах та відправлення повідомлень, що містять текст, картинки. та кнопки для навігації до наступного каталогу. Висвітлено механізми управління ботом і реакції на події користувача.
Перспективи
Не вистачає функціонала з прийняття контактної інформації від користувача зі збереженням замовлень у базі даних та повідомлення адміністратора. Про це намагатимуся розповісти у наступній статті.
Основи Python и Django. -> Перевіряємо домени, що закінчуються.
Перевіряємо домени з терміном, що закінчується.
Нещодавно від “насельника” мені надійшло таке завдання:
“Дімич скажи пож ось звідси - https://www.reg.ru/domain/deleted/? Чи можна пограбувати назви доменів?”
Я зайшов за посиланням, побачив список доменів, у яких закінчується термін, посторінкову навігацію та всі справи.
Я подумав “Чому б і ні?” буду пробігати по сторінці, змінюючи параметр page=1,2,3 в урлі, парсить сторінки і зберігати домени.
Що може бути простішим?
Потім надійшло доповнення до завдання. Як виявилося, ці домени потрібно перевіряти на присутність на одному сайті, скажімо yandex.ru:
Наприклад, ось так:
https://yandex.ru/sabai.tv
Куди замість sabai.tv підставляти мої пограбовані доменчики.
З цим начебто проблем бути не повинно. Тим більше, що за відсутності домену в цьому інтернет-каталозі, він повертає 404 помилки.
Прийдеться трохи заддосить напружити цей сайт.
Тож поїхали.
Мені знадобляться дві пітонівські бібліотеки:
requests - для створення HTTP GET запитів
beautifulsoup4 - для парсингу HTML та пошуку корисної інформації на сторінці.
Все це добро складемо у віртуальне оточення.
Створюю та активую його.
mkdir finedomain
cd finedomain
virtualenv venv
source ./venv/bin/activate
Ставлю пакунки.
pip install requests beautifulsoup4
Пишу код із запитом у файлі grabing.py.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Імпортуємо все що треба для роботи import requests from bs4 import BeautifulSoup import json import time import sys # початок програми print(‘Start’) # визначаю рядок з урлом # до неї з кінця приліплюватиму сторінки url = ‘https://www.reg.ru/domain/deleted/?&free_from=12.06.2019&free_to=12.06.2019&order=ASC&sort_field=dname_idn%20%20%20%20&page=’
# запит першої сторінки
r = requests.get(url+'0')
print(r.text)
Результат.
Непогано, HTML отримали, далі потрібно розпарити це все добро beautifulsoup-ом.
soup = BeautifulSoup(r.text, 'html.parser')
Тепер у змінній soup у нас не тільки весь код сторінки, але й інструменти для пошуку її елементів.
Відкидаємо інспектор браузера і дивимося, де у нас список.
Ага, таблиця з класом b-table__content. Немає нічого простішого її витягнути так.
table = soup.find(“table”,{“class”: “b-table__content”})
find() - шукає один елемент findAll() - знайде список
Далі отримаю tr-ки (рядки) з тега tbody.
tbody = table.find("tbody")
trs = tbody.findAll("tr")
Втечу за ними циклом, знайду td-шки (комірки таблиці) і виведу з першої за рахунком доменне ім’я.
soup = BeautifulSoup(r.text, 'html.parser')
table = soup.find("table",{"class": "b-table__content"})
tbody = table.find("tbody")
trs = tbody.findAll("tr")
for tr in trs:
tds = tr.findAll("td")
print(tds[0].text)
Спрацювало!
Тепер створимо список для зберігання доменів та дати закінчення, і в циклі заповню його.
Повний код вийшов таким.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Імпортуємо все що треба для роботи
import requests
from bs4 import BeautifulSoup
import json
import time
# початок програми
print('Start')
# визначаю рядок з урлом
# до неї з кінця приліплюватиму сторінки
url = 'https://www.reg.ru/domain/deleted/?&free_from=12.06.2019&free_to=12.06.2019&order=ASC&sort_field=dname_idn%20%20%20%20&page='
# запит першої сторінки
r = requests.get(url+'0')
# тут зберігатиму домени
data = []
# парсим html
soup = BeautifulSoup(r.text, 'html.parser')
# знаходимо першу таблицю за тегом та css класом
table = soup.find("table",{"class": "b-table__content"})
# в ній блок tbody на ім'я тега
tbody = table.find("tbody")
# рядки
trs = tbody.findAll("tr")
for tr in trs:
# осередки
tds = tr.findAll("td")
# додаємо домен та термін закінчення до списку
data.append({"domain": tds[0].text, "date_expire": tds[2].text})
print(data)
Час перевірити ці домени на сайті.
Зроблю при цьому функцію, яка приймає доменне ім’я параметром.
def check_domain(domain):
# формую url, адресу підставляючи домен у рядок
url = 'https://yandex.ru/%s' %domain
# пробую зробити запит
try:
# отримую сторінку
r = requests.get(url)
if r.status_code == 404: # перевіряю статус
print('Domain %s result %s' % (domain, 'FAIL!'))
return False
else:
print('Domain %s result %s' % (domain, 'SUCCESS'))
# якщо пощастило і домен знайшли
return True
# це якщо запит обрушився з якоїсь причини
except:
print('Request error %s' % url)
return False
Залишилось викликати цю функцію у циклі.
for tr in trs:
# ячейки
tds = tr.findAll("td")
# додаємо домен та термін закінчення до списку
data.append({"domain": tds[0].text, "date_expire": tds[2].text})
# перевіряю домен
check_domain(tds[0].text)
Результат не втішає і попадань на першій сторінці немає, проте працює!
Тепер мені потрібно визначити кінець посторінкової “нафігації” та прогнати процес по всіх доменах на зазначену дату.
У найкращих традиціях функціонального програмування виконую функцію.
def check_end(soup):
'''Визначаю кінець сторінки '''
try:
# на кінцевій сторінці було помічено заголовок h1 з текстом
'''
Доменів, що відповідають заданим критеріям фільтрації, не знайдено.
'''
h2 = soup.find("h2")
# тут для пошуку сказало, що потрібно спочатку кодувати в UTF-8
if h2.text.encode('utf-8').find('відповідають заданим критеріям')>0:
return True
else:
return False
except:
return False
Мучу цикл while доки дійшло остаточно з лічильником сторінок.
is_end = False
page = 0
while not is_end:
# визначаю рядок з урлом
# до неї з кінця приліплюватиму сторінки
url = 'https://www.reg.ru/domain/deleted/?&free_from=12.06.2019&free_to=12.06.2019&order=ASC&sort_field=dname_idn%20%20%20%20&page=%s' % page
print("Роблю сторінку %s" % page)
# додаємо лічильник сторінок
page = page + 1
запит першої сторінки
r = requests.get(url)
# тут зберігатиму домени
data = []
# парсим html
soup = BeautifulSoup(r.text, 'html.parser')
# Парсим html
soup = BeautifulSoup(r.text, 'html.parser')
# перевіримо кінець постранички
if check_end(soup):
is_end = True
break # ламаємо цикл
# знаходимо першу таблицю за тегом та css класом
table = soup.find("table",{"class": "b-table__content"})
# в ній блок tbody на ім'я тега
tbody = table.find("tbody")
# рядки
trs = tbody.findAll("tr")
for tr in trs:
# осередки
tds = tr.findAll("td")
# додаємо домен та термін закінчення до списку
data.append({"domain": tds[0].text, "date_expire": tds[2].text})
# перевіряю домен
check_domain(tds[0].text)
Вирішив перервати процес ctrl+c, отримав такий облом
Довелося гасити термінал, образливе непорозуміння.
Сталося воно у цьому блоці під час перевірки домену:
except:
print('Request error %s' % url)
return False
А якщо я відпрацюю виняток KeyboardInterrupt?
try:
# отримую сторінку
...
except KeyboardInterrupt: # вихід ctrl+c
print("Ну поки що!")
sys.exit() # нахабно вивалюється з проги
# це якщо запит обрушився з якоїсь іншої причини
except:
print('Request error %s' % url)
return False
Набагато краще!
Залишилося забрати дату у користувача десь на початку скрипту.
print("Введи дату типу 12.06.2019: ")
user_date = raw_input() #
print("Працюємо по {}".format(user_date))
Просто input() не канає! Використовуйте тільки raw_input.
І вставити її до урла запиту.
Наприкінці я вивантажу успішні домени у файл.
def save_success(domain):
with open('success.txt', 'a+') as of: # открываем для добавления и создаем если нет
of.write(domain+';')
print('Domain %s result %s' % (domain, 'SUCCESS'))
Повний код програми..
#!/usr/bin/env python
# -*- coding: utf-8 -*-
Імпортуємо все що треба для роботи import requests from bs4 import BeautifulSoup import json import time import sys # початок програми print(‘Починаємо грабувати домени.’) print(“Вводь за яку дату? (типу 12.06.2019): “) user_date = raw_input() print(“Працюємо по {}”.format(user_date))
def save_success(domain):
with open('success.txt', 'a+') as of: # відкриваємо для додавання і створюємо якщо ні
of.write(domain+';')
print('Domain %s result %s' % (domain, 'SUCCESS'))
def check_domain(domain):
# формую url, адресу підставляючи домен у рядок
url = 'https://yandex.ru/%s' %domain
# пробую зробити запит
try:
# отримую сторінку
r = requests.get(url)
if r.status_code == 404: # перевіряю статус
print('Domain %s result %s' % (domain, 'FAIL!'))
return False
else:
print('Domain %s result %s' % (domain, 'SUCCESS'))
# якщо пощастило і домен знайшли
return True
except KeyboardInterrupt: # вихід ctrl+c
print("Ну поки що!")
sys.exit() # нагло вываливаемся из проги
# це якщо запит обрушився з якоїсь причини
except:
print('Request error %s' % url)
return False
def check_end(soup):
''' Визначаю кінець посторінки ''
try:
# на кінцевій сторінці було помічено заголовок h1 з текстом
'''
Доменів, що відповідають заданим критеріям фільтрації, не знайдено.
'''
h2 = soup.find("h2")
# тут для пошуку сказало, що потрібно спочатку кодувати в UTF-8
if h2.text.encode('utf-8').find('відповідають заданим критеріям')>0:
return True
else:
return False
except:
return False
is_end = False
page = 0
while not is_end:
# визначаю рядок з урлом
# до неї з кінця приліплюватиму сторінки
url = 'https://www.reg.ru/domain/deleted/?&free_from=%s&free_to=%s&order=ASC&sort_field=dname_idn%20%20%20%20&page=%s' % (user_date,user_date,page)
print("Роблю сторінку %s" % page)
# додаємо лічильник сторінок
page = page + 1
# запит першої сторінки
r = requests.get(url)
# тут зберігатиму домени
data = []
# Парсим html
soup = BeautifulSoup(r.text, 'html.parser')
# перевіримо кінець постранички
if check_end(soup):
is_end = True
break # ламаємо цикл
знаходимо першу таблицю за тегом та css класом
table = soup.find("table",{"class": "b-table__content"})
# в ній блок tbody на ім'я тега
tbody = table.find("tbody")
# рядки
trs = tbody.findAll("tr")
for tr in trs:
# осередки
tds = tr.findAll("td")
# додаємо домен та термін закінчення до списку
data.append({"domain": tds[0].text, "date_expire": tds[2].text})
# перевіряю домен
if check_domain(tds[0].text):
save_success(tds[0].text)
print('The end')
Висновки.
У статті висвітлено прийоми програмування мовою Python на прикладі реального, практичного завдання.
Викладено основні функції Python бібліотек requests та BeautifulSoup для отримання вмісту сторінки , пошуку та вилучення з неї корисної інформації зі збереженням у текстовий файл.GIT](https://github.com/zdimon/finedomain)
Основи Python и Django. -> Створення інтернет-магазину. Частина 2.
Створення інтернет-магазину. Частина 2.
Створення програми Django.
django-admin.py startproject prj
cd prj
./manage.py migrate
Створення програми shop
./manage.py startapp shop
Додамо додаток у проект.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'shop'
]
Створимо модель (shop/models.py).
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=250)
name_slug = models.CharField(max_length=250)
class Subcategory(models.Model):
name = models.CharField(max_length=250)
name_slug = models.CharField(max_length=250)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
class Good(models.Model):
name = models.CharField(max_length=250)
name_slug = models.CharField(max_length=250)
desc = models.TextField()
subcategory = models.ForeignKey(Subcategory, on_delete=models.SET_NULL, null=True)
class Image(models.Model):
good = models.ForeignKey(Good, on_delete=models.SET_NULL, null=True)
image = models.ImageField()
Опишемо класи адмін інтерфейсу та прив’яжемо їх до моделі (shop/admin.py).
from django.contrib import admin
from .models import *
class CategoryAdmin(admin.ModelAdmin):
pass
class SubcategoryAdmin(admin.ModelAdmin):
pass
class GoodAdmin(admin.ModelAdmin):
pass
class ImageAdmin(admin.ModelAdmin):
pass
admin.site.register(Category, CategoryAdmin)
admin.site.register(Subcategory,SubcategoryAdmin)
admin.site.register(Good, GoodAdmin)
admin.site.register(Image, ImageAdmin)
Створимо нову команду імпорту даних (shop/management/commands/import.py).
from django.core.management.base import BaseCommand, CommandError
from shop.models import *
class Command(BaseCommand):
def handle(self, *args, **options):
print('Importing data')
** Не забуваємо створювати файли ini.py у нових каталогах management та commands**
Основи Python и Django. -> Створення інтернет-магазину. Частина 1.
Створення інтернет-магазину. Частина 1.
На початку подумаємо з чого складається типовий інтернет-магазин? Я виділив 3 основні функціональні блоки:
Каталог товарів. Кошик користувача. Оформлення замовле
Ці три частини складають основу і без них неможливо уявити роботу магазину. Почнемо із першої частини т.к. вона найбільш трудомістка.
Каталог товарів.
Якщо ви вирішили створити свій інтернет-магазин, то, очевидно, знаєте приклади готових рішень, які вам підходять найбільше і відображають вашу ідею та асортимент продукції. Нерозумно цим не скористатися і не скопіювати інформацію з інших сайтів. Я говорю саме про інформацію про товари, ціни, картинки, опис і т.д. Наявність інструменту для автоматичного збору подібних даних може заощадити колосальну кількість часу, яка потрібна для наповнення вашого майбутнього інтернет магазину. Найпростішим буде створення деякого скрипту, який би ходив сайтом-прикладом і збирав все необхідне, складаючи у вигляді певної структури папок і файлів. Далі цю структуру ми передамо іншій програмі, яка заповнить нашу базу даних і сам сайт.
Тепер обговоримо структуру каталогу даних, назвемо його data.
Припустимо, ми продаємо штори та хочемо створити інтернет магазин штор. Візьмемо якийсь приклад. У видачі “магазин штор” у мене першим вийшов https://pangardin.com.ua Думаю, було б логічним підкаталоги назвати ім’ям інтернет-ресурсу. Але крім каталогу продукції, на всіх сайтах присутні і статичні сторінки з інформацією про власника, доставку, зворотний зв’язок та ін. Думаю що подібну інформацію можна оформити у спеціальних текстових файлах у корені каталогу data.
Орієнтовна структура каталогу даних:
. data
..pages
...main.txt
...about.txt
...contact.txt
..pangardin.com.ua
...tul
...shtory
...furnitura
Як видно, ми розбиваємо товари за каталогами, називатимемо їх каталог-категоріями, при цьому всередині таких категорій необхідний якийсь мета файл з інформацією про цю категорію.
Можна скористатися форматом yml, як на мене, це найбільш читальний варіант оформлення структурованих даних.
Так і назвемо цей файл – meta.yml. З урахуванням багатомовності цей файл може виглядати так:
content_type: category
is_published: true
order: 1
title_ru: Тюль.
title_ua: Тюль.
desc_ru: |
Самая лучшая <strong>тюль</strong>!
desc_ua: |
Дуже гарна тюль!
meta_title_ru: Купить тюль. Интернет-магазин штор.
meta_title_ua: Створення iнтернет магазина штор
meta_keywords_ru: тюль шторы купить интернет-магазин
meta_keywords_ua: ….
meta_description_ru: ….
meta_description_ua: ….
Я заложил минимум информации о каталоге. Первым делом я определяю тип информации, для того чтобы отличать описание категории и товара и определять в каком каталоге находится программа. Либо это каталог-категория, либо каталог-товар. Далее идет признак опубликованности на сайте, сортировка и информация для страницы категории.
Подобным образом можно оформить и статичные страницы в файлах *.yml в каталоге pages. Например так:
content_type: page
is_published: true
title_ru: Разработка интернет-магазина штор.
desc_ru: |
Как разработать интернет магазин на <strong>python</strong>!
Примерная структура каталога товарной позиции:
.tul-metis
..images
...1.png
...2.png
meta.yml
description.md
Для опису товару я визначив файл description.md, який використовує формат markdown для розмітки тексту, але дозволяє використовувати і html нативний html.
Файл опису товарної позиції.
name_slug: shtory-raduga
name_ru: Веселка штори.
meta_title_ua: Перші кроки Python.
meta_keywords_ua: Перші кроки Python.
meta_description_ua: Перші кроки Python.
is_published: false
Для простоти, поки оперуватимемо однією, російською локалізацією.
Отже, наше наступне завдання – опитати сайт-джерело скриптом та створити необхідну структуру каталогів. Для цього я використовуватиму бібліотеки Python requests і BeatifulSoup.
Бібліотекою requests ми будемо генерувати запити HTTP на сервер і отримувати HTML код сторінок.
Бібліотекою BeatifulSoup ми цей HTML парсим і знаходимо в ньому елементи (теги), що цікавлять, і забираємо їх вміст.
На початку нас цікавить сама структура каталогу. Її можна знайти на головній сторінці у спливаючому блоці.
HTML код цього списку виглядає так:
<ul class="sub-menu">
<li class="menu-item ..."><h4 class='mega_menu_title'>Категорії</h4>
<ul class="sub-menu">
<li class="menu-item ..."><a href="..."><span class="avia-bullet"></span>Тканини-компаньйони/a></li>
...
Я прибрав зайві css класи для наочності. Що ми маємо - це два вкладені списки ul з категоріями та підкатегоріями.
Підкатегорії, крім назви, мають і посилання на сторінку з товарами, її ми також використовуватимемо для отримання інформації про продукти кожної взятої категорії.
Усі наведені нижче приклади наведено для операційної системи на базі Debian.
Створення віртуального оточення.
virtualenv -p python3 venv3
Після створення віртуального оточення необхідно його активувати, набравши таку команду в консолі.
. ./venv3/bin/activate
Встановити наші бібліотеки можна такими командами.
pip install requests
pip install beatifulsoup4
Якщо у вас не встановлена програма pip, то можна поставити її так:
sudo apt install python-pip
Нижче наведено Python код із докладними коментарями, який розбирає структуру категорій та виводить її на екран терміналу.
#!/usr/bin/env python
# імпортуємо бібліотеки
import requests
from bs4 import BeautifulSoup
print("Getting catalog")
# робимо запит на отримання головної сторінки
url = 'https://pangardin.com.ua'
r = requests.get(url)
# розпізнаємо HTML, створюючи спеціальний об'єкт soup
soup = BeautifulSoup(r.text, 'html.parser')
# нзнаходимо головний (батьківський) елемент списку категорій
ulsout = soup.find('ul',{'class': 'sub-menu'})
# у циклі проходимо по всіх його елементах
for ulout in ulsout:
liout = ulout.find('li')
if liout != -1:
print('.'+liout.text)
# для ккожної батьківської категорії знаходимо дочірній список підкатегорій
ulins = ulout.find('ul')
# у циклі проходимо за підкатегоріями
for liin in ulins.findAll('li'):
print('..'+liin.text)
Висновок програми:
.Тканини-Компаньйони
..Тканини-Компаньйони
..Тюль
..Портьєрні тканини
..Готові штори
..Штори нитки
..Постільна білизна
..Фурнітура
….
Все що тут відбувається – це перебираються ul – списки у двох вкладених циклах.
Тепер залишилося перевести назви до трансліту та створити потрібні каталоги.
Транслітувати можна бібліотекою python-slugify і робиться це передачею потрібного рядка у функцію slugify.
name_slug = slugify(name)
Для зручності програму завжди поділяють на дрібніші підпрограми, наприклад функції.
Функцію збереження категорії на диск можна описати так:
def save_category(cat_name,parent_name):
''' охорона каталогів-категорій '''
# транслітеруємо російську назву категорії
cat_name_slug = slugify(cat_name)
# транслітеруємо російську назву підкатегорії
parent_name_slug = slugify(parent_name)
# виводимо налагоджувальне повідомлення в консоль
print("Creating category %s with parent %s" % (cat_name,parent_name))
# формуємо шлях для створення каталогу
dir_name = os.path.join(DATA_DIR,cat_name_slug)
# створюємо каталог, якщо його не існує
if not os.path.isdir(dir_name):
os.mkdir(dir_name)
Для того, щоб використовувати таку функцію, її необхідно викликати, передавши два параметри - назву категорії та підкатегорії.
Це робитимемо у внутрішньому циклі.
for liin in ulins.findAll('li'):
save_category(liin.text, liout.text)
Повний код програми та результат його роботи наведено нижче.
#!/usr/bin/env python
import requests
from bs4 import BeautifulSoup
import os
from slugify import slugify
### створюю каталоги з даними
if not os.path.isdir('data'):
os.mkdir('data')
if not os.path.isdir(os.path.join('data','pangardin.com.ua')):
os.mkdir(os.path.join('data','pangardin.com.ua'))
###
DATA_DIR = os.path.join('data','pangardin.com.ua')
def save_category(cat_name,parent_name):
''' сохранение каталогов-категорий '''
cat_name_slug = slugify(cat_name)
parent_name_slug = slugify(parent_name)
print("Creating category %s with parent %s" % (cat_name,parent_name))
dir_name = os.path.join(DATA_DIR,cat_name_slug)
if not os.path.isdir(dir_name):
os.mkdir(dir_name)
print("Getting catalog")
url = 'https://pangardin.com.ua'
r = requests.get(url)
soup = BeautifulSoup(r.text, 'html.parser')
ulsout = soup.find('ul',{'class': 'sub-menu'})
for ulout in ulsout:
liout = ulout.find('li')
if liout != -1:
ulins = ulout.find('ul')
for liin in ulins.findAll('li'):
save_category(liin.text, liout.text)
Доповнимо функцію save_category створенням meta.yml файлів, що мінімально описують категорії.
У ці файли будемо писати імена та псевдоніми поточної та батьківської категорії.
def save_category(cat_name,parent_name):
''' береження каталогів-категорій '''
# транслітеруємо російську назву категорії
cat_name_slug = slugify(cat_name)
# транслітеруємо російську назву підкатегорії
parent_name_slug = slugify(parent_name)
# виводимо налагоджувальне повідомлення в консоль
print("Creating category %s with parent %s" % (cat_name,parent_name))
# формуємо шлях для створення каталогу
dir_name = os.path.join(DATA_DIR,cat_name_slug)
# створюємо каталог, якщо його не існує
if not os.path.isdir(dir_name):
os.mkdir(dir_name)
# створюємо рядок в який вставляємо назви категорій
meta_info = '''
content_type: category
is_published: true
name_slug: %s
name_ru: %s
parent_slug: %s
parent_name_ru: %s
order: 1
descr: |
''' % (cat_name_slug,cat_name,parent_name_slug,parent_name)
with open(os.path.join(dir_name,'meta.yml'),'w') as f:
f.write(meta_info)
Функція проходу сторінкою каталогу
def save_goods(url,category):
# Отримуємо сторінку категорії
r = requests.get(url)
soup = BeautifulSoup(r.text, 'html.parser')
# знаходимо список товарів
products = soup.find('ul',{'class': 'products'}).findAll('li')
for product in products:
# Заголовок
title = product.find('div',{'class': 'inner_product_header'}).find('h3').text
title_slug = slugify(title)
# фоормуємо шлях
path = os.path.join(DATA_DIR,category,title_slug)
# створюємо каталог товару
print("Saving ... %s" % title_slug)
if not os.path.isdir(path):
os.mkdir(path)
# знаходимо посилання на сторінку товару
link = product.find('div',{"class": "inner_cart_button"}).find('a').get('href')
# зберігаємо позицію
save_position(link,path)
Поглянемо на те, як виглядає HTML код позиції товару на сторінці категорії
https://pangardin.com.ua/category/complect/artdeco/
<li class="...">
<div class="inner_product wrapped_style ">
<div class="avia_cart_buttons single_button">
<div class="inner_cart_button">
<a href="https://pangardin.com.ua/magazin/ballades/" >Перегляд товару/a>
</div>
</div>
<a href="https://pangardin.com.ua/magazin/ballades/">
<div class="thumbnail_container">
<img src="..." class="attachment-shop_catalog wp-post-image" alt="Ballade">
</div>
<div class="inner_product_header">
<h3>Ballades</h3>
<span class="price"><span class="amount">208грн.</span>
<span class="amount">366грн.</span>
</span>
</div>
</a>
</div>
</li>
Після отримання елемента li
products = soup.find(‘ul’,{‘class’: ‘products’}).findAll(‘li’)
ми вибрали блок із класом inner_product_header і використовували заголовок для знаходження назви товару.
itle = product.find(‘div’,{‘class’: ‘inner_product_header’}).find(‘h3’).text
Тепер необхідно отримати URL адресу сторінки з описом товару.
link = product.find(‘div’,{“class”: “inner_cart_button”}).find(‘a’).get(‘href’)
Опишемо функцію збереження позиції.
def save_position(link,path):
r = requests.get(link)
with open('log.html','w') as f:
f.write(r.text)
soup = BeautifulSoup(r.text, 'html.parser')
print(link)
#sys.exit('s')
# знаходимо посилання на зображення
img_link = soup.find('a',{"class": "MagicZoomPlus"}).get('href')
print("Downloading pic %s" % img_link)
# забираємо картинку
r = requests.get(img_link, stream=True)
# створюємо каталог images
path_to_img = os.path.join(path,'images')
if not os.path.isdir(path_to_img):
os.mkdir(path_to_img)
# зберігаємо картинку
image_full_path = os.path.join(path_to_img,'1.png')
if r.status_code == 200:
with open(image_full_path, 'wb') as f:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
# знаходимо назву та опис
title = soup.find('h1',{"class": "product_title"}).text
description = soup.find('div',{"itemprop": "description"}).text
# формуємо вміст meta.yml
meta_info = '''
name_slug: %s
name_ru: %s
meta_title_ru: %s
meta_keywords_ru: %s
meta_description_ru: %s
is_published: true
description_ru: |
%s
''' % (slugify(title),title,title,title,title,description)
# запись в файл
with open(os.path.join(path,'meta.yml'),'w') as f:
f.write(meta_info)
При пошуку картинки ми спочатку знаходимо елемент div з класом inner_cart_button, а потім у ньому посилання, з якого атрибут href. Завантаживши сторінку опису товару знайдемо елемент із зображенням у вихідному коді.
<a class="MagicZoomPlus" href="https://pangardin.com.ua/wp-content/uploads/2013/04/DSC00047.jpg" >
<img itemprop="image" src="..." >
<div class="MagicZoomPup">
</div>
...
</a>
Отримати на нього посилання можна таким рядком коду, де ми шукаємо за класом MagicZoomPlus.
img_link = soup.find('a',{"class": "MagicZoomPlus"}).get('href')
Щоб отримати більш докладну і “чисту” інформацію про товар знадобиться деяка вправність у роботі зі структурою HTML сторінки.
Швидше за все, треба буде відсівати зайву інформацію та вибирати те, що вам потрібно для майбутнього сайту.
Приклад нашого сайту-джерела написаний на Wordpress і для кожного нового сайту знадобиться написати свій власний парсер контенту.