197 lines
9.5 KiB
Python
197 lines
9.5 KiB
Python
# pyright: reportOptionalMemberAccess=false, reportAttributeAccessIssue=false, reportOperatorIssue=false
|
||
# pyright: reportArgumentType=false, reportIndexIssue=false, reportCallIssue=false, reportGeneralTypeIssues=false
|
||
# Подсветка синтаксиса отключена, т.к. тип данных везде кооректены и обрабатывается if.
|
||
|
||
import requests
|
||
from bs4 import BeautifulSoup
|
||
import re
|
||
import error_to_log
|
||
|
||
def parse_html(url: str):
|
||
"""
|
||
Фунция принимает строку URL, выполняет запрос.
|
||
Создает и возращает объект BeautifulSoup(HTML).
|
||
"""
|
||
# Запрос страницы
|
||
response = requests.get(url)
|
||
# Проверка статуса
|
||
if not(200 <= response.status_code <= 299):
|
||
# Повторный запрос
|
||
response = requests.get(url)
|
||
if not(200 <= response.status_code <= 299):
|
||
print("Ошибка при запросе: ", response.status_code)
|
||
return response.status_code
|
||
# Создание обьекта BeautifulSoup(HTML страница)
|
||
soup = BeautifulSoup(response.text, 'html.parser')
|
||
return soup
|
||
|
||
def parse_date_report(url: str):
|
||
"""
|
||
Функия ожидает list из URL ввида:
|
||
url = https://formats.saby.ru/report/fns/128513
|
||
Заходит по URL, запоминает дату.
|
||
Возвращает две строки ввида:
|
||
'date = 01.07.22'
|
||
'id_date = 5.01'
|
||
"""
|
||
# HTML report
|
||
soup = parse_html(url)
|
||
# Проверка на ошибку:
|
||
if soup == int: return None # Вызовит ошибку, что бы пропустить данный url
|
||
# Поиск в HTML строки ввида: 'Действующий формат (с 10.01.23) 5.01'
|
||
div_element = soup.find('div', class_='controls-Dropdown__text')
|
||
# Извлекаем текст из элемента
|
||
text = div_element.get_text()
|
||
# Парсим нужные данные.
|
||
if '_' in text: # Уcловия ловит: text = 'Действующий формат 02-04-2025 _2025_001
|
||
regex = r'\w+\s\w+\s(\S+) (\S+)'
|
||
date, id_date = re.search(regex, text).groups()
|
||
return date, None, id_date
|
||
else: # Все остальные
|
||
regex = r'(\d{2}\D\d{2}\D\d{2})' \
|
||
r'(?:\D+(\d{1,2}\D\d{2}\D\d{2}))?.*?\)' \
|
||
r'\s*#?\s*(\d+(?:\D\d+)?)'
|
||
match = re.search(regex, text)
|
||
from_date = match.group(1) # Первая дата (обязательная)
|
||
to_date = match.group(2) # Вторая дата (может быть None)
|
||
version = match.group(3) # Число (обязательное)
|
||
return from_date, to_date, version
|
||
|
||
def parse_reports(soup:BeautifulSoup, # HTML объект
|
||
report_title:str, # Строка ввида: 'report/fns'
|
||
url_formats:str, # Строка ввида: 'https://formats.saby.ru'
|
||
name_title:str): # Имя тайтла: 'fns'
|
||
"""
|
||
Достаются все необходимые данные, возвращаются в ввиде словаря:
|
||
{106538: ('fns', 'НД по косвенным налогам', '01.08.23', '5.04')}
|
||
"""
|
||
result_dict_data = {}
|
||
# Перебарает все URL, ищутся по тегу 'a'
|
||
for link in soup.find_all('a'):
|
||
try:
|
||
# Ищет по тегу: href
|
||
href = link.get('href')
|
||
if f'{report_title}/' in href:
|
||
# id report
|
||
id = href.rstrip('/').split('/')[-1]
|
||
# URL report
|
||
url_report = f'{url_formats}{href}'
|
||
link = soup.find('a', href=href)
|
||
# Name report
|
||
span = link.find('span', class_="ProxySbisRu__registry-BrowserItem_typeName")
|
||
try:
|
||
# Данные получены из 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)})
|
||
except Exception as e:
|
||
error_message = f"Ошибка при обработке отчета {report_title}: {e}"
|
||
print(error_message)
|
||
error_to_log.write_to_log(error_message)
|
||
continue
|
||
return result_dict_data
|
||
|
||
def print_report_data(dict_name:dict, name_title:str):
|
||
"""
|
||
Вывод на стандартный поток вывода итоговых данных
|
||
"""
|
||
#Блок для красивого офорлмения файла
|
||
def center_text():
|
||
"""
|
||
Функия парсит сколько нужно подчеркивания,
|
||
что бы центролизовать текст в строке с этими подчеркиваниями
|
||
Возвращает две строки одна для начало блока записей тайтал, вторая конца блока.
|
||
"""
|
||
start_str = f'START_{name_title}'
|
||
end_str = f'END_{name_title}'
|
||
dash = 100
|
||
list_result = []
|
||
# сначал вернеться dash_start, потом end_start.
|
||
for text in start_str, end_str:
|
||
remaining_space = dash - len(text)
|
||
left = remaining_space // 2
|
||
right = remaining_space - left # Чтобы учесть нечётную разницу
|
||
list_result.append('_' * left + text + '_' * right)
|
||
return list_result[:2]
|
||
dash_start, dash_end = center_text()
|
||
#Конец блока
|
||
|
||
#Вывод с красивым офрмление в виде нижнего подчеркивания
|
||
print(f'\n{dash_start}\n')
|
||
for key, value in dict_name.items():
|
||
str_k_v = f'{key}: {value}\n'
|
||
print(str_k_v, end='')
|
||
print(f'{dash_end}\n')
|
||
|
||
def search_title(url_format_report = 'https://formats.saby.ru/report'):
|
||
"""
|
||
Функция ищет все тайтлы на странице formats.saby.ru/report.
|
||
Парамметры функции:
|
||
url в котором будет происходить поиск
|
||
Возвращает:
|
||
Список URL-путей, например: ['/report/fns', '/report/example', ...]
|
||
Исключения:
|
||
ValueError: Если запрос к странице завершился с ошибкой (неверный статус).
|
||
"""
|
||
# Получаем HTML-cтраницу
|
||
html = parse_html(url_format_report)
|
||
# Проверяем, что html не является кодом ошибки (int)
|
||
if isinstance(html, int):
|
||
raise Exception(f'Ошибка при запросе {url_format_report}: {html}')
|
||
|
||
# Множество в который будут заноситься тайтлы
|
||
report_urls = set()
|
||
|
||
# Ищем все ссылки <a> с href, соответствующие шаблону /report/{title}
|
||
for link in html.find_all('a', href=True):
|
||
# Ищем по тегу: href
|
||
href = link['href']
|
||
# Проверям что href содержит: /report/{title}
|
||
if re.search(r'\/report\/(\w+)$', href):
|
||
report_urls.add(href)
|
||
return report_urls
|
||
|
||
def process_reports_parse(url_formats = 'https://formats.saby.ru'):
|
||
"""
|
||
Функция пробегается по каждому тайтлу.
|
||
Для всех записей(reports) выполняется запрос HTML страницы,
|
||
которая парситься в объект BeautifulSoup(HTML страница).
|
||
Из это обьекта достаются не обходимые данные
|
||
Параметр функции:
|
||
url_formats - используется для создание полных URL
|
||
"""
|
||
# Лист имеет вид: ['/report/fns', '/report/sfr'...]
|
||
set_title = search_title()
|
||
dict_result = {}
|
||
for report_title in set_title:
|
||
try:
|
||
# Получаем имя тайтла через парсинг
|
||
name_title = report_title.rstrip('/').split('/')[-1]
|
||
|
||
# URL на конкретный title
|
||
url_title = f'{url_formats}{report_title}'
|
||
|
||
# Объект HTML, одного(конкретного) title
|
||
soup = parse_html(url_title)
|
||
if isinstance(soup, int):
|
||
print(f'Не удалось установить соедение c {url_title}')
|
||
continue
|
||
# Словарь с нужными данными
|
||
dict_result_title = parse_reports(soup, report_title, url_formats, name_title)
|
||
dict_result.update(dict_result_title)
|
||
# Вывод на стандратный поток вывода
|
||
print_report_data(dict_result_title, name_title)
|
||
except Exception as e:
|
||
print(f"Ошибка при обработке отчета {report_title}: {str(e)}")
|
||
error_message = f"ERROR-PARSE_SABY: {e} DATA: {report_title}"
|
||
error_to_log.write_to_log(error_message)
|
||
continue
|
||
return dict_result
|
||
|
||
if __name__ == '__main__':
|
||
# Можно запустить отдельно от всего проекта
|
||
process_reports_parse()
|
||
|
||
|