1
0
Fork 0
mirror of https://github.com/EDeev/npr_meteobot.git synced 2026-10-08 04:59:33 +03:00

Compare commits

...

5 commits

Author SHA1 Message Date
69830f156f README на русском и английском 2026-10-02 23:36:49 +00:00
646943c21e requirements.txt и CI (ruff, создание базы) 2026-10-02 23:36:49 +00:00
28a5170be0 Мелкие исправления по ruff: дублирующееся имя обработчика /weather, неиспользуемые переменные 2026-10-02 23:36:49 +00:00
f223570bab Токен из окружения, база создаётся сама
- токен бота — из BOT_TOKEN, а не из config.py;
- sub.db (с идентификатором подписчика) убран из git: SQLighter создаёт таблицы и
  служебные строки из schema.sql, если базы нет.
2026-10-02 23:36:49 +00:00
be4411e269 Переводы строк приведены к LF 2026-10-02 23:33:14 +00:00
12 changed files with 480 additions and 475 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
* text=auto eol=lf
*.db binary

21
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- run: pip install ruff==0.14.0
- name: Ruff (синтаксис и ошибки)
run: ruff check --select E9,F .
- name: База создаётся из schema.sql
run: |
python -c "import sql; db = sql.SQLighter('/tmp/t.db'); db.add_user('1'); assert db.user_exists('1')"

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
__pycache__/
*.py[cod]
.venv/
.env
*.db

41
README.en.md Normal file
View file

@ -0,0 +1,41 @@
# NPR MeteoBot
[Русский](README.md) · **English**
[![CI](https://github.com/EDeev/npr_meteobot/actions/workflows/ci.yml/badge.svg)](https://github.com/EDeev/npr_meteobot/actions/workflows/ci.yml)
A Telegram bot for schoolchildren in the Norilsk industrial area: it sends class cancellations due to frost
and wind ("aktirovka") for the user's shift and district and shows the weather forecast.
**Status:** school project (2022), completed, archived
**Stack:** Python 3.10 · aiogram 2 · SQLite · Selenium + BeautifulSoup (city administration site, Gismeteo) · pymorphy2
- Subscription to cancellations by shift (1 or 2) and district: Norilsk, Talnakh, Oganer, Kayerkan
- Regular checks of the administration site and broadcasts to subscribers
- Weather forecast for the day and up to 10 days ahead (Gismeteo data)
## Running
```bash
pip install -r requirements.txt
BOT_TOKEN=token_from_BotFather python bot.py
```
The `sub.db` database is created on first run from `schema.sql`. Cancellations need Chrome: the
administration site is read with Selenium.
## License
School project (2022). The code is open for study; there is no separate license.
## Author
**Egor Deev** — [GitHub](https://github.com/EDeev) · [Telegram](https://t.me/DeevEgor) · [egor@deev.space](mailto:egor@deev.space)
---
<div align="center">
<sub>⭐ If you find this project useful, give it a star on GitHub!</sub>
<p><sub>Made with ❤️ — <a href="https://deev.space">deev.space</a></sub></p>
</div>

138
README.md
View file

@ -1,139 +1,41 @@
# NPR MeteoBot # NPR MeteoBot
Автоматизированная система мониторинга метеорологических условий и уведомлений об актировках учебных занятий для Норильского промышленного района (НПР). **Русский** · [English](README.en.md)
## 📋 Описание проекта [![CI](https://github.com/EDeev/npr_meteobot/actions/workflows/ci.yml/badge.svg)](https://github.com/EDeev/npr_meteobot/actions/workflows/ci.yml)
NPR MeteoBot представляет собой интеллектуальную систему на базе Telegram-бота, предназначенную для: Telegram-бот для школьников Норильского промышленного района: присылает актировки (отмену занятий
- Мониторинга погодных условий в четырех районах НПР из-за мороза и ветра) для своей смены и района и показывает прогноз погоды.
- Автоматического отслеживания актировок учебных занятий
- Персонализированных уведомлений пользователей
- Предоставления детализированных метеорологических данных
### Основные возможности: **Статус:** школьный проект (2022), завершён, в архиве
- **Персонализация**: Настройка по смене обучения и району проживания
- **Автоматизация**: Регулярный мониторинг официальных источников
- **Уведомления**: Система подписок с гибкими настройками
- **Актуальность**: Интеграция с Gismeteo и официальным сайтом города
## 🏗️ Архитектура системы **Стек:** Python 3.10 · aiogram 2 · SQLite · Selenium + BeautifulSoup (сайт администрации, Gismeteo) · pymorphy2
### Компоненты: - Подписка на актировки по смене (1 или 2) и району: Норильск, Талнах, Оганер, Кайеркан
- `bot.py` - Основной модуль Telegram-бота с обработкой команд - Регулярная проверка сайта администрации и рассылка подписчикам
- `sql.py` - Модуль работы с базой данных SQLite - Прогноз погоды на день и на 10 дней вперёд (данные Gismeteo)
- `info.py` - Парсинг данных с внешних источников
- `config.py` - Конфигурационные параметры системы
### Структура базы данных: ## Запуск
- **users**: Профили пользователей с настройками
- **weather**: Кэш метеорологических данных
- **parts**: Данные об актировках по сменам
- **data**: Системная информация
## 🛠️ Технологический стек ```bash
### Backend:
- **Python 3.7+** - Основной язык разработки
- **aiogram 2.x** - Фреймворк для Telegram Bot API
- **SQLite** - Локальная база данных
- **asyncio** - Асинхронное программирование
### Парсинг данных:
- **Selenium WebDriver** - Автоматизация браузера
- **BeautifulSoup4** - HTML-парсинг
- **requests** - HTTP-запросы
### Обработка данных:
- **pymorphy2** - Морфологический анализ русского языка
## 🚀 Установка и настройка
### Предварительные требования:
```python
# Установка ChromeDriver для Selenium
# Ubuntu/Debian:
sudo apt-get install chromium-chromedriver
# Windows: загрузить с https://chromedriver.chromium.org/
```
### Установка зависимостей:
```python
pip install -r requirements.txt pip install -r requirements.txt
BOT_TOKEN=токен_от_BotFather python bot.py
``` ```
### Конфигурация: База `sub.db` создаётся при первом запуске из `schema.sql`. Для актировок нужен Chrome: сайт
1. Получите токен бота в [@BotFather](https://t.me/BotFather) администрации читается через Selenium.
2. Обновите `API_TOKEN` в `config.py`
3. Создайте структуру базы данных SQLite
### Запуск: ## Лицензия
```python
python bot.py
```
## 📊 Функциональные возможности Школьный проект (2022). Код открыт для изучения, отдельной лицензии нет.
### Команды бота: ## Автор
- `/start` - Инициализация и настройка профиля
- `/town` - Изменение города и смены обучения
- `/acta` - Просмотр текущих актировок
- `/weather` - Детализированный прогноз погоды
- `/storm` - Штормовые предупреждения
- `/letter` - Управление подпиской на уведомления
- `/notice` - Настройка типа уведомлений
### Автоматические процессы: **Деев Егор Викторович** — [GitHub](https://github.com/EDeev) · [Telegram](https://t.me/DeevEgor) · [egor@deev.space](mailto:egor@deev.space)
- **timer()** - Мониторинг актировок (каждую минуту)
- **wind()** - Обновление погодных данных (каждые 20 минут)
- **clear()** - Очистка устаревших данных (каждые 5 минут)
## 🎯 Целевые районы
Система обслуживает четыре района НПР:
- **Центральный район** (Норильск)
- **Район Талнах**
- **Район Оганер**
- **Район Кайеркан**
## 🔧 Конфигурационные параметры
### Временные интервалы мониторинга:
- Прогноз погоды: 1, 4, 7, 10, 13, 16, 19, 22 часа
- Проверка актировок: ежеминутно
- Обновление погоды: каждые 20 минут
### Источники данных:
- **Актировки**: [norilsk-city.ru/meteo](https://www.norilsk-city.ru/meteo/)
- **Погода**: [gismeteo.ru](https://www.gismeteo.ru/)
## 📈 Масштабируемость
Система спроектирована для:
- Обработки 17,000+ активных пользователей
- Асинхронной обработки запросов
- Отказоустойчивой работы с внешними API
## ⚠️ Ограничения
- Зависимость от доступности внешних источников данных
- Требует настройки ChromeDriver для корректного парсинга
- Локальная SQLite база данных (не подходит для кластерного развертывания)
## 📝 Лицензия
Проект разработан для образовательных и некоммерческих целей.
## 👥 Автор
**Деев Егор Викторович** - Backend Developer
- GitHub: [@EDeev](https://github.com/EDeev)
- Email: egor@deev.space
- Telegram: [@Egor_Deev](https://t.me/Egor_Deev)
--- ---
<div align="center"> <div align="center">
<sub>Разработано с использованием современных технологий асинхронного программирования и автоматизации веб-парсинга.</sub> <sub>⭐ Если проект оказался полезным, поставьте звёздочку на GitHub!</sub>
<p><sub>Создано с ❤️ от вашего дорогого - deev.space ©</sub></p> <p><sub>Сделано с ❤️ — <a href="https://deev.space">deev.space</a></sub></p>
</div> </div>

524
bot.py
View file

@ -1,262 +1,262 @@
import config, asyncio, info import config, asyncio, info
import logging, pymorphy2, time import logging, pymorphy2, time
from aiogram import Bot, Dispatcher, executor, types from aiogram import Bot, Dispatcher, executor, types
from sql import SQLighter from sql import SQLighter
from datetime import datetime as dt from datetime import datetime as dt
# задаем уровень логов # задаем уровень логов
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
# инициализируем бота # инициализируем бота
bot = Bot(token=config.API_TOKEN) bot = Bot(token=config.API_TOKEN)
dp = Dispatcher(bot) dp = Dispatcher(bot)
keyboard = types.InlineKeyboardMarkup() keyboard = types.InlineKeyboardMarkup()
morph = pymorphy2.MorphAnalyzer() morph = pymorphy2.MorphAnalyzer()
# инициализируем соединение с БД # инициализируем соединение с БД
db = SQLighter('sub.db') db = SQLighter('sub.db')
# СТАРТОВЫЕ КОМАНДЫ # СТАРТОВЫЕ КОМАНДЫ
@dp.message_handler(commands=['start']) @dp.message_handler(commands=['start'])
async def start(message: types.Message): async def start(message: types.Message):
if not db.user_exists(message.from_user.id): if not db.user_exists(message.from_user.id):
# если юзера нет в базе, добавляем его # если юзера нет в базе, добавляем его
db.add_user(message.from_user.id) db.add_user(message.from_user.id)
buttons = [types.InlineKeyboardButton(text="1-я смена", callback_data="prt1"), buttons = [types.InlineKeyboardButton(text="1-я смена", callback_data="prt1"),
types.InlineKeyboardButton(text="2-я смена", callback_data="prt2")] types.InlineKeyboardButton(text="2-я смена", callback_data="prt2")]
keyboard = types.InlineKeyboardMarkup(row_width=2) keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.add(*buttons) keyboard.add(*buttons)
await message.answer(f"Привет, {message.from_user.first_name}!\nЯ - погодный бот, созданный для выдачи информации о" await message.answer(f"Привет, {message.from_user.first_name}!\nЯ - погодный бот, созданный для выдачи информации о"
f" вероятных актировках. Прошу вас выбрать вашу смену обучения!\n\n<i>Для последующей " f" вероятных актировках. Прошу вас выбрать вашу смену обучения!\n\n<i>Для последующей "
f"смены: города или смены обучения воспользуйтесь командой <b>/town</b></i>", f"смены: города или смены обучения воспользуйтесь командой <b>/town</b></i>",
parse_mode=types.ParseMode.HTML, reply_markup=keyboard) parse_mode=types.ParseMode.HTML, reply_markup=keyboard)
@dp.message_handler(content_types=['sticker']) @dp.message_handler(content_types=['sticker'])
async def stick(message: types.Message): async def stick(message: types.Message):
print(message.sticker.file_id) print(message.sticker.file_id)
@dp.message_handler(commands=['town']) @dp.message_handler(commands=['town'])
async def town(message: types.Message): async def town(message: types.Message):
buttons = [types.InlineKeyboardButton(text="1-я смена", callback_data="prt1"), buttons = [types.InlineKeyboardButton(text="1-я смена", callback_data="prt1"),
types.InlineKeyboardButton(text="2-я смена", callback_data="prt2")] types.InlineKeyboardButton(text="2-я смена", callback_data="prt2")]
keyboard = types.InlineKeyboardMarkup(row_width=2) keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.add(*buttons) keyboard.add(*buttons)
await message.answer("<b>Прошу вас выбрать вашу смену обучения!</b>", parse_mode=types.ParseMode.HTML, await message.answer("<b>Прошу вас выбрать вашу смену обучения!</b>", parse_mode=types.ParseMode.HTML,
reply_markup=keyboard) reply_markup=keyboard)
# ИЛАЙН КЛАВИАТУРА # ИЛАЙН КЛАВИАТУРА
@dp.callback_query_handler(text="prt1") @dp.callback_query_handler(text="prt1")
async def part_one(call: types.CallbackQuery): async def part_one(call: types.CallbackQuery):
db.update_user(call.message.chat.id, 3, 1) db.update_user(call.message.chat.id, 3, 1)
buttons = [types.InlineKeyboardButton(text="Норильск", callback_data="nor"), buttons = [types.InlineKeyboardButton(text="Норильск", callback_data="nor"),
types.InlineKeyboardButton(text="Талнах", callback_data="tal"), types.InlineKeyboardButton(text="Талнах", callback_data="tal"),
types.InlineKeyboardButton(text="Оганер", callback_data="oga"), types.InlineKeyboardButton(text="Оганер", callback_data="oga"),
types.InlineKeyboardButton(text="Каеркан", callback_data="kae")] types.InlineKeyboardButton(text="Каеркан", callback_data="kae")]
keyboard = types.InlineKeyboardMarkup(row_width=2) keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.add(*buttons) keyboard.add(*buttons)
await call.message.answer(text="<b><i>Отлично!</i></b> Теперь вам надо выбрать район в котором вы проживаете " await call.message.answer(text="<b><i>Отлично!</i></b> Теперь вам надо выбрать район в котором вы проживаете "
"на территории НПР!", "на территории НПР!",
parse_mode=types.ParseMode.HTML, reply_markup=keyboard) parse_mode=types.ParseMode.HTML, reply_markup=keyboard)
@dp.callback_query_handler(text="prt2") @dp.callback_query_handler(text="prt2")
async def part_two(call: types.CallbackQuery): async def part_two(call: types.CallbackQuery):
db.update_user(call.message.chat.id, 3, 2) db.update_user(call.message.chat.id, 3, 2)
buttons = [types.InlineKeyboardButton(text="Норильск", callback_data="nor"), buttons = [types.InlineKeyboardButton(text="Норильск", callback_data="nor"),
types.InlineKeyboardButton(text="Талнах", callback_data="tal"), types.InlineKeyboardButton(text="Талнах", callback_data="tal"),
types.InlineKeyboardButton(text="Оганер", callback_data="oga"), types.InlineKeyboardButton(text="Оганер", callback_data="oga"),
types.InlineKeyboardButton(text="Каеркан", callback_data="kae")] types.InlineKeyboardButton(text="Каеркан", callback_data="kae")]
keyboard = types.InlineKeyboardMarkup(row_width=2) keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.add(*buttons) keyboard.add(*buttons)
await call.message.answer(text="<b><i>Конечно я вам не завидую, но куда деваться)</i></b> Теперь вам надо выбрать " await call.message.answer(text="<b><i>Конечно я вам не завидую, но куда деваться)</i></b> Теперь вам надо выбрать "
"район в котором вы проживаете на территории НПР!", "район в котором вы проживаете на территории НПР!",
parse_mode=types.ParseMode.HTML, reply_markup=keyboard) parse_mode=types.ParseMode.HTML, reply_markup=keyboard)
@dp.callback_query_handler(text=["nor", "tal", "oga", "kae"]) @dp.callback_query_handler(text=["nor", "tal", "oga", "kae"])
async def function(call: types.CallbackQuery): async def function(call: types.CallbackQuery):
txt = call.data txt = call.data
if txt == "nor": if txt == "nor":
db.update_user(call.message.chat.id, 4, 1) db.update_user(call.message.chat.id, 4, 1)
elif txt == "tal": elif txt == "tal":
db.update_user(call.message.chat.id, 4, 2) db.update_user(call.message.chat.id, 4, 2)
elif txt == "oga": elif txt == "oga":
db.update_user(call.message.chat.id, 4, 3) db.update_user(call.message.chat.id, 4, 3)
elif txt == "kae": elif txt == "kae":
db.update_user(call.message.chat.id, 4, 4) db.update_user(call.message.chat.id, 4, 4)
await call.message.answer(text='<b>Прекрасно!</b> Вы заполнили данные, теперь если вы хотите подписаться на ' await call.message.answer(text='<b>Прекрасно!</b> Вы заполнили данные, теперь если вы хотите подписаться на '
'утреннюю рассылку об актировках воспользуйтесь командой <b>/letter</b>.\n\n' 'утреннюю рассылку об актировках воспользуйтесь командой <b>/letter</b>.\n\n'
'<i><b>Для полного ознакомления можете воспользоваться командой /help</b></i>', '<i><b>Для полного ознакомления можете воспользоваться командой /help</b></i>',
parse_mode=types.ParseMode.HTML) parse_mode=types.ParseMode.HTML)
# КОМАНДЫ АКТИВАЦИИ ПОДПИСОК # КОМАНДЫ АКТИВАЦИИ ПОДПИСОК
@dp.message_handler(commands=['notice']) @dp.message_handler(commands=['notice'])
async def notification(message: types.Message): async def notification(message: types.Message):
if str(message.chat.id) not in db.get_send(): if str(message.chat.id) not in db.get_send():
db.update_user(message.chat.id, 2, 1) db.update_user(message.chat.id, 2, 1)
await message.answer("Увидомление приходит каждый день не зависимо от наличия актировки!") await message.answer("Увидомление приходит каждый день не зависимо от наличия актировки!")
else: else:
db.update_user(message.chat.id, 2, 0) db.update_user(message.chat.id, 2, 0)
await message.answer("Уведомление приходит, только если есть актировка!") await message.answer("Уведомление приходит, только если есть актировка!")
@dp.message_handler(commands=['letter']) @dp.message_handler(commands=['letter'])
async def letter(message: types.Message): async def letter(message: types.Message):
if str(message.chat.id) not in db.get_subs(): if str(message.chat.id) not in db.get_subs():
db.update_user(message.chat.id, 1, 1) db.update_user(message.chat.id, 1, 1)
await message.answer("Вы подписались на рассылку!") await message.answer("Вы подписались на рассылку!")
else: else:
db.update_user(message.chat.id, 1, 0) db.update_user(message.chat.id, 1, 0)
await message.answer("Вы отписались от расслыки!") await message.answer("Вы отписались от расслыки!")
@dp.message_handler(commands=['storm']) @dp.message_handler(commands=['storm'])
async def storm(message: types.Message): async def storm(message: types.Message):
await message.answer(info.storm(), parse_mode=types.ParseMode.MARKDOWN) await message.answer(info.storm(), parse_mode=types.ParseMode.MARKDOWN)
@dp.message_handler(commands=['acta']) @dp.message_handler(commands=['acta'])
async def act(message: types.Message): async def act(message: types.Message):
mail = f"<b>Актировка за {'.'.join(reversed(str(db.get_data()[0]).split('-')))}</b>\n" mail = f"<b>Актировка за {'.'.join(reversed(str(db.get_data()[0]).split('-')))}</b>\n"
for part in [1, 2]: for part in [1, 2]:
mail = mail + f"\n<b>Для {part}-ой смены:</b>\n" mail = mail + f"\n<b>Для {part}-ой смены:</b>\n"
if db.check_acta(part): if db.check_acta(part):
cities = config.NAMES cities = config.NAMES
city = db.get_acta(part) city = db.get_acta(part)
for i, town in enumerate(city): for i, town in enumerate(city):
if str(town) != "нет": if str(town) != "нет":
mail = mail + f"<b>*</b> В {morph.parse(cities[i])[0].inflect({'loct'}).word.title()} актировка " \ mail = mail + f"<b>*</b> В {morph.parse(cities[i])[0].inflect({'loct'}).word.title()} актировка " \
f"с 1 по {town} классы!\n" f"с 1 по {town} классы!\n"
else: else:
mail = mail + f"<b>*</b> В {morph.parse(cities[i])[0].inflect({'loct'}).word.title()} актировки нет!\n" mail = mail + f"<b>*</b> В {morph.parse(cities[i])[0].inflect({'loct'}).word.title()} актировки нет!\n"
else: else:
mail = mail + "На данный момент информации на сайте нету!\n\n" mail = mail + "На данный момент информации на сайте нету!\n\n"
await message.answer(mail, parse_mode=types.ParseMode.HTML) await message.answer(mail, parse_mode=types.ParseMode.HTML)
@dp.message_handler(commands=['weather']) @dp.message_handler(commands=['weather'])
async def storm(message: types.Message): async def weather_command(message: types.Message):
text = message.text.split() text = message.text.split()
try: try:
day = int(text[1]) day = int(text[1])
if 0 <= day <= 9: if 0 <= day <= 9:
weather = db.get_weather(day) weather = db.get_weather(day)
txt = "<b>Погода:</b>\n\n" txt = "<b>Погода:</b>\n\n"
for hour in weather: for hour in weather:
dat = weather[hour] dat = weather[hour]
if len(dat[1]) == 2: if len(dat[1]) == 2:
par = f"{dat[1][0]}-{dat[1][1]}" par = f"{dat[1][0]}-{dat[1][1]}"
else: else:
par = str(dat[1][0]) par = str(dat[1][0])
txt = txt + f"<b>В {hour}:</b>\n Температура: {dat[0]}\n Скорость и порывы ветра: {par}\n Направление ветра: {dat[2]}\n Осадки: {dat[3]}\n\n" txt = txt + f"<b>В {hour}:</b>\n Температура: {dat[0]}\n Скорость и порывы ветра: {par}\n Направление ветра: {dat[2]}\n Осадки: {dat[3]}\n\n"
await message.answer(txt, parse_mode=types.ParseMode.HTML) await message.answer(txt, parse_mode=types.ParseMode.HTML)
else: else:
raise NameError('Число не вподходящем диапозоне!') raise NameError('Число не вподходящем диапозоне!')
except Exception: except Exception:
await message.answer("Введи число дня по счёту от сегодня, погода которого тебе нужна!\n\nТо есть:\nСегодня - {/waether 0}\nЗавтра - {/weather 1}", await message.answer("Введи число дня по счёту от сегодня, погода которого тебе нужна!\n\nТо есть:\nСегодня - {/waether 0}\nЗавтра - {/weather 1}",
parse_mode=types.ParseMode.MARKDOWN) parse_mode=types.ParseMode.MARKDOWN)
@dp.message_handler(commands=['help']) @dp.message_handler(commands=['help'])
async def main(message: types.Message): async def main(message: types.Message):
await message.answer('*Инструкция использования:*\n\n' await message.answer('*Инструкция использования:*\n\n'
'*/town* - с помощью этой команды вы можете изменить данные о городе и смене обучения\n' '*/town* - с помощью этой команды вы можете изменить данные о городе и смене обучения\n'
'*/acta* - команда, выдающая данные об актировке на все города на данный момент\n' '*/acta* - команда, выдающая данные об актировке на все города на данный момент\n'
'*/storm* - команда, выдающая данные о штормовом предупреждении\n' '*/storm* - команда, выдающая данные о штормовом предупреждении\n'
'*/letter* - команда, позваляющая подписаться на утреннюю рассылку о наличии актировки\n' '*/letter* - команда, позваляющая подписаться на утреннюю рассылку о наличии актировки\n'
'*/notice* - команда, дающая выбор - получать уведомления в любом случае или только при ' '*/notice* - команда, дающая выбор - получать уведомления в любом случае или только при '
'наличии актировки\n', types.ParseMode.MARKDOWN) 'наличии актировки\n', types.ParseMode.MARKDOWN)
# УВЕДОМЛЕНИЕ # УВЕДОМЛЕНИЕ
async def timer(wait_for): async def timer(wait_for):
while True: while True:
await asyncio.sleep(wait_for) await asyncio.sleep(wait_for)
city, part = info.act() city, part = info.act()
if city: if city:
date_old, part_old = db.get_data() date_old, part_old = db.get_data()
date_now, part_num = str(dt.now().date()), int(part[0]) date_now, part_num = str(dt.now().date()), int(part[0])
if (part_num == 1 and date_old != date_now) or (part_num == 2 and date_old == date_now): if (part_num == 1 and date_old != date_now) or (part_num == 2 and date_old == date_now):
if part_old != part_num: if part_old != part_num:
db.update_data(dt.now().date(), part_num) db.update_data(dt.now().date(), part_num)
db.save_acta(city, part_num) db.save_acta(city, part_num)
for twn in city: for twn in city:
town = morph.parse(twn[0][1])[0].inflect({'datv'}).word town = morph.parse(twn[0][1])[0].inflect({'datv'}).word
if isinstance(twn[1], int): if isinstance(twn[1], int):
for s in db.get_users(part_num, twn[0][0]): for s in db.get_users(part_num, twn[0][0]):
try: try:
await bot.send_sticker(chat_id=s, sticker=config.ID_STICKERS + config.STICKERS[1]) await bot.send_sticker(chat_id=s, sticker=config.ID_STICKERS + config.STICKERS[1])
await bot.send_message(chat_id=s, text=f"*Для {part} по {town.title()} объявлена актировка c 1 по {twn[1]} классы!*", await bot.send_message(chat_id=s, text=f"*Для {part} по {town.title()} объявлена актировка c 1 по {twn[1]} классы!*",
parse_mode=types.ParseMode.MARKDOWN) parse_mode=types.ParseMode.MARKDOWN)
except Exception as e: except Exception as e:
print(repr(e)) print(repr(e))
db.update_status(s) db.update_status(s)
else: else:
for s in db.get_sends(part_num, twn[0][0]): for s in db.get_sends(part_num, twn[0][0]):
try: try:
await bot.send_sticker(chat_id=s, sticker=config.ID_STICKERS + config.STICKERS[6]) await bot.send_sticker(chat_id=s, sticker=config.ID_STICKERS + config.STICKERS[6])
await bot.send_message(chat_id=s, text=f"*Для {part} по {town.title()} актировка отсутствует!", await bot.send_message(chat_id=s, text=f"*Для {part} по {town.title()} актировка отсутствует!",
parse_mode=types.ParseMode.MARKDOWN) parse_mode=types.ParseMode.MARKDOWN)
except Exception as e: except Exception as e:
print(repr(e)) print(repr(e))
db.update_status(s) db.update_status(s)
print("---------------------------------") print("---------------------------------")
# ОБНОВЛЕНИЕ ДАННЫХ О ПОГОДЫ # ОБНОВЛЕНИЕ ДАННЫХ О ПОГОДЫ
async def wind(wait_for): async def wind(wait_for):
while True: while True:
await asyncio.sleep(wait_for) await asyncio.sleep(wait_for)
for moment in db.get_moments(): for moment in db.get_moments():
weather = info.weather(moment) weather = info.weather(moment)
if type(weather) == dict: if type(weather) == dict:
for tm in weather: for tm in weather:
db.update_weather(weather[tm], moment, tm) db.update_weather(weather[tm], moment, tm)
else: else:
print(weather) print(weather)
time.sleep(10) time.sleep(10)
# ОЧИСТКА ДАННЫХ ОБ АКТИРОВКАХ # ОЧИСТКА ДАННЫХ ОБ АКТИРОВКАХ
async def clear(wait_for): async def clear(wait_for):
while True: while True:
await asyncio.sleep(wait_for) await asyncio.sleep(wait_for)
if (db.get_data()[0] != str(dt.now().date())) and (db.check_acta(1) or db.check_acta(2)): if (db.get_data()[0] != str(dt.now().date())) and (db.check_acta(1) or db.check_acta(2)):
db.del_acta() db.del_acta()
# ЗАПУСК ЛОНГ ПОЛЛИНГА # ЗАПУСК ЛОНГ ПОЛЛИНГА
if __name__ == '__main__': if __name__ == '__main__':
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.create_task(timer(60)) # ПРОВЕРКА МЕТЕО НОРИЛЬСК КАЖДУЮ МИНУТУ loop.create_task(timer(60)) # ПРОВЕРКА МЕТЕО НОРИЛЬСК КАЖДУЮ МИНУТУ
loop.create_task(clear(300)) # ПРОВЕРКА НА НОВЫЙ ДЕНЬ КАЖДЫЕ 10 МИНУТ loop.create_task(clear(300)) # ПРОВЕРКА НА НОВЫЙ ДЕНЬ КАЖДЫЕ 10 МИНУТ
loop.create_task(wind(1200)) # ПРОВЕРКА НА ОБНОВЛЕНИЕ ПОГОДЫ КАЖДЫЕ 20 МИНУТ loop.create_task(wind(1200)) # ПРОВЕРКА НА ОБНОВЛЕНИЕ ПОГОДЫ КАЖДЫЕ 20 МИНУТ
executor.start_polling(dp, skip_updates=True) executor.start_polling(dp, skip_updates=True)

View file

@ -1,19 +1,21 @@
API_TOKEN = 'token from BotFather' # ОСНОВНОЙ import os
# API_TOKEN = 'token from BotFather' # ТЕСТЫ
# Токен бота от @BotFather — из переменной окружения BOT_TOKEN
CITY = {'Центральный район': [1, 'Норильск'], 'район Талнах': [2, 'Талнах'], API_TOKEN = os.environ.get('BOT_TOKEN', '')
'ж/о Оганер': [3, 'Оганер'], 'район Кайеркан': [4, 'Кайеркан']}
CITY = {'Центральный район': [1, 'Норильск'], 'район Талнах': [2, 'Талнах'],
NAMES = ['Норильск', 'Талнах', 'Оганер', 'Кайеркан'] 'ж/о Оганер': [3, 'Оганер'], 'район Кайеркан': [4, 'Кайеркан']}
TIME = [1, 4, 7, 10, 13, 16, 19, 22] NAMES = ['Норильск', 'Талнах', 'Оганер', 'Кайеркан']
ID_STICKERS = r"CAACAgIAAxkBAAID" TIME = [1, 4, 7, 10, 13, 16, 19, 22]
STICKERS = {"пасмурно": r"2mIKSBBtFNR1uHLhdztkhVwqTxRRAAKHEgACljNZSAgtQYHfmE4WIwQ", # облако
"поземок": r"22IKSBIdUSxApfTAEk5mMjldDexZAAIPFwAClIBRSCSyRnG71n6CIwQ", # солнце и облако ID_STICKERS = r"CAACAgIAAxkBAAID"
1: r"3GIKSBRW4sCgYX8AAaq01U6DaNF1IwACXhMAAlzDUUirRDtYEkGJ3iME", # ветер STICKERS = {"пасмурно": r"2mIKSBBtFNR1uHLhdztkhVwqTxRRAAKHEgACljNZSAgtQYHfmE4WIwQ", # облако
4: r"3WIKSBec5Ch8vL-czNkXikARo-JmAALmFAACGHZRSHmiNc0n7VZPIwQ", # гроза "поземок": r"22IKSBIdUSxApfTAEk5mMjldDexZAAIPFwAClIBRSCSyRnG71n6CIwQ", # солнце и облако
"снег": r"3mIKSBpHLfkJOnI4HCQny_a_XSVbAAI8FAACSixRSN7lm05cAAEj0yME", # снегопад 1: r"3GIKSBRW4sCgYX8AAaq01U6DaNF1IwACXhMAAlzDUUirRDtYEkGJ3iME", # ветер
6: r"32IKSByANsmxbok8VePu7k6mTzexAALMFgACVopQSHXJa0spfMe7IwQ"} # ясно 4: r"3WIKSBec5Ch8vL-czNkXikARo-JmAALmFAACGHZRSHmiNc0n7VZPIwQ", # гроза
"снег": r"3mIKSBpHLfkJOnI4HCQny_a_XSVbAAI8FAACSixRSN7lm05cAAEj0yME", # снегопад
# await message.answer_sticker(ID_STICKERS + STICKERS[1]) 6: r"32IKSByANsmxbok8VePu7k6mTzexAALMFgACVopQSHXJa0spfMe7IwQ"} # ясно
# await message.answer_sticker(ID_STICKERS + STICKERS[1])

152
info.py
View file

@ -1,77 +1,77 @@
# - * - coding: utf-8 - * - # - * - coding: utf-8 - * -
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import requests, config import requests, config
def act(): def act():
try: try:
soup = BeautifulSoup(requests.get('https://www.norilsk-city.ru/meteo/').text, 'html.parser') soup = BeautifulSoup(requests.get('https://www.norilsk-city.ru/meteo/').text, 'html.parser')
acta = [list(map(lambda x: x.get_text(), _.find_all("p"))) for _ in soup.find_all("tbody")[0] if _ != "\n"] acta = [list(map(lambda x: x.get_text(), _.find_all("p"))) for _ in soup.find_all("tbody")[0] if _ != "\n"]
count = '1-ой смены' if "1" in str(soup.find_all("div", count = '1-ой смены' if "1" in str(soup.find_all("div",
class_="textContent", class_="textContent",
limit=1)[0].find_all("p", limit=1)[0].find_all("p",
limit=1)[0].get_text()) else '2-ой смены' limit=1)[0].get_text()) else '2-ой смены'
for lst in acta: for lst in acta:
if "нет" in lst[1]: if "нет" in lst[1]:
acta[acta.index(lst)] = [config.CITY[lst[0]], "нет"] acta[acta.index(lst)] = [config.CITY[lst[0]], "нет"]
else: else:
acta[acta.index(lst)] = [config.CITY[lst[0]], int(lst[1].split("-")[1].split()[0])] acta[acta.index(lst)] = [config.CITY[lst[0]], int(lst[1].split("-")[1].split()[0])]
return sorted(acta), count return sorted(acta), count
except Exception as e: except Exception:
return [], "" return [], ""
def storm(): def storm():
soup = BeautifulSoup(requests.get('https://www.norilsk-city.ru/meteo/').text, 'html.parser') soup = BeautifulSoup(requests.get('https://www.norilsk-city.ru/meteo/').text, 'html.parser')
message = str(str(soup.find_all("p")[13])[35:-40]) message = str(str(soup.find_all("p")[13])[35:-40])
stm = "*" + str(str(soup.find_all("h2")[1])[4:-5]) + \ stm = "*" + str(str(soup.find_all("h2")[1])[4:-5]) + \
"*\n\n" + message[:-67] + " " + message[96:-36] + message[118:-24] + message[129:-1] "*\n\n" + message[:-67] + " " + message[96:-36] + message[118:-24] + message[129:-1]
return stm return stm
def weather(moment): def weather(moment):
try: try:
# путь к драйверу chrome # путь к драйверу chrome
chrome_options = Options() chrome_options = Options()
chrome_options.add_argument("--headless") chrome_options.add_argument("--headless")
chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('--no-sandbox')
chromedriver = 'chromedriver' chromedriver = 'chromedriver'
browser = webdriver.Chrome(executable_path=chromedriver, chrome_options=chrome_options) browser = webdriver.Chrome(executable_path=chromedriver, chrome_options=chrome_options)
# После успешного входа в систему переходим на страницу «Gismeteo» # После успешного входа в систему переходим на страницу «Gismeteo»
browser.get(f'https://www.gismeteo.ru/weather-norilsk-3957/{moment}') browser.get(f'https://www.gismeteo.ru/weather-norilsk-3957/{moment}')
# Получение HTML-содержимого # Получение HTML-содержимого
requiredHtml = browser.page_source requiredHtml = browser.page_source
soup = BeautifulSoup(requiredHtml, 'html.parser') soup = BeautifulSoup(requiredHtml, 'html.parser')
# weather = soup.find_all() # weather = soup.find_all()
temp = soup.find_all("span", class_="unit unit_temperature_c")[6:] temp = soup.find_all("span", class_="unit unit_temperature_c")[6:]
wind = soup.find_all("span", class_=["wind-unit unit unit_wind_m_s", "wind-unit unit unit_wind_m_s warning"])[:8] wind = soup.find_all("span", class_=["wind-unit unit unit_wind_m_s", "wind-unit unit unit_wind_m_s warning"])[:8]
dire = soup.find_all("div", class_="direction") dire = soup.find_all("div", class_="direction")
prec = soup.find_all("div", class_="item-unit") prec = soup.find_all("div", class_="item-unit")
k, s, weather = 0, 0, {} weather = {}
for i in range(8): for i in range(8):
par = wind[i].get_text().split()[0].split('-') par = wind[i].get_text().split()[0].split('-')
if len(par) == 1: if len(par) == 1:
par = [int(par[0])] par = [int(par[0])]
else: else:
par = [int(par[0]), int(par[1])] par = [int(par[0]), int(par[1])]
pre = prec[i].get_text() pre = prec[i].get_text()
# pre = prec[i].get_text().split('\n') # pre = prec[i].get_text().split('\n')
# precti = pre[1].split(' ')[-1] if len(pre) > 1 else pre[0] # precti = pre[1].split(' ')[-1] if len(pre) > 1 else pre[0]
weather[config.TIME[i]] = [temp[i].get_text(), par, dire[i].get_text(), pre] weather[config.TIME[i]] = [temp[i].get_text(), par, dire[i].get_text(), pre]
return weather return weather
except Exception as e: except Exception as e:
return [repr(e)] return [repr(e)]

6
requirements.txt Normal file
View file

@ -0,0 +1,6 @@
aiogram==2.25.2
pymorphy2==0.9.1
pymorphy2-dicts-ru==2.4.417127.4579844
selenium==4.15.2
beautifulsoup4==4.12.3
requests==2.31.0

15
schema.sql Normal file
View file

@ -0,0 +1,15 @@
-- Схема базы бота и служебные строки, которые он ожидает (подписчиков нет)
CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, id_user VARCHAR (255) NOT NULL,
rating INTEGER (4) NOT NULL DEFAULT (0), status BOOLEAN NOT NULL DEFAULT (False),
send BOOLEAN NOT NULL DEFAULT (True), part INTEGER (1) NOT NULL, city INTEGER (1) NOT NULL);
CREATE TABLE IF NOT EXISTS parts (part INTEGER PRIMARY KEY AUTOINCREMENT, nor VARCHAR (3), tal VARCHAR (3),
oga VARCHAR (3), kae VARCHAR (3));
CREATE TABLE IF NOT EXISTS weather (id STRING, "1" VARCHAR, "4" VARCHAR, "7" VARCHAR, "10" VARCHAR,
"13" VARCHAR, "16" VARCHAR, "19" VARCHAR, "22" VARCHAR);
CREATE TABLE IF NOT EXISTS data (id INTEGER PRIMARY KEY AUTOINCREMENT, date DATE NOT NULL, part INTEGER (2) NOT NULL,
weath STRING);
INSERT INTO parts (part, nor, tal, oga, kae) VALUES (1, '11', '11', '11', '11'), (2, '11', '11', '11', '11');
INSERT INTO weather (id) VALUES (' '), ('tomorrow'), ('3-day'), ('4-day'), ('5-day'), ('6-day'), ('7-day'),
('8-day'), ('9-day'), ('10-day');
INSERT INTO data (id, date, part) VALUES (1, '2022-02-24', 1);

11
sql.py
View file

@ -1,11 +1,22 @@
import os
import sqlite3 import sqlite3
SCHEMA = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'schema.sql')
class SQLighter: class SQLighter:
def __init__(self, database): def __init__(self, database):
"""Подключаемся к БД и сохраняем курсор соединения""" """Подключаемся к БД и сохраняем курсор соединения"""
self.connection = sqlite3.connect(database) self.connection = sqlite3.connect(database)
self.cursor = self.connection.cursor() self.cursor = self.connection.cursor()
self._create_if_empty()
def _create_if_empty(self):
"""Новая база: создаём таблицы и служебные строки из schema.sql"""
exists = self.cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'users'").fetchone()
if not exists:
with open(SCHEMA, encoding='utf-8') as f:
self.connection.executescript(f.read())
def user_exists(self, id_user): def user_exists(self, id_user):
"""Проверяем, есть ли уже пользователь в базе""" """Проверяем, есть ли уже пользователь в базе"""

BIN
sub.db

Binary file not shown.