Добален планировщик заданий и README.md

main
Лев 2025-09-03 13:01:51 +05:00
parent 3a93afc96d
commit cdc2fb9336
4 changed files with 126 additions and 23 deletions

78
README.md 100644
View File

@ -0,0 +1,78 @@
# About
- Получаем и парсим данные с https://formats.saby.ru/
- Выполняем валидацию данных для БД
- Отправляем данные в БД oracle
- Планировщик настроенный на определеное время
# Usage
**Боевой** запуск через **main.py**
**Тестовый** запуск чере **test.py**. В test.py не выполняется подключение к БД,
выполяется лишь подготовка данных. Благодоря этому **не имеет значение где запщуен код**.
Можно полностью отследить процесс парсинга и валидации.
# How it's works
Запуск кода осущетсвляется через main.py или test.py
Важно: в test.py не выполняется 4 и 5 шаг.
## 1. Получение данных с сайта.
C перва запускается скрипт parse_saby.py
```
result_dict_data = parse_saby.process_reports_parse()
```
Основным модулем для забора нужных данных является bs4 с классом BeautifulSoup.
BeautifulSoup представляет объект html страницы, это позволяет обращаеться по тэгам,
что бы достать нужные данные. Модуль возвращает cписок словарей. Пример:
```
result_dict_data = [{128513: ('fns', 'Уведомление о налогах для ЕНП', '01.07.22', None, '5.03')},
{132526: ('sfr', 'АДВ-1 Анкета застрахованного лица', '09.01.23', '31.12.34', '2.0')},
{...}]
```
## 2. parse_data_in_list()
```
list_data = parse_data_in_list(result_dict_data)
```
Легкий парсиннг, для преобразование, нужное в дальнейшем.
## 3. validate_data() and validation.py
```
list_data_validated = validate_data(list_data)
```
Тут из листа по листу передаем данные в класс DataValid из модуля validation.py.
DataValid наследуется от класса BaseModul модуля pydantic.
Сначало парсится дата, вторая дата в списке может быть None.
Потом проверяется соответсвие типов, и может выполняется явное преобразование.
Забираем валидные данные в новый лист.
Класс в рамках цикла пересоздается, для валидации следующего листа.
## 4. Отправка данных в БД
```
working_database.SimpleDB().data_transfer_in_database(list_data_validated)
```
### 4.1. Инциализация модуля working_database.py, подключение к hvac
_Сервер hvac настрое на работу в тихом режиме, реализуется с помощью: init_oracle_client()_
Тут выполняется сначало подключение к hvac серверу, получение секретов,
необохдимых для подключения к БД ```_create_db_pool_from_vault()```. Что бы подключиться к серверу hvac
используется секретный токен. Он забирается из переменной окружения ОС,
передается при создание контейнера(определенно в docker-compouse.yaml).
### 4.2. Подключение и отправка данных в БД
Метод класса ```data_transfer_in_database()``` получает данные для отправки в БД.
Данные имеют структуру лист словарей. Выполняется подключение к БД используя пул секретов из шага 4.1.
После чего передаются данные в процедуру P_RK_GOVERNMENT_REPORTS_INSERS.
## 5. Планировщик заданий.
```
scheduler.launch_the_scheduler()
```
Планировщик работает в фоновом режиме, пока не наступит заданое время.
Когда наступает заданое время запукает main.py. Время запуска по умолчанию 6 часов 0 минут.
Время можно изменить например на 9:30 следующим образом:
```
scheduler.launch_the_scheduler(h=9, m=30)
```

View File

@ -1,6 +1,7 @@
import working_database import working_database
import parse_saby import parse_saby
from validation import DataValid from validation import DataValid
import scheduler
def parse_data_in_list(dict_data: dict) -> list: def parse_data_in_list(dict_data: dict) -> list:
""" """
@ -35,3 +36,4 @@ result_dict_data = parse_saby.process_reports_parse()
list_data = parse_data_in_list(result_dict_data) list_data = parse_data_in_list(result_dict_data)
list_data_validated = validate_data(list_data) list_data_validated = validate_data(list_data)
working_database.SimpleDB().data_transfer_in_database(list_data_validated) working_database.SimpleDB().data_transfer_in_database(list_data_validated)
scheduler.launch_the_scheduler()

View File

@ -1,7 +1,12 @@
# pyright: reportOptionalMemberAccess=false, reportAttributeAccessIssue=false, reportOperatorIssue=false
# pyright: reportArgumentType=false, reportIndexIssue=false, reportCallIssue=false, reportGeneralTypeIssues=false
# Подсветка синтаксиса отключена, т.к. тип данных везде кооректены и обрабатывается if.
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import re import re
def parse_html(url: str): def parse_html(url: str):
""" """
Фунция принимает строку URL, выполняет запрос. Фунция принимает строку URL, выполняет запрос.
@ -9,13 +14,13 @@ def parse_html(url: str):
""" """
# Запрос страницы # Запрос страницы
response = requests.get(url) response = requests.get(url)
# Проверка статуса в ответе на запрос # Проверка статуса
if not(200 <= response.status_code <= 299): if not(200 <= response.status_code <= 299):
# Повторный запрос # Повторный запрос
response = requests.get(url) response = requests.get(url)
if not(200 <= response.status_code <= 299): if not(200 <= response.status_code <= 299):
print("Ошибка при запросе: ", response.status_code) print("Ошибка при запросе: ", response.status_code)
return response.status_code return response.status_code
# Создание обьекта BeautifulSoup(HTML страница) # Создание обьекта BeautifulSoup(HTML страница)
soup = BeautifulSoup(response.text, 'html.parser') soup = BeautifulSoup(response.text, 'html.parser')
return soup return soup
@ -32,8 +37,7 @@ def parse_date_report(url: str):
# HTML report # HTML report
soup = parse_html(url) soup = parse_html(url)
# Проверка на ошибку: # Проверка на ошибку:
#TODO проверка на int, а ошибка про None if soup == int: return None # Вызовит ошибку, что бы пропустить данный url
if soup == int: raise ValueError("Объект soup не должен быть None")
# Поиск в HTML строки ввида: 'Действующий формат (с 10.01.23) 5.01' # Поиск в HTML строки ввида: 'Действующий формат (с 10.01.23) 5.01'
div_element = soup.find('div', class_='controls-Dropdown__text') div_element = soup.find('div', class_='controls-Dropdown__text')
# Извлекаем текст из элемента # Извлекаем текст из элемента
@ -48,9 +52,9 @@ def parse_date_report(url: str):
r'(?:\D+(\d{1,2}\D\d{2}\D\d{2}))?.*?\)' \ r'(?:\D+(\d{1,2}\D\d{2}\D\d{2}))?.*?\)' \
r'\s*#?\s*(\d+(?:\D\d+)?)' r'\s*#?\s*(\d+(?:\D\d+)?)'
match = re.search(regex, text) match = re.search(regex, text)
from_date = match.group(1) # Первая дата (обязательная) from_date = match.group(1) # Первая дата (обязательная)
to_date = match.group(2) # Вторая дата (может быть None) to_date = match.group(2) # Вторая дата (может быть None)
version = match.group(3) # Число (обязательное) version = match.group(3) # Число (обязательное)
return from_date, to_date, version return from_date, to_date, version
def parse_reports(soup:BeautifulSoup, # HTML объект def parse_reports(soup:BeautifulSoup, # HTML объект
@ -75,21 +79,20 @@ def parse_reports(soup:BeautifulSoup, # HTML объект
link = soup.find('a', href=href) link = soup.find('a', href=href)
# Name report # Name report
span = link.find('span', class_="ProxySbisRu__registry-BrowserItem_typeName") span = link.find('span', class_="ProxySbisRu__registry-BrowserItem_typeName")
# Данные получены из url после парсинга try:
from_date, to_date, version = parse_date_report(url_report) # Данные получены из url после парсинга
from_date, to_date, version = parse_date_report(url_report)
except Exception: continue # Может быть ошибка если url не доступен
# Добавление всех данных в итоговый словарь # Добавление всех данных в итоговый словарь
result_dict_data.update({id: (name_title, span.text, from_date, to_date, version)}) result_dict_data.update({id: (name_title, span.text, from_date, to_date, version)})
except Exception as e: except Exception as e:
print(f"Ошибка при обработке отчета {report_title}: {str(e)}") print(f"Ошибка при обработке отчета {report_title}: ", e)
continue continue
return result_dict_data return result_dict_data
def write_report_data(dict_name:dict, name_title:str): def print_report_data(dict_name:dict, name_title:str):
""" """
Сохраняем запись, каждая запись с новой строки: Вывод на стандартный поток вывода итоговых данных
'ключ: значение'
'ключ: значение'
...
""" """
#Блок для красивого офорлмения файла #Блок для красивого офорлмения файла
def center_text(): def center_text():
@ -123,19 +126,17 @@ def search_title(url_format_report = 'https://formats.saby.ru/report'):
""" """
Функция ищет все тайтлы на странице formats.saby.ru/report. Функция ищет все тайтлы на странице formats.saby.ru/report.
Парамметры функции: Парамметры функции:
url по которому в котором будет происходить поиск url в котором будет происходить поиск
Возвращает: Возвращает:
Список URL-путей, например: ['/report/fns', '/report/example', ...] Список URL-путей, например: ['/report/fns', '/report/example', ...]
Исключения: Исключения:
ValueError: Если запрос к странице завершился с ошибкой (неверный статус). ValueError: Если запрос к странице завершился с ошибкой (неверный статус).
""" """
# Получаем HTML-cтраницу # Получаем HTML-cтраницу
html = parse_html(url_format_report) html = parse_html(url_format_report)
# Проверяем, что html не является кодом ошибки (int) # Проверяем, что html не является кодом ошибки (int)
if isinstance(html, int): if isinstance(html, int):
error_message = f'Ошибка при запросе {url_format_report}: {html}' raise Exception(f'Ошибка при запросе {url_format_report}: {html}')
raise Exception(error_message)
# Множество в который будут заноситься тайтлы # Множество в который будут заноситься тайтлы
report_urls = set() report_urls = set()
@ -158,7 +159,6 @@ def process_reports_parse(url_formats = 'https://formats.saby.ru'):
Параметр функции: Параметр функции:
url_formats - используется для создание полных URL url_formats - используется для создание полных URL
""" """
# Лист имеет вид: ['/report/fns', '/report/sfr'...] # Лист имеет вид: ['/report/fns', '/report/sfr'...]
set_title = search_title() set_title = search_title()
dict_result = {} dict_result = {}
@ -179,13 +179,14 @@ def process_reports_parse(url_formats = 'https://formats.saby.ru'):
dict_result_title = parse_reports(soup, report_title, url_formats, name_title) dict_result_title = parse_reports(soup, report_title, url_formats, name_title)
dict_result.update(dict_result_title) dict_result.update(dict_result_title)
# Вывод на стандратный поток вывода # Вывод на стандратный поток вывода
write_report_data(dict_result, name_title) print_report_data(dict_result, name_title)
except Exception as e: except Exception as e:
print(f"Ошибка при обработке отчета {report_title}: {str(e)}") print(f"Ошибка при обработке отчета {report_title}: {str(e)}")
continue continue
return dict_result return dict_result
if __name__ == '__main__': if __name__ == '__main__':
# Можно запустить отдельно от всего проекта
process_reports_parse() process_reports_parse()

22
app/scheduler.py 100644
View File

@ -0,0 +1,22 @@
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
import subprocess
def run_parser(filename = 'main.py'):
print("Запуск main.py...")
subprocess.run(['python', filename])
def launch_the_scheduler():
scheduler = BlockingScheduler() # Создание планировщика
# Каждый день в 6:00 утра запуск run_parser()
scheduler.add_job(run_parser, trigger=CronTrigger(hour=6, minute=00))
print("Планировщик запущен. Нажмите Ctrl+C для остановки.")
try:
scheduler.start()
except KeyboardInterrupt:
print("Планировщик остановлен")
except Exception as e:
print("Не непредвиденная ошибка: ", e)