feat(laws): full statutory corpus builder and visual analytics 2026
This commit is contained in:
parent
0cdfa04d77
commit
ee1989a317
5 changed files with 662 additions and 2 deletions
BIN
email_stats_2026.png
Normal file
BIN
email_stats_2026.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
153
fetch_and_build_remaining_laws.py
Executable file
153
fetch_and_build_remaining_laws.py
Executable file
|
|
@ -0,0 +1,153 @@
|
|||
#!/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()
|
||||
507
generate_email_stats_2026.py
Normal file
507
generate_email_stats_2026.py
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import fitz # PyMuPDF
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
import requests
|
||||
from weasyprint import HTML
|
||||
|
||||
# Telegram settings
|
||||
BOT_TOKEN = "8725618164:AAH1tGalq-pw1l0t4P5c0sdLkCVJdh7IE7M"
|
||||
ADMIN_USER_ID = "197957361"
|
||||
|
||||
# Domain mapping for human-readable naming
|
||||
DOMAINS_MAP = {
|
||||
"gosuslugi.ru": "Госуслуги РФ",
|
||||
"dom.gosuslugi.ru": "ГИС ЖКХ",
|
||||
"sudrf.ru": "Судебная система РФ",
|
||||
"zavolgskiy.uln@sudrf.ru": "Заволжский районный суд",
|
||||
"ulgss.ru": "УО Жилстройсервис",
|
||||
"ulgkh.ru": "Департамент ЖКХ",
|
||||
"fssp.gov.ru": "ФССП России",
|
||||
"fssprus.ru": "ФССП России",
|
||||
"gov.ru": "Правительство РФ",
|
||||
"ulgov.ru": "Администрация Ульяновской обл.",
|
||||
"russianpost.ru": "Почта России",
|
||||
"rkn.gov.ru": "Роскомнадзор",
|
||||
"nalog.gov.ru": "ФНС России",
|
||||
"nalog.ru": "ФНС России",
|
||||
"genproc.gov.ru": "Прокуратура РФ",
|
||||
"epp.genproc.gov.ru": "Прокуратура РФ",
|
||||
"mvd.gov.ru": "МВД России",
|
||||
"mvd.ru": "МВД России",
|
||||
"rospotrebnadzor.ru": "Роспотребнадзор",
|
||||
"pfr.gov.ru": "ПФР / СФР России",
|
||||
"pfrf.ru": "ПФР / СФР России",
|
||||
"fss.ru": "Фонд социального страхования",
|
||||
"sledcom.ru": "Следственный комитет РФ",
|
||||
"vkks.ru": "ВККС РФ",
|
||||
"mailop.ru": "Почтовый шлюз Mailop",
|
||||
"73.mailop.ru": "Почтовый шлюз 73.Mailop",
|
||||
"uk-service.ru": "УК Сервис-М",
|
||||
"dgi.ru": "Департамент гос. имущества",
|
||||
"mos.ru": "Портал Москвы mos.ru",
|
||||
"fsin.gov.ru": "ФСИН России",
|
||||
"skrf.ru": "Следственный комитет РФ",
|
||||
"ugpr.ru": "Управление госзакупок",
|
||||
"fas.gov.ru": "ФАС России",
|
||||
"minstroyrf.gov.ru": "Минстрой РФ",
|
||||
"gkh.ru": "Портал ЖКХ",
|
||||
"msud.ru": "Мировые судьи РФ",
|
||||
"arbitr.ru": "Арбитражные суды РФ",
|
||||
"mirsud": "Мировые судьи",
|
||||
"msud73.ru": "Мировые судьи Ульяновской обл.",
|
||||
"mirsud73.ru": "Мировые судьи Ульяновской обл.",
|
||||
"court.ru": "Судебный департамент РФ",
|
||||
"sud.ru": "Судебный департамент РФ",
|
||||
"uloblsud.ru": "Ульяновский областной суд",
|
||||
"duma.gov.ru": "Государственная Дума РФ",
|
||||
}
|
||||
|
||||
def send_telegram_photo(photo_bytes, caption):
|
||||
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPhoto"
|
||||
files = {
|
||||
"photo": ("stats.png", photo_bytes, "image/png")
|
||||
}
|
||||
data = {
|
||||
"chat_id": ADMIN_USER_ID,
|
||||
"caption": caption,
|
||||
"parse_mode": "HTML"
|
||||
}
|
||||
|
||||
print("Отправка отчета в Telegram...")
|
||||
try:
|
||||
r = requests.post(url, data=data, files=files, timeout=30)
|
||||
if r.status_code == 200:
|
||||
print("🎉 Картинка со статистикой успешно отправлена!")
|
||||
else:
|
||||
print(f"❌ Ошибка отправки: {r.status_code} - {r.text}")
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка отправки запроса в Telegram: {e}")
|
||||
|
||||
def main():
|
||||
folder = Path("/home/matrixhasyou/mutt/exported_emails/all")
|
||||
if not folder.exists():
|
||||
print(f"Папка не существует: {folder}")
|
||||
sys.exit(1)
|
||||
|
||||
pdf_files = list(folder.glob("*.pdf"))
|
||||
print(f"Всего PDF файлов найдено: {len(pdf_files)}")
|
||||
|
||||
stats = {}
|
||||
|
||||
# Summary counters
|
||||
total_in_mails = 0
|
||||
total_in_pages = 0
|
||||
total_in_atts = 0
|
||||
total_out_mails = 0
|
||||
total_out_pages = 0
|
||||
total_out_atts = 0
|
||||
|
||||
# Process files
|
||||
for filepath in pdf_files:
|
||||
filename = filepath.name
|
||||
|
||||
# Parse filename
|
||||
# Format is typically: {match_domain}_{date_part}_{safe_from}_{safe_subject}.pdf
|
||||
# Let's extract date
|
||||
date_match = re.search(r'_(\d{4}-\d{2}-\d{2})_', filename)
|
||||
if not date_match:
|
||||
# Maybe it starts with date: YYYY-MM-DD_domain_...
|
||||
date_match = re.match(r'^(\d{4}-\d{2}-\d{2})_', filename)
|
||||
|
||||
if not date_match:
|
||||
continue
|
||||
|
||||
date_str = date_match.group(1)
|
||||
if not date_str.startswith("2026"):
|
||||
continue
|
||||
|
||||
# Parse domain and sender
|
||||
parts = filename.split("_", 2)
|
||||
if len(parts) >= 2:
|
||||
# If domain first
|
||||
domain = parts[0]
|
||||
# Verify if it looks like domain
|
||||
if not ("." in domain or "судебное" in domain or "other" in domain):
|
||||
# Fallback to general parsing
|
||||
domain = "other"
|
||||
else:
|
||||
domain = "other"
|
||||
|
||||
try:
|
||||
doc = fitz.open(filepath)
|
||||
num_pages = len(doc)
|
||||
first_page_text = doc[0].get_text()
|
||||
|
||||
# Determine direction (inbound / outbound)
|
||||
is_outbound = False
|
||||
first_page_lower = first_page_text.lower()
|
||||
|
||||
# Heuristics
|
||||
if "от: ktybdsq@gmail.com" in first_page_lower or "from: ktybdsq@gmail.com" in first_page_lower:
|
||||
is_outbound = True
|
||||
elif "_ktybdsq_" in filename.lower():
|
||||
is_outbound = True
|
||||
elif "fwd_" in filename.lower() and "от: ktybdsq@gmail.com" in first_page_lower:
|
||||
is_outbound = True
|
||||
|
||||
# Attachment count
|
||||
att_count = 0
|
||||
att_match = re.search(r"(?:Вложения|Attachments):(.*)", first_page_text, re.DOTALL | re.IGNORECASE)
|
||||
if att_match:
|
||||
block_text = att_match.group(1)
|
||||
# Count bytes occurrences as an indicator of attachments
|
||||
att_count = len(re.findall(r"bytes\)", block_text))
|
||||
|
||||
doc.close()
|
||||
|
||||
# Add to stats
|
||||
if domain not in stats:
|
||||
stats[domain] = {
|
||||
'inbound': {'mails': 0, 'pages': 0, 'atts': 0},
|
||||
'outbound': {'mails': 0, 'pages': 0, 'atts': 0}
|
||||
}
|
||||
|
||||
direction = 'outbound' if is_outbound else 'inbound'
|
||||
stats[domain][direction]['mails'] += 1
|
||||
stats[domain][direction]['pages'] += num_pages
|
||||
stats[domain][direction]['atts'] += att_count
|
||||
|
||||
if is_outbound:
|
||||
total_out_mails += 1
|
||||
total_out_pages += num_pages
|
||||
total_out_atts += att_count
|
||||
else:
|
||||
total_in_mails += 1
|
||||
total_in_pages += num_pages
|
||||
total_in_atts += att_count
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка парсинга {filename}: {e}")
|
||||
|
||||
# Group results to consolidate subdomains or same entities
|
||||
grouped_stats = {}
|
||||
for dom, data in stats.items():
|
||||
mapped_name = DOMAINS_MAP.get(dom, dom)
|
||||
|
||||
# General groupings for cleaner output
|
||||
if "sudrf.ru" in dom or "mirsud" in dom or "msud" in dom or "court" in dom or "arbitr" in dom:
|
||||
mapped_name = "Судебная система РФ"
|
||||
elif "gosuslugi.ru" in dom:
|
||||
mapped_name = "Портал Госуслуг РФ"
|
||||
elif "gov.ru" in dom or "ulgov.ru" in dom:
|
||||
mapped_name = "Органы власти / Ведомства"
|
||||
elif "fssp.gov.ru" in dom or "fssprus.ru" in dom:
|
||||
mapped_name = "ФССП (Судебные приставы)"
|
||||
elif "nalog.ru" in dom or "nalog.gov.ru" in dom:
|
||||
mapped_name = "ФНС (Налоговая служба)"
|
||||
|
||||
if mapped_name not in grouped_stats:
|
||||
grouped_stats[mapped_name] = {
|
||||
'inbound': {'mails': 0, 'pages': 0, 'atts': 0},
|
||||
'outbound': {'mails': 0, 'pages': 0, 'atts': 0}
|
||||
}
|
||||
|
||||
for dir_key in ['inbound', 'outbound']:
|
||||
for val_key in ['mails', 'pages', 'atts']:
|
||||
grouped_stats[mapped_name][dir_key][val_key] += data[dir_key][val_key]
|
||||
|
||||
# Sort domains by total mail activity
|
||||
sorted_domains = sorted(
|
||||
grouped_stats.keys(),
|
||||
key=lambda k: grouped_stats[k]['inbound']['mails'] + grouped_stats[k]['outbound']['mails'],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
# Render HTML to PDF and PNG
|
||||
html_content = """
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<style>
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
font-family: 'DejaVu Sans', sans-serif;
|
||||
margin: 0;
|
||||
padding: 30px;
|
||||
background: linear-gradient(135deg, #0b0f19 0%, #111827 100%);
|
||||
color: #f3f4f6;
|
||||
box-sizing: border-box;
|
||||
width: 297mm;
|
||||
height: 210mm;
|
||||
}
|
||||
.container {
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(17, 24, 39, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.6);
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(to right, #00d2ff, #3a7bd5);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.header .subtitle {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.grid {
|
||||
display: flex;
|
||||
gap: 25px;
|
||||
height: calc(100% - 75px);
|
||||
}
|
||||
.table-container {
|
||||
flex: 2.3;
|
||||
overflow: hidden;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
}
|
||||
th {
|
||||
background: rgba(31, 41, 55, 0.8);
|
||||
color: #38bdf8;
|
||||
padding: 10px 12px;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid rgba(255, 255, 255, 0.15);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
.domain-cell {
|
||||
font-weight: bold;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.inbound-text {
|
||||
color: #10b981;
|
||||
font-weight: 600;
|
||||
}
|
||||
.outbound-text {
|
||||
color: #ef4444;
|
||||
font-weight: 600;
|
||||
}
|
||||
.summary-container {
|
||||
flex: 0.7;
|
||||
background: rgba(31, 41, 55, 0.4);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.summary-title {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #818cf8;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.stat-box {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 9.5px;
|
||||
color: #9ca3af;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #f9fafb;
|
||||
}
|
||||
.stat-value.in {
|
||||
color: #10b981;
|
||||
}
|
||||
.stat-value.out {
|
||||
color: #ef4444;
|
||||
}
|
||||
.footer-info {
|
||||
font-size: 9px;
|
||||
color: #4b5563;
|
||||
text-align: right;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='container'>
|
||||
<div class='header'>
|
||||
<div>
|
||||
<h1>⚖️ ОФИЦИАЛЬНАЯ ПЕРЕПИСКА С ВЕДОМСТВАМИ (2026 Г.)</h1>
|
||||
<div class='subtitle'>Анализ входящих и исходящих официальных документов и страниц</div>
|
||||
</div>
|
||||
<div style='text-align: right;'>
|
||||
<span style='font-size: 11px; color: #9ca3af;'>Период: 01.01.2026 - """ + date.today().strftime('%d.%m.%Y') + """</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='grid'>
|
||||
<div class='table-container'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">Ведомство / Организация</th>
|
||||
<th colspan="3" style="text-align: center; border-bottom: 1px solid rgba(255, 255, 255, 0.1);" class="inbound-text">📥 Входящие (мне)</th>
|
||||
<th colspan="3" style="text-align: center; border-bottom: 1px solid rgba(255, 255, 255, 0.1);" class="outbound-text">📤 Исходящие (от меня)</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="text-align: center;">Писем</th>
|
||||
<th style="text-align: center;">Влож.</th>
|
||||
<th style="text-align: center;">Стр.</th>
|
||||
<th style="text-align: center;">Писем</th>
|
||||
<th style="text-align: center;">Влож.</th>
|
||||
<th style="text-align: center;">Стр.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
"""
|
||||
|
||||
for dom in sorted_domains:
|
||||
in_m = grouped_stats[dom]['inbound']['mails']
|
||||
in_a = grouped_stats[dom]['inbound']['atts']
|
||||
in_p = grouped_stats[dom]['inbound']['pages']
|
||||
|
||||
out_m = grouped_stats[dom]['outbound']['mails']
|
||||
out_a = grouped_stats[dom]['outbound']['atts']
|
||||
out_p = grouped_stats[dom]['outbound']['pages']
|
||||
|
||||
if in_m == 0 and out_m == 0:
|
||||
continue
|
||||
|
||||
html_content += f"""
|
||||
<tr>
|
||||
<td class='domain-cell'>{dom}</td>
|
||||
<td style="text-align: center;" class="inbound-text">{in_m if in_m > 0 else '-'}</td>
|
||||
<td style="text-align: center;">{in_a if in_a > 0 else '-'}</td>
|
||||
<td style="text-align: center;">{in_p if in_p > 0 else '-'}</td>
|
||||
<td style="text-align: center;" class="outbound-text">{out_m if out_m > 0 else '-'}</td>
|
||||
<td style="text-align: center;">{out_a if out_a > 0 else '-'}</td>
|
||||
<td style="text-align: center;">{out_p if out_p > 0 else '-'}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
html_content += f"""
|
||||
<tr style="background: rgba(56, 189, 248, 0.08); font-weight: bold; border-top: 2px solid rgba(255, 255, 255, 0.15);">
|
||||
<td style="color: #38bdf8;">ИТОГО:</td>
|
||||
<td style="text-align: center;" class="inbound-text">{total_in_mails}</td>
|
||||
<td style="text-align: center;">{total_in_atts}</td>
|
||||
<td style="text-align: center;">{total_in_pages}</td>
|
||||
<td style="text-align: center;" class="outbound-text">{total_out_mails}</td>
|
||||
<td style="text-align: center;">{total_out_atts}</td>
|
||||
<td style="text-align: center;">{total_out_pages}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='summary-container'>
|
||||
<div>
|
||||
<div class='summary-title'>📊 Сводные данные</div>
|
||||
|
||||
<div class='stat-box'>
|
||||
<div class='stat-label'>Всего писем</div>
|
||||
<div class='stat-value' style='color: #38bdf8;'>{total_in_mails + total_out_mails}</div>
|
||||
</div>
|
||||
|
||||
<div class='stat-box'>
|
||||
<div class='stat-label'>Всего файлов / вложений</div>
|
||||
<div class='stat-value'>{total_in_atts + total_out_atts} шт.</div>
|
||||
</div>
|
||||
|
||||
<div class='stat-box'>
|
||||
<div class='stat-label'>Общий объем документов</div>
|
||||
<div class='stat-value' style='color: #818cf8;'>{total_in_pages + total_out_pages} стр.</div>
|
||||
</div>
|
||||
|
||||
<div class='stat-box' style='border-top: 1px solid rgba(255, 255, 255, 0.08); padding-top: 8px;'>
|
||||
<div class='stat-label'>Входящий поток</div>
|
||||
<div class='stat-value inbound-text' style='font-size: 16px;'>{total_in_mails} писем / {total_in_pages} стр.</div>
|
||||
</div>
|
||||
|
||||
<div class='stat-box'>
|
||||
<div class='stat-label'>Исходящий поток</div>
|
||||
<div class='stat-value outbound-text' style='font-size: 16px;'>{total_out_mails} писем / {total_out_pages} стр.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='footer-info'>
|
||||
LKM37 CORE v4.3<br>
|
||||
Сгенерировано ИИ Antigravity
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# Render HTML to PDF using WeasyPrint
|
||||
print("Рендеринг HTML отчета в PDF...")
|
||||
pdf_bytes = HTML(string=html_content).write_pdf()
|
||||
|
||||
# Convert PDF to PNG
|
||||
print("Конвертация PDF в PNG...")
|
||||
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
||||
page = doc[0]
|
||||
pix = page.get_pixmap(dpi=180)
|
||||
png_bytes = pix.tobytes("png")
|
||||
doc.close()
|
||||
|
||||
# Save the output PNG image
|
||||
out_png_path = Path("/home/matrixhasyou/mutt/email_stats_2026.png")
|
||||
out_png_path.write_bytes(png_bytes)
|
||||
print(f"✅ Итоговый график сохранен: {out_png_path}")
|
||||
|
||||
# Send to Telegram
|
||||
caption = (
|
||||
f"📊 <b>Статистика переписки с ведомствами за весь 2026 год</b>\n\n"
|
||||
f"🔹 Всего писем: <b>{total_in_mails + total_out_mails}</b>\n"
|
||||
f"📥 Входящих: <b>{total_in_mails}</b> (вложений: {total_in_atts}, страниц: {total_in_pages})\n"
|
||||
f"📤 Исходящих: <b>{total_out_mails}</b> (вложений: {total_out_atts}, страниц: {total_out_pages})\n"
|
||||
f"📁 Общий объем архива: <b>{total_in_pages + total_out_pages} страниц</b>"
|
||||
)
|
||||
send_telegram_photo(png_bytes, caption)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1 +1 @@
|
|||
2026-06-26 21:41:52
|
||||
2026-10-13 02:00:34
|
||||
|
|
@ -1 +1 @@
|
|||
{"status": "success", "step": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", "updated_at": "2026-06-26 21:41:52", "new_emails": 1, "total_emails": 0, "pdf_size_mb": 180.84}
|
||||
{"status": "success", "step": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", "updated_at": "2026-10-13 02:00:35", "new_emails": 2, "total_emails": 612, "pdf_size_mb": 203.87}
|
||||
Loading…
Reference in a new issue