This commit is contained in:
Andrey Sazonov 2025-10-02 21:11:22 +03:00
parent ee58d3a7a4
commit 9937d4e250
9 changed files with 53 additions and 68 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
BOT_TOKEN=xxxxxxxxxxxxxxxxxxxxx
WEBHOOK_URL=https://ваш-домен.ru

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.env
__pycache__/
*.pyc
logs/

View File

@ -8,14 +8,11 @@ ENV PYTHONFAULTHANDLER=1 \
PIP_DISABLE_PIP_VERSION_CHECK=on \ PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 PIP_DEFAULT_TIMEOUT=100
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN pip install --upgrade pip && \ RUN pip install --upgrade pip && \
pip install --no-cache-dir --disable-pip-version-check -r requirements.txt pip install --no-cache-dir --disable-pip-version-check -r requirements.txt

View File

@ -32,10 +32,10 @@ Telegram-бот, который автоматически парсит и от
### 1. Клонируйте репозиторий ### 1. Клонируйте репозиторий
### 2. Настройте конфигурацию ### 2. Настройте конфигурацию
Отредактируйте файл `config.py`: Скопируйте `.env.example` в `.env` и отредактируйте.
```python ```python
token = 'ВАШ_TELEGRAM_BOT_TOKEN' BOT_TOKEN=xxxxxxxxxxxxxxxxxxxxx
URL_APP = 'https://ваш-домен.ru' WEBHOOK_URL=https://ваш-домен.ru
``` ```
### 3. Соберите и запустите через Docker ### 3. Соберите и запустите через Docker
@ -48,25 +48,12 @@ docker run -d \
mrsk_bot mrsk_bot
``` ```
- Или через docker compose
> Убедитесь, что ваш сервер доступен по `URL_APP` и настроен reverse proxy (например, Nginx) на порт 5000. > Убедитесь, что ваш сервер доступен по `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) Если у вас есть вопросы, предложения или вы нашли баг — пишите: [@pikusQQ](https://t.me/pikusQQ)

28
bot.py
View File

@ -1,13 +1,18 @@
import asyncio import asyncio
import logging import logging
import os
from pathlib import Path
import pytz import pytz
from datetime import datetime from datetime import datetime
from pathlib import Path
from aiogram import Bot, Dispatcher, executor, types from aiogram import Bot, Dispatcher, executor, types
from aiogram.dispatcher.filters import Text from aiogram.dispatcher.filters import Text
import config from dotenv import load_dotenv
from main import start_parser from main import start_parser
load_dotenv()
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
filename="logs/bot.log", filename="logs/bot.log",
@ -16,11 +21,19 @@ logging.basicConfig(
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
BOT_TOKEN = os.getenv("BOT_TOKEN")
WEBHOOK_URL = os.getenv("WEBHOOK_URL")
if not BOT_TOKEN:
raise ValueError("❌ Переменная BOT_TOKEN не задана в .env")
if not WEBHOOK_URL:
raise ValueError("❌ Переменная WEBHOOK_URL не задана в .env")
TZ = pytz.timezone('Europe/Moscow') TZ = pytz.timezone('Europe/Moscow')
USER_LOG = Path("logs/users.log") USER_LOG = Path("logs/users.log")
UPDATE_INTERVAL = 300 # 5 минут в секундах UPDATE_INTERVAL = 300 # 5 минут
bot = Bot(token=config.token) bot = Bot(token=BOT_TOKEN)
dp = Dispatcher(bot) dp = Dispatcher(bot)
async def periodic_update(): async def periodic_update():
@ -40,7 +53,6 @@ async def cmd_start(message: types.Message):
keyboard.add(*buttons) keyboard.add(*buttons)
await message.answer('Выберите тип отключений:', reply_markup=keyboard) await message.answer('Выберите тип отключений:', reply_markup=keyboard)
# Утилита для отправки файла
async def send_file_content(message: types.Message, file_path: Path, empty_msg: str, log_msg: str): async def send_file_content(message: types.Message, file_path: Path, empty_msg: str, log_msg: str):
await message.answer("Обновляю...") await message.answer("Обновляю...")
@ -101,12 +113,10 @@ async def info(message: types.Message):
async def fallback(message: types.Message): async def fallback(message: types.Message):
await message.answer("Нажмите /start для начала работы.") await message.answer("Нажмите /start для начала работы.")
# Webhook handlers
async def on_startup(dp): async def on_startup(dp):
await bot.set_webhook(config.URL_APP) await bot.set_webhook(WEBHOOK_URL)
# Запуск фоновой задачи после старта
asyncio.create_task(periodic_update()) asyncio.create_task(periodic_update())
logger.info("Webhook установлен. Фоновое обновление запущено.") logger.info(f"Webhook установлен на {WEBHOOK_URL}. Фоновое обновление запущено.")
async def on_shutdown(dp): async def on_shutdown(dp):
await bot.delete_webhook() await bot.delete_webhook()

View File

@ -1,8 +0,0 @@
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&regionCode=31&districtCode={districtCode}&locality=Белгород&localityCode={localityCode}'
url_avar = f'{main_url}4&regionCode=31&districtCode={districtCode}&locality=Белгород&localityCode={localityCode}&Street=-'
url_plan = f'{main_url}2&regionCode=31&districtCode={districtCode}&locality=Белгород&localityCode={localityCode}'
token = ''
URL_APP = ''

View File

@ -1,17 +1,14 @@
version: '3.8'
services: services:
mrsk-bot: power-outage-bot:
build: . build: .
container_name: mrsk-bot container_name: power-outage-bot
restart: unless-stopped restart: unless-stopped
ports: ports:
- "5002:5000" - "5000:5000"
volumes: volumes:
- ./logs:/app/logs - ./logs:/app/logs
- ./.env:/app/.env:ro
environment: environment:
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
networks:
- bot-network
networks:
bot-network:
driver: bridge

33
main.py
View File

@ -1,3 +1,5 @@
# main.py
import json import json
import logging import logging
import requests import requests
@ -5,8 +7,6 @@ from pathlib import Path
from urllib3.exceptions import InsecureRequestWarning from urllib3.exceptions import InsecureRequestWarning
import urllib3 import urllib3
import config
urllib3.disable_warnings(InsecureRequestWarning) urllib3.disable_warnings(InsecureRequestWarning)
logging.basicConfig( logging.basicConfig(
@ -15,30 +15,28 @@ logging.basicConfig(
format="%(asctime)s - %(module)s - %(levelname)s - %(funcName)s: %(lineno)d - %(message)s", format="%(asctime)s - %(module)s - %(levelname)s - %(funcName)s: %(lineno)d - %(message)s",
datefmt='%H:%M:%S', datefmt='%H:%M:%S',
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAIN_URL = 'https://www.mrsk-1.ru/customers/customer-service/power-outage/ajax.php?request='
DISTRICT_CODE = '0ECC8970-2702-4C14-9E4D-956606EFFEE3'
LOCALITY_CODE = '02E9C019-AB4D-4FA0-928E-D6C0A41DC256'
URL_PLAN = f'{MAIN_URL}2&regionCode=31&districtCode={DISTRICT_CODE}&locality=Белгород&localityCode={LOCALITY_CODE}'
URL_VNEREGLAMENT = f'{MAIN_URL}3&regionCode=31&districtCode={DISTRICT_CODE}&locality=Белгород&localityCode={LOCALITY_CODE}'
URL_AVAR = f'{MAIN_URL}4&regionCode=31&districtCode={DISTRICT_CODE}&locality=Белгород&localityCode={LOCALITY_CODE}&Street=-'
PLAN_FILE = Path("plan.txt") PLAN_FILE = Path("plan.txt")
VN_FILE = Path("vnereglament.txt") VN_FILE = Path("vnereglament.txt")
AVAR_FILE = Path("avar.txt") AVAR_FILE = Path("avar.txt")
def clean_street(street: str) -> str: def clean_street(street: str) -> str:
"""Очищает и форматирует строку с адресами."""
street = street.replace('Белгород г; ', '').replace('\n\n', '\n').strip() street = street.replace('Белгород г; ', '').replace('\n\n', '\n').strip()
lines = [line.strip() for line in street.split('\n') if line.strip()] lines = [line.strip() for line in street.split('\n') if line.strip()]
return '\n'.join(lines) return '\n'.join(lines)
def format_time(dt_str: str) -> str: def format_time(dt_str: str) -> str:
"""Преобразует '2025-10-02T09:00:00''02.10.2025 09:00'.""" return dt_str.replace('T', ' ')
try:
return dt_str.replace('T', ' ')
except:
return dt_str
def parse_and_save(data, file_path: Path, mode: str): def parse_and_save(data, file_path: Path, mode: str):
"""
mode: 'plan', 'vnereglament', 'avar'
"""
if not data or data == 0: if not data or data == 0:
file_path.write_text("[Нет отключений]", encoding='utf-8') file_path.write_text("[Нет отключений]", encoding='utf-8')
return return
@ -61,7 +59,7 @@ def parse_and_save(data, file_path: Path, mode: str):
def get_plan(): def get_plan():
try: try:
r = requests.get(url=config.url_plan, verify=False, timeout=10) r = requests.get(url=URL_PLAN, verify=False, timeout=10)
r.raise_for_status() r.raise_for_status()
data = r.json() data = r.json()
parse_and_save(data, PLAN_FILE, 'plan') parse_and_save(data, PLAN_FILE, 'plan')
@ -72,7 +70,7 @@ def get_plan():
def get_vnereglament(): def get_vnereglament():
try: try:
r = requests.get(url=config.url_vnereglament, verify=False, timeout=10) r = requests.get(url=URL_VNEREGLAMENT, verify=False, timeout=10)
r.raise_for_status() r.raise_for_status()
data = r.json() data = r.json()
parse_and_save(data, VN_FILE, 'vnereglament') parse_and_save(data, VN_FILE, 'vnereglament')
@ -83,7 +81,7 @@ def get_vnereglament():
def get_avar(): def get_avar():
try: try:
r = requests.get(url=config.url_avar, verify=False, timeout=10) r = requests.get(url=URL_AVAR, verify=False, timeout=10)
r.raise_for_status() r.raise_for_status()
data = r.json() data = r.json()
parse_and_save(data, AVAR_FILE, 'avar') parse_and_save(data, AVAR_FILE, 'avar')
@ -96,7 +94,4 @@ def start_parser():
get_plan() get_plan()
get_vnereglament() get_vnereglament()
get_avar() get_avar()
logger.info("Парсер отработал") logger.info("Парсер отработал")
if __name__ == '__main__':
start_parser()

View File

@ -14,3 +14,4 @@ pytz==2024.1
requests==2.32.3 requests==2.32.3
urllib3==2.2.1 urllib3==2.2.1
yarl==1.9.4 yarl==1.9.4
python-dotenv==1.0.1