Практикум. Створюємо телеграм бота.

Основи 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 питання з проханням вказати:

  1. Ім’я робота.

  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)

Результат роботи із введеним не валідним ключем.

python requests

Як зареєструвати бота та отримати секретний ключ (токен).

Потрібно знайти контакт 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()

Результат праці.

python requests

Пробуємо надіслати ботом повідомлення привітання.

Це робиться функцією 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, 'Привет!')

python requests

Якщо треба надіслати картинку, то зробити це можна іншою функцією.

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)

python requests

Сигнатура функції-обробника для кнопок виглядає за аналогією зі 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, яка буде викликатись при натисканні на кнопку, я робитиму наступне:

  1. Переходити у потрібний каталог.

  2. Посилати картинку image.png якщо вона там є.

  3. Прочитати файл message.txt.

  4. Посилати вміст тега 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 requests

Висновки

У статті розглянуто процес створення телеграм-бота мовою Python. В його основу покладено принцип навігації по каталогах та відправлення повідомлень, що містять текст, картинки. та кнопки для навігації до наступного каталогу. Висвітлено механізми управління ботом і реакції на події користувача.

Перспективи

Не вистачає функціонала з прийняття контактної інформації від користувача зі збереженням замовлень у базі даних та повідомлення адміністратора. Про це намагатимуся розповісти у наступній статті.

Посилання на репозиторій GIT

Основи Python и Django. -> Перевіряємо домени, що закінчуються.

Перевіряємо домени з терміном, що закінчується.

Нещодавно від “насельника” мені надійшло таке завдання:

“Дімич скажи пож ось звідси - https://www.reg.ru/domain/deleted/? Чи можна пограбувати назви доменів?”

Я зайшов за посиланням, побачив список доменів, у яких закінчується термін, посторінкову навігацію та всі справи.

start page

Я подумав “Чому б і ні?” буду пробігати по сторінці, змінюючи параметр page=1,2,3 в урлі, парсить сторінки і зберігати домени.

Що може бути простішим?

Потім надійшло доповнення до завдання. Як виявилося, ці домени потрібно перевіряти на присутність на одному сайті, скажімо yandex.ru:

Наприклад, ось так:

https://yandex.ru/sabai.tv

Куди замість sabai.tv підставляти мої пограбовані доменчики.

З цим начебто проблем бути не повинно. Тим більше, що за відсутності домену в цьому інтернет-каталозі, він повертає 404 помилки.

start page

Прийдеться трохи заддосить напружити цей сайт.

Тож поїхали.

Мені знадобляться дві пітонівські бібліотеки:

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)

Результат.

python requests

Непогано, HTML отримали, далі потрібно розпарити це все добро beautifulsoup-ом.

soup = BeautifulSoup(r.text, 'html.parser')

Тепер у змінній soup у нас не тільки весь код сторінки, але й інструменти для пошуку її елементів.

Відкидаємо інспектор браузера і дивимося, де у нас список.

python requests

Ага, таблиця з класом 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)

Спрацювало!

python requests

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

Повний код вийшов таким.

#!/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)

python requests

Час перевірити ці домени на сайті.

Зроблю при цьому функцію, яка приймає доменне ім’я параметром.

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)

python requests

Результат не втішає і попадань на першій сторінці немає, проте працює!

Тепер мені потрібно визначити кінець посторінкової “нафігації” та прогнати процес по всіх доменах на зазначену дату.

У найкращих традиціях функціонального програмування виконую функцію.

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)

python requests

Вирішив перервати процес ctrl+c, отримав такий облом

python requests

Довелося гасити термінал, образливе непорозуміння.

Сталося воно у цьому блоці під час перевірки домену:

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

python requests

Набагато краще!

Залишилося забрати дату у користувача десь на початку скрипту.

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 і для кожного нового сайту знадобиться написати свій власний парсер контенту.