• Курсы Академии Кодебай, стартующие в мае - июне, от команды The Codeby

    1. Цифровая криминалистика и реагирование на инциденты
    2. ОС Linux (DFIR) Старт: 16 мая
    3. Анализ фишинговых атак Старт: 16 мая Устройства для тестирования на проникновение Старт: 16 мая

    Скидки до 10%

    Полный список ближайших курсов ...

Статья Иконка для скрипта Python в трее, всплывающие сообщения и парсинг курса валют

В данной статье я хотел бы сделать обзор на библиотеку, которая позволяет поместить значок приложения в трей, а также добавить к нему контекстное меню с выполнением различных операций при нажатии на них. А также, немного рассказать о новой библиотеке, которая позволяет выводить всплывающие сообщения в трее, как Windows 10, так и Windows 11. Использовать для этих целей мы будем небольшой скрипт на Python, в котором и будет показана работа библиотек.

000.jpg


Что потребуется?

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

Это:
  • pystray – с помощью данной библиотеки мы будем реализовывать значок скрипта в трее;
  • Pillow – данная библиотека потребуется для того, чтобы открыть изображение, которое будет служить иконкой для скрипта в трее;
  • bs4 и lxml – с помощью данных библиотек мы будем парсить html-код страницы, который вернется к нам по запросу;
  • requests – с помощью данной библиотеки мы будем получать html-код страницы по нужному нам адресу;
  • pywin32 – данная библиотека потребуется для того, чтобы свернуть терминал со скриптом на панель задач;
  • win11toast – с помощью данной библиотеки мы будем выводить всплывающие сообщения с интерактивными кнопками (win10toast в связке с python 3.11 работать не хочет, выбрасывает исключение).

Для установки данных библиотек необходимо набрать в терминале следующую команду:

pip install pystray Pillow requests bs4 lxml pywin32 win11toast

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

У меня это выглядит вот так:

01.png


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

Python:
from pathlib import Path

import win32con
import win32console
import win32gui
from PIL import Image
from bs4 import BeautifulSoup
from pystray import Icon, MenuItem, Menu
from requests import get
from win11toast import toast

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


Получение html-кода страницы

Создадим функцию get_text(url: str) -> (str, bool). На вход она получает ссылку на страницу, делает к ней запрос и если статус-код равен 200, возвращает html-код страницы. Если же код будет другим, вернется значение False.
Дополнительно укажем заголовки, в которых пропишем user-agent. Все же, парсить будем Google, а потому, нужно немного притвориться браузером.

Python:
def get_text(url: str) -> (str, bool):
    """
    Получение кода страницы по заданной ссылке.
    """
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/110.0.0.0 Safari/537.36'
    }
    try:
        res = get(url=url, headers=headers)
        return res.text if res.status_code == 200 else False
    except Exception:
        return False


Парсим стоимость валюты

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

Создадим функцию parse_course(url: str) -> (str, bool), которая на входе получает ссылку с запросом, а возвращает полученное значение в случае удачи или False, в случае, если значение получить не удалось.

Выполняем функцию get_text, в которую передаем полученную в функцию ссылку. В зависимости от ответа создаем объект BeautifulSoup, в который передаем полученный код страницы и указываем парсер, с помощью которого будем получать значения. Ищем значение валюты на странице, забираем его в переменную course и, если она не пуста возвращаем его из функции. Если же значение получено не было, или возникло исключение, возвращаем из функции False.

Python:
def parse_course(url: str) -> (str, bool):
    """
    Парсинг стоимости валюты по отношению к рублю.
    """
    try:
        if res := get_text(url):
            soup = BeautifulSoup(res, 'lxml')
            course = soup.find('div', class_='dDoNo ikb4Bb gsrt').find('span', class_='DFlfde SwHCTb').text.strip()
            return course if course else False
        else:
            return False
    except AttributeError:
        return False


Вывод всплывающих сообщений

Создадим функцию notify_send(text: str, url='', icon=None), которая на входе получает текст сообщения, ссылку, по которой был выполнен запрос к странице с курсом валюты, а также словарь, в котором содержится путь к иконке и значение, которое указывает на необходимость переопределения логотипа приложения.

Проверяем не является ли значение icon – None. Такое может быть, если значение курса валют не было получено и вернулась ошибка. Если оно не None, создаем две кнопки, в качестве аргумента первой кнопки указываем ссылку на страницу, а у второй значение, которое закроет сообщение. Таким образом у нас должны быть две кнопки: Открыть в Google и Закрыть. Ну, а далее, выводим сообщение.

Если же значение icon равно None, выводим в сообщении только одну кнопку – Закрыть.

Python:
def notify_send(text: str, url='', icon=None):
    """
    Вывод уведомления с активными кнопками.
    """
    if icon:
        buttons = [{'activationType': 'protocol', 'arguments': f'{url}', 'content': 'Открыть в Google'},
                   {'activationType': 'protocol', 'arguments': 'http:Dismiss', 'content': 'Закрыть'}]
        toast("Курс к рублю", f'{text}', buttons=buttons, icon=icon)
    else:
        toast("Курс к рублю", f'{text}',
              button={'activationType': 'protocol', 'arguments': 'http:Dismiss', 'content': 'Закрыть'}, icon=icon)


Выбор валюты, по которой было получено значение

Создадим функцию notify_choice(course: str, url: str, val: str), которая на входе получает текст с курсом валюты, ссылку по которой был выполнен запрос, значение val, указывающее, по какой из трех валют выполнялся запрос. В зависимости от этого формируем сообщение и отправляем для вывода на экран, в функцию notify_send.

Python:
def notify_choice(course: str, url: str, val: str):
    """
    Выбор сообщения для определенной валюты, в зависимости от значения val.
    """
    if val == "b":
        icon = {
            'src': f'{Path.cwd() / "pic" / "free-dollar.png"}',
            'placement': 'appLogoOverride'
        }
        notify_send(f'Курс ₽1/$1 составляет: {course} руб.за доллар', url, icon)
    elif val == "y":
        icon = {
            'src': f'{Path.cwd() / "pic" / "free-yen.png"}',
            'placement': 'appLogoOverride'
        }
        notify_send(f'Курс ₽1/JP¥1 составляет: {course} руб.за йену', url, icon)
    elif val == "u":
        icon = {
            'src': f'{Path.cwd() / "pic" / "free-yuan.png"}',
            'placement': 'appLogoOverride'
        }
        notify_send(f'Курс ₽1/CN¥1 составляет: {course} руб.за юань', url, icon)


Обработка нажатия на пункт меню иконки в трее

Создадим функцию click(icon: Icon, item: MenuItem), которая на входе получает значения, возвращаемые при нажатии на тот или иной пункт меню. Переводим полученное значение в текст, проверяем чему этот текст равен. В зависимости от этого запрашиваем значение нужной валюты в рублях и отправляем полученное значение в функцию notify_choice для формирования текста и вывода всплывающего сообщения.

Python:
def click(icon: Icon, item: MenuItem):
    """
    Обработка полученных значений меню. В зависимости от выбранного
    пункта выполняется запрос данных по определенной валюте.
    """
    if str(item) == 'доллара':
        url = 'https://www.google.ru/search?q=курс+доллара'
        if course := parse_course(url):
            notify_choice(course, url, "b")
        else:
            notify_send("Данные по курсу не получены")
    elif str(item) == 'йены':
        url = 'https://www.google.ru/search?q=курс+йены'
        if course := parse_course(url):
            notify_choice(course, url, "y")
        else:
            notify_send("Данные по курсу не получены")
    elif str(item) == 'юаня':
        url = 'https://www.google.ru/search?q=курс+юаня'
        if course := parse_course(url):
            notify_choice(course, url, "u")
        else:
            notify_send("Данные по курсу не получены")
    elif str(item) == 'Выход':
        icon.stop()

Нужно обратить внимание, что остановка скрипта происходит при нажатии на пункт меню «Выход». В этом случае обрабатывается команда icon.stop(), которая и завершает работу скрипта.


Формирование меню и запуск скрипта

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

Загрузим иконку для приложения с помощью функции Pillow Image. Создадим переменную icon, в которую поместим объект Icon. В качестве параметров передадим название для созданного объекта, иконку, которая была загружена в переменную image, и начнем создавать меню. Как видно далее, доступно не только создание меню, но, для каждого пункта возможно создание подменю, чем мы и воспользуемся. Создадим два пункта меню у иконки: «Курс к рублю:» и «Выход». Для пункта «Курс к рублю:» создадим подменю, где разместим подменю для доллара, йены и юаня.

Затем выполним команду, которая свернет терминал в «Панель задач» и запустим скрипт на выполнение.

Python:
def main():
    image = Image.open(Path.cwd() / "pic" / "cat.png")
    icon = Icon('Cat Vampire', image, menu=Menu(
        MenuItem('Курс к рублю:', Menu(
            MenuItem('доллара', click),
            MenuItem('йены', click),
            MenuItem('юаня', click)
        )),
        MenuItem('Выход', click)
    ))
    win32gui.ShowWindow(win32console.GetConsoleWindow(), win32con.SW_HIDE)
    icon.run()


if __name__ == "__main__":
    main()

Давайте посмотрим, что у нас получилось. Как видим, иконка скрипта появилась в трее.

02.png


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

03.png


Теперь получим курс какой-нибудь валюты и посмотрим на всплывающее сообщение.

04.png


Данное сообщение имеет две кнопки. При клике на кнопку «Открыть в Google» будет запущен браузер, а при клике на кнопку «Закрыть» сообщение будет закрыто.

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

05.png


Python:
"""
pip install pystray Pillow requests bs4 lxml pywin32 win11toast
"""

from pathlib import Path

import win32con
import win32console
import win32gui
from PIL import Image
from bs4 import BeautifulSoup
from pystray import Icon, MenuItem, Menu
from requests import get
from win11toast import toast


def get_text(url: str) -> (str, bool):
    """
    Получение кода страницы по заданной ссылке.
    """
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
                      'Chrome/110.0.0.0 Safari/537.36'
    }
    try:
        res = get(url=url, headers=headers)
        return res.text if res.status_code == 200 else False
    except Exception:
        return False


def parse_course(url: str) -> (str, bool):
    """
    Парсинг стоимости валюты по отношению к рублю.
    """
    try:
        if res := get_text(url):
            soup = BeautifulSoup(res, 'lxml')
            course = soup.find('div', class_='dDoNo ikb4Bb gsrt').find('span', class_='DFlfde SwHCTb').text.strip()
            return course if course else False
        else:
            return False
    except AttributeError:
        return False


def notify_send(text: str, url='', icon=None):
    """
    Вывод уведомления с активными кнопками.
    """
    if icon:
        buttons = [{'activationType': 'protocol', 'arguments': f'{url}', 'content': 'Открыть в Google'},
                   {'activationType': 'protocol', 'arguments': 'http:Dismiss', 'content': 'Закрыть'}]
        toast("Курс к рублю", f'{text}', buttons=buttons, icon=icon)
    else:
        toast("Курс к рублю", f'{text}',
              button={'activationType': 'protocol', 'arguments': 'http:Dismiss', 'content': 'Закрыть'}, icon=icon)


def notify_choice(course: str, url: str, val: str):
    """
    Выбор сообщения для определенной валюты, в зависимости от значения val.
    """
    if val == "b":
        icon = {
            'src': f'{Path.cwd() / "pic" / "free-dollar.png"}',
            'placement': 'appLogoOverride'
        }
        notify_send(f'Курс ₽1/$1 составляет: {course} руб.за доллар', url, icon)
    elif val == "y":
        icon = {
            'src': f'{Path.cwd() / "pic" / "free-yen.png"}',
            'placement': 'appLogoOverride'
        }
        notify_send(f'Курс ₽1/JP¥1 составляет: {course} руб.за йену', url, icon)
    elif val == "u":
        icon = {
            'src': f'{Path.cwd() / "pic" / "free-yuan.png"}',
            'placement': 'appLogoOverride'
        }
        notify_send(f'Курс ₽1/CN¥1 составляет: {course} руб.за юань', url, icon)


def click(icon: Icon, item: MenuItem):
    """
    Обработка полученных значений меню. В зависимости от выбранного
    пункта выполняется запрос данных по определенной валюте.
    """
    if str(item) == 'доллара':
        url = 'https://www.google.ru/search?q=курс+доллара'
        if course := parse_course(url):
            notify_choice(course, url, "b")
        else:
            notify_send("Данные по курсу не получены")
    elif str(item) == 'йены':
        url = 'https://www.google.ru/search?q=курс+йены'
        if course := parse_course(url):
            notify_choice(course, url, "y")
        else:
            notify_send("Данные по курсу не получены")
    elif str(item) == 'юаня':
        url = 'https://www.google.ru/search?q=курс+юаня'
        if course := parse_course(url):
            notify_choice(course, url, "u")
        else:
            notify_send("Данные по курсу не получены")
    elif str(item) == 'Выход':
        icon.stop()


def main():
    image = Image.open(Path.cwd() / "pic" / "cat.png")
    icon = Icon('Cat Vampire', image, menu=Menu(
        MenuItem('Курс к рублю:', Menu(
            MenuItem('доллара', click),
            MenuItem('йены', click),
            MenuItem('юаня', click)
        )),
        MenuItem('Выход', click)
    ))
    win32gui.ShowWindow(win32console.GetConsoleWindow(), win32con.SW_HIDE)
    icon.run()


if __name__ == "__main__":
    main()

А на этом, пожалуй, все.

Спасибо за внимание. Надеюсь, данное сообщение будет вам полезно
 

Вложения

  • tray_icon_notify.zip
    44,9 КБ · Просмотры: 119
Последнее редактирование модератором:

system-exe

One Level
13.11.2020
3
1
BIT
2
работает.
а как изменять валюты по умолчанию и применять к евро а не рублю
 

Johan Van

Green Team
13.06.2020
352
662
BIT
167
работает.
а как изменять валюты по умолчанию и применять к евро а не рублю

Для начала вам нужно изменить пункты меню, чтобы было понятно, что вы запрашиваете:

01.png

то есть, "Курс к рублю:" меняете, например: "Курс к евро:".
Меняете вложенные меню. Или не меняете. Все зависит от того, какую валюту вы хотите мониторить. Для примера, можно пункт изменить на франк.
Если вы будете менять вложенные меню, значит нужно будет изменить проверку названий:

02.png


Ну и соответственно, вы должны выполнить другой запрос в Google. То есть, для примера:
  • Доллар к евро: https://www.google.ru/search?q=курс+доллара+к+евро
  • Юань к евро: https://www.google.ru/search?q=курс+юаня+к+евро
  • Йена к евро: https://www.google.ru/search?q=курс+йены+к+евро
Парсинг данных измениться не должен. Теги остаются одинаковыми.
 
  • Нравится
Реакции: system-exe

system-exe

One Level
13.11.2020
3
1
BIT
2
БОЛЬШОЕ спасибо за столь развернутый ответ. Прямо удивился. Человек не поленился прям блок-схему изобразил.
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!