153 lines
6.5 KiB
Python
Executable file
153 lines
6.5 KiB
Python
Executable file
#!/usr/bin/env python3
|
||
"""
|
||
Скрипт автономной сборки официальных полных текстов действующих нормативно-правовых актов РФ
|
||
с портала КонсультантПлюс в структурированные PDF через wkhtmltopdf.
|
||
|
||
Собираемые документы:
|
||
1. Постановление Правительства РФ № 354 (cons_doc_LAW_114247)
|
||
2. Постановление Госстроя РФ № 170 (cons_doc_LAW_44772)
|
||
3. СанПиН 2.1.3684-21 (cons_doc_LAW_376166)
|
||
4. Приказ Генеральной прокуратуры РФ № 45 (cons_doc_LAW_141740)
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import time
|
||
import subprocess
|
||
import urllib.request
|
||
from bs4 import BeautifulSoup
|
||
import fitz
|
||
|
||
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
|
||
|
||
DOCS = [
|
||
{
|
||
'law_id': 'cons_doc_LAW_114247',
|
||
'out_name': '04_ПП_РФ_354_Коммунальные_Услуги_Полный.pdf',
|
||
'title_prefix': 'Постановление Правительства РФ от 06.05.2011 № 354'
|
||
},
|
||
{
|
||
'law_id': 'cons_doc_LAW_44772',
|
||
'out_name': '06_Постановление_Госстроя_170_Эксплуатация_Жилфонда_Полный.pdf',
|
||
'title_prefix': 'Постановление Госстроя РФ от 27.09.2003 № 170 (МДК 2-03.2003)'
|
||
},
|
||
{
|
||
'law_id': 'cons_doc_LAW_376166',
|
||
'out_name': '07_СанПиН_2.1.3684-21_Санитарные_Требования_МКД_Полный.pdf',
|
||
'title_prefix': 'СанПиН 2.1.3684-21 (Постановление Главного санврача РФ № 3)'
|
||
},
|
||
{
|
||
'law_id': 'cons_doc_LAW_141740',
|
||
'out_name': '08_Приказ_Генпрокуратуры_45_Инструкция_Рассмотрения_Обращений_Полный.pdf',
|
||
'title_prefix': 'Приказ Генеральной прокуратуры РФ от 30.01.2013 № 45'
|
||
}
|
||
]
|
||
|
||
DEST_DIR = '/home/matrixhasyou/mutt/legal_sources'
|
||
os.makedirs(DEST_DIR, exist_ok=True)
|
||
|
||
def build_law(law_info):
|
||
law_id = law_info['law_id']
|
||
out_pdf = os.path.join(DEST_DIR, law_info['out_name'])
|
||
base_url = f'https://www.consultant.ru/document/{law_id}/'
|
||
|
||
print(f"\n=======================================================")
|
||
print(f"[*] Сборка документа: {law_info['title_prefix']} ({law_id})")
|
||
print(f"=======================================================")
|
||
|
||
req = urllib.request.Request(base_url, headers=HEADERS)
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||
soup = BeautifulSoup(resp.read().decode('utf-8', errors='ignore'), 'html.parser')
|
||
except Exception as e:
|
||
print(f"[-] Ошибка загрузки оглавления {base_url}: {e}")
|
||
return False
|
||
|
||
raw_title = soup.find('title').text
|
||
main_title = raw_title.split('\\')[0].strip() if '\\' in raw_title else raw_title.strip()
|
||
|
||
links = []
|
||
seen = set()
|
||
for a in soup.find_all('a', href=True):
|
||
href = a['href']
|
||
if href.startswith(f'/document/{law_id}/') and len(href) > len(f'/document/{law_id}/'):
|
||
clean_href = href.split('#')[0]
|
||
if clean_href not in seen:
|
||
seen.add(clean_href)
|
||
links.append((clean_href, a.text.strip()))
|
||
|
||
print(f"[+] Найдено разделов/статей: {len(links)}")
|
||
|
||
html_parts = [f"""<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<style>
|
||
body {{ font-family: 'DejaVu Sans', sans-serif; margin: 25mm 20mm; font-size: 10pt; line-height: 1.45; color: #111; }}
|
||
h1 {{ font-size: 14pt; text-align: center; border-bottom: 2px solid #333; padding-bottom: 8px; margin-bottom: 20px; }}
|
||
h2 {{ font-size: 11pt; margin-top: 22px; border-bottom: 1px solid #777; padding-bottom: 4px; color: #002b49; page-break-after: avoid; }}
|
||
p {{ margin: 6px 0; text-align: justify; text-indent: 1.5em; }}
|
||
table {{ width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 9pt; }}
|
||
th, td {{ border: 1px solid #777; padding: 5px 8px; text-align: left; }}
|
||
th {{ background-color: #f0f0f0; }}
|
||
.header-meta {{ text-align: center; font-style: italic; margin-bottom: 25px; color: #444; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>{main_title}</h1>
|
||
<div class="header-meta">Официальный действующий текст в актуальной редакции со всеми изменениями и приложениями</div>
|
||
"""]
|
||
|
||
for i, (href, sec_title) in enumerate(links, 1):
|
||
url = 'https://www.consultant.ru' + href
|
||
print(f" [{i}/{len(links)}] Загрузка: {sec_title[:60]}...")
|
||
r = urllib.request.Request(url, headers=HEADERS)
|
||
try:
|
||
with urllib.request.urlopen(r, timeout=20) as res:
|
||
sec_soup = BeautifulSoup(res.read().decode('utf-8', errors='ignore'), 'html.parser')
|
||
html_parts.append(f'<h2>{sec_title}</h2>')
|
||
for elem in sec_soup.find_all(['p', 'table']):
|
||
if elem.name == 'p':
|
||
t = elem.text.strip()
|
||
if t and not 'Подготовлена редакция' in t and not 'КонсультантПлюс' in t and not 'Открыть документ' in t:
|
||
html_parts.append(f'<p>{t}</p>')
|
||
elif elem.name == 'table':
|
||
html_parts.append(str(elem))
|
||
time.sleep(0.2)
|
||
except Exception as e:
|
||
print(f" [!] Ошибка загрузки раздела {href}: {e}")
|
||
|
||
html_parts.append('</body></html>')
|
||
|
||
tmp_html = f'/tmp/{law_id}.html'
|
||
with open(tmp_html, 'w', encoding='utf-8') as f:
|
||
f.write('\n'.join(html_parts))
|
||
|
||
print(f"[*] Компиляция PDF через wkhtmltopdf: {out_pdf}...")
|
||
subprocess.run([
|
||
'wkhtmltopdf',
|
||
'--encoding', 'utf-8',
|
||
'--margin-top', '20mm',
|
||
'--margin-bottom', '20mm',
|
||
'--margin-left', '15mm',
|
||
'--margin-right', '15mm',
|
||
tmp_html, out_pdf
|
||
], check=True)
|
||
|
||
try:
|
||
doc = fitz.open(out_pdf)
|
||
pages_count = len(doc)
|
||
except Exception:
|
||
pages_count = 0
|
||
|
||
size_kb = os.path.getsize(out_pdf) / 1024
|
||
print(f"[✔] УСПЕШНО СОЗДАН: {out_pdf} ({size_kb:.1f} КБ, {pages_count} страниц)")
|
||
return True
|
||
|
||
def main():
|
||
for doc_info in DOCS:
|
||
build_law(doc_info)
|
||
print("\n[🎉] ВСЕ ЗАКОНЫ РФ ПОЛНОСТЬЮ СОБРАНЫ В PDF!")
|
||
|
||
if __name__ == '__main__':
|
||
main()
|