INIT
This commit is contained in:
commit
b6d046c5e2
26
Dockerfile
Normal file
26
Dockerfile
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
FROM python:3.11-slim
|
||||
|
||||
ENV PYTHONFAULTHANDLER=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONHASHSEED=random \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PIP_NO_CACHE_DIR=off \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||
PIP_DEFAULT_TIMEOUT=100
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install --no-cache-dir --disable-pip-version-check -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p logs
|
||||
|
||||
CMD ["python", "bot.py"]
|
||||
78
README.md
Normal file
78
README.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
|
||||
---
|
||||
|
||||
# 🚨 Бот отключений электроэнергии (МРСК Белгород)
|
||||
|
||||
Telegram-бот, который автоматически парсит и отображает актуальную информацию об **плановых**, **внерегламентных** и **аварийных** отключениях электроэнергии в г. Белгород.
|
||||
|
||||
Данные берутся с официального сайта [МРСК Центра и Приволжья](https://www.mrsk-1.ru/).
|
||||
|
||||
---
|
||||
|
||||
## Возможности
|
||||
|
||||
- Просмотр трёх типов отключений
|
||||
- Автоматическое обновление данных каждые **5 минут**
|
||||
- Удобный интерфейс через Telegram
|
||||
- Подробное логирование запросов и ошибок
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Технологии
|
||||
|
||||
- Python 3.11
|
||||
- [aiogram](https://docs.aiogram.dev/) (v2)
|
||||
- requests, pytz
|
||||
- Docker (для развёртывания)
|
||||
|
||||
---
|
||||
|
||||
## Запуск
|
||||
|
||||
### 1. Клонируйте репозиторий
|
||||
|
||||
### 2. Настройте конфигурацию
|
||||
Отредактируйте файл `config.py`:
|
||||
```python
|
||||
token = 'ВАШ_TELEGRAM_BOT_TOKEN'
|
||||
URL_APP = 'https://ваш-домен.ru'
|
||||
```
|
||||
|
||||
### 3. Соберите и запустите через Docker
|
||||
```bash
|
||||
docker build -t power-outage-bot .
|
||||
docker run -d \
|
||||
--name power-bot \
|
||||
-p 5000:5000 \
|
||||
-v $(pwd)/logs:/app/logs \
|
||||
power-outage-bot
|
||||
```
|
||||
|
||||
> Убедитесь, что ваш сервер доступен по `URL_APP` и настроен reverse proxy (например, Nginx) на порт 5000.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
```
|
||||
.
|
||||
├── bot.py # Основной файл бота
|
||||
├── main.py # Парсер данных с сайта МРСК
|
||||
├── config.py # Конфигурация (токен, URL, API-эндпоинты)
|
||||
├── requirements.txt # Зависимости Python
|
||||
├── Dockerfile # Для сборки образа
|
||||
├── logs/ # Директория для логов (бот.log, parser.log, users.log)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Обратная связь
|
||||
|
||||
Если у вас есть вопросы, предложения или вы нашли баг — пишите: [@pikusQQ](https://t.me/pikusQQ)
|
||||
|
||||
---
|
||||
|
||||
> ⚠️ **Важно**: Бот использует **webhook**, поэтому должен быть развёрнут на сервере с HTTPS.
|
||||
|
||||
---
|
||||
124
bot.py
Normal file
124
bot.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import pytz
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from aiogram import Bot, Dispatcher, executor, types
|
||||
from aiogram.dispatcher.filters import Text
|
||||
import config
|
||||
from main import start_parser
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
filename="logs/bot.log",
|
||||
format="%(asctime)s - %(module)s - %(levelname)s - %(funcName)s: %(lineno)d - %(message)s",
|
||||
datefmt='%H:%M:%S',
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TZ = pytz.timezone('Europe/Moscow')
|
||||
USER_LOG = Path("logs/users.log")
|
||||
UPDATE_INTERVAL = 300 # 5 минут в секундах
|
||||
|
||||
bot = Bot(token=config.token)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
async def periodic_update():
|
||||
while True:
|
||||
logger.info("Запуск автоматического обновления данных...")
|
||||
try:
|
||||
start_parser()
|
||||
logger.info("Автоматическое обновление завершено")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при автоматическом обновлении: {e}")
|
||||
await asyncio.sleep(UPDATE_INTERVAL)
|
||||
|
||||
@dp.message_handler(commands='start')
|
||||
async def cmd_start(message: types.Message):
|
||||
buttons = ['Плановые', 'Внерегламентные', 'Аварийные', 'Информация']
|
||||
keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
|
||||
keyboard.add(*buttons)
|
||||
await message.answer('Выберите тип отключений:', reply_markup=keyboard)
|
||||
|
||||
# Утилита для отправки файла
|
||||
async def send_file_content(message: types.Message, file_path: Path, empty_msg: str, log_msg: str):
|
||||
await message.answer("Обновляю...")
|
||||
|
||||
if not file_path.exists() or file_path.stat().st_size == 0:
|
||||
await message.answer(empty_msg)
|
||||
else:
|
||||
text = file_path.read_text(encoding='utf-8')
|
||||
if len(text) > 4095:
|
||||
for i in range(0, len(text), 4095):
|
||||
await message.answer(text[i:i + 4095])
|
||||
else:
|
||||
await message.answer(text)
|
||||
|
||||
now = datetime.now(TZ).strftime('%d/%m/%Y %H:%M')
|
||||
username = message.from_user.username or f"id{message.from_user.id}"
|
||||
log_entry = f"{now} t.me/{username} получил инфу по {log_msg}.\n"
|
||||
USER_LOG.parent.mkdir(exist_ok=True)
|
||||
with USER_LOG.open('a', encoding='utf-8') as f:
|
||||
f.write(log_entry)
|
||||
logger.info(f"Пользователь {username} запросил {log_msg}")
|
||||
|
||||
@dp.message_handler(Text(equals='Плановые'))
|
||||
async def plan(message: types.Message):
|
||||
await send_file_content(
|
||||
message,
|
||||
Path("plan.txt"),
|
||||
"[Нет плановых отключений]",
|
||||
"плановым отключениям"
|
||||
)
|
||||
|
||||
@dp.message_handler(Text(equals='Внерегламентные'))
|
||||
async def vnereglament(message: types.Message):
|
||||
await send_file_content(
|
||||
message,
|
||||
Path("vnereglament.txt"),
|
||||
"[Нет внерегламентных отключений]",
|
||||
"внерегламентным отключениям"
|
||||
)
|
||||
|
||||
@dp.message_handler(Text(equals='Аварийные'))
|
||||
async def avar(message: types.Message):
|
||||
await send_file_content(
|
||||
message,
|
||||
Path("avar.txt"),
|
||||
"[Нет аварийных отключений]",
|
||||
"аварийным отключениям"
|
||||
)
|
||||
|
||||
@dp.message_handler(Text(equals='Информация'))
|
||||
async def info(message: types.Message):
|
||||
await message.answer(
|
||||
"ℹ️ Информация МРСК Белгород по отключениям эл-ва (v2)\n\n"
|
||||
"✅ Данные обновляются автоматически каждые 5 минут.\n"
|
||||
"📩 Вопросы и предложения: @pikusQQ"
|
||||
)
|
||||
|
||||
@dp.message_handler()
|
||||
async def fallback(message: types.Message):
|
||||
await message.answer("Нажмите /start для начала работы.")
|
||||
|
||||
# Webhook handlers
|
||||
async def on_startup(dp):
|
||||
await bot.set_webhook(config.URL_APP)
|
||||
# Запуск фоновой задачи после старта
|
||||
asyncio.create_task(periodic_update())
|
||||
logger.info("Webhook установлен. Фоновое обновление запущено.")
|
||||
|
||||
async def on_shutdown(dp):
|
||||
await bot.delete_webhook()
|
||||
logger.info("Webhook удалён")
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_webhook(
|
||||
dispatcher=dp,
|
||||
webhook_path='',
|
||||
on_startup=on_startup,
|
||||
on_shutdown=on_shutdown,
|
||||
skip_updates=True,
|
||||
host='0.0.0.0',
|
||||
port=5000,
|
||||
)
|
||||
8
config.py
Normal file
8
config.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
main_url = 'https://www.mrsk-1.ru/customers/customer-service/power-outage/ajax.php?request='
|
||||
districtCode = '0ECC8970-2702-4C14-9E4D-956606EFFEE3'
|
||||
localityCode = '02E9C019-AB4D-4FA0-928E-D6C0A41DC256'
|
||||
url_vnereglament = f'{main_url}3®ionCode=31&districtCode={districtCode}&locality=Белгород&localityCode={localityCode}'
|
||||
url_avar = f'{main_url}4®ionCode=31&districtCode={districtCode}&locality=Белгород&localityCode={localityCode}&Street=-'
|
||||
url_plan = f'{main_url}2®ionCode=31&districtCode={districtCode}&locality=Белгород&localityCode={localityCode}'
|
||||
token = ''
|
||||
URL_APP = ''
|
||||
102
main.py
Normal file
102
main.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import json
|
||||
import logging
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
import urllib3
|
||||
|
||||
import config
|
||||
|
||||
urllib3.disable_warnings(InsecureRequestWarning)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
filename="logs/parser.log",
|
||||
format="%(asctime)s - %(module)s - %(levelname)s - %(funcName)s: %(lineno)d - %(message)s",
|
||||
datefmt='%H:%M:%S',
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PLAN_FILE = Path("plan.txt")
|
||||
VN_FILE = Path("vnereglament.txt")
|
||||
AVAR_FILE = Path("avar.txt")
|
||||
|
||||
def clean_street(street: str) -> str:
|
||||
"""Очищает и форматирует строку с адресами."""
|
||||
street = street.replace('Белгород г; ', '').replace('\n\n', '\n').strip()
|
||||
lines = [line.strip() for line in street.split('\n') if line.strip()]
|
||||
return '\n'.join(lines)
|
||||
|
||||
def format_time(dt_str: str) -> str:
|
||||
"""Преобразует '2025-10-02T09:00:00' → '02.10.2025 09:00'."""
|
||||
try:
|
||||
return dt_str.replace('T', ' ')
|
||||
except:
|
||||
return dt_str
|
||||
|
||||
def parse_and_save(data, file_path: Path, mode: str):
|
||||
"""
|
||||
mode: 'plan', 'vnereglament', 'avar'
|
||||
"""
|
||||
if not data or data == 0:
|
||||
file_path.write_text("[Нет отключений]", encoding='utf-8')
|
||||
return
|
||||
|
||||
records = data if isinstance(data, list) else [data]
|
||||
lines = []
|
||||
|
||||
for item in records:
|
||||
if mode in ('plan', 'vnereglament'):
|
||||
street = clean_street(item.get("DisconnectionObject", ""))
|
||||
time_down = format_time(item.get("DisconnectionDateTime", ""))
|
||||
time_up = format_time(item.get("EnergyOnPlanningDateTime", ""))
|
||||
lines.append(f"{street}\nОтключение: {time_down} — {time_up}\n{'─' * 30}")
|
||||
elif mode == 'avar':
|
||||
street = clean_street(item.get("StreetHome", ""))
|
||||
time_up = format_time(item.get("ScheduledTimeRemoval", ""))
|
||||
lines.append(f"{street}\nВосстановление: ~{time_up}\n{'─' * 30}")
|
||||
|
||||
file_path.write_text('\n\n'.join(lines), encoding='utf-8')
|
||||
|
||||
def get_plan():
|
||||
try:
|
||||
r = requests.get(url=config.url_plan, verify=False, timeout=10)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
parse_and_save(data, PLAN_FILE, 'plan')
|
||||
logger.info("[Плановые отключения] обновлены")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении плановых отключений: {e}")
|
||||
PLAN_FILE.write_text("[Ошибка загрузки данных]", encoding='utf-8')
|
||||
|
||||
def get_vnereglament():
|
||||
try:
|
||||
r = requests.get(url=config.url_vnereglament, verify=False, timeout=10)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
parse_and_save(data, VN_FILE, 'vnereglament')
|
||||
logger.info("[Внерегламентные отключения] обновлены")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении внерегламентных отключений: {e}")
|
||||
VN_FILE.write_text("[Ошибка загрузки данных]", encoding='utf-8')
|
||||
|
||||
def get_avar():
|
||||
try:
|
||||
r = requests.get(url=config.url_avar, verify=False, timeout=10)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
parse_and_save(data, AVAR_FILE, 'avar')
|
||||
logger.info("[Аварийные отключения] обновлены")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении аварийных отключений: {e}")
|
||||
AVAR_FILE.write_text("[Ошибка загрузки данных]", encoding='utf-8')
|
||||
|
||||
def start_parser():
|
||||
get_plan()
|
||||
get_vnereglament()
|
||||
get_avar()
|
||||
logger.info("Парсер отработал")
|
||||
|
||||
if __name__ == '__main__':
|
||||
start_parser()
|
||||
16
requirements.txt
Normal file
16
requirements.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
aiogram==2.25.2
|
||||
aiohttp==3.8.6
|
||||
aiosignal==1.3.1
|
||||
async-timeout==4.0.3
|
||||
attrs==23.2.0
|
||||
Babel==2.9.1
|
||||
certifi==2024.6.2
|
||||
charset-normalizer==3.3.2
|
||||
frozenlist==1.4.1
|
||||
idna==3.7
|
||||
magic-filter==1.0.12
|
||||
multidict==6.0.5
|
||||
pytz==2024.1
|
||||
requests==2.32.3
|
||||
urllib3==2.2.1
|
||||
yarl==1.9.4
|
||||
Loading…
Reference in New Issue
Block a user