feat: add document optimization script
This commit is contained in:
@@ -0,0 +1,289 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Универсальный скрипт для улучшения MD-файлов документов АО «ХК «Сибцем».
|
||||||
|
|
||||||
|
Применяет паттерны:
|
||||||
|
1. Удаляет футеры страниц
|
||||||
|
2. Форматирует блок "УТВЕРЖДАЮ"
|
||||||
|
3. Выравнивает иерархию заголовков
|
||||||
|
4. Объединяет таблицы терминов
|
||||||
|
5. Добавляет разделители между разделами
|
||||||
|
6. Приводит таблицы к единому виду
|
||||||
|
|
||||||
|
Использование:
|
||||||
|
python3 optimize_document.py <путь_к_оригиналу_на_сервере>
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
python3 optimize_document.py "/opt/documents_xk/АОХКСибцем/07 Служба.../08 Положение Управление ИТ-инцидентами от 20.02.2024 № ПОЛ-123-2.md"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
# Конфигурация
|
||||||
|
SSH_KEY = "/home/hermes/.ssh/192.168.1.106"
|
||||||
|
SSH_USER = "administrator@192.168.1.106"
|
||||||
|
SERVER_BASE = "/opt/documents_xk/АОХКСибцем"
|
||||||
|
FILEBROWSER_URL = "https://smb.dddennnisss.ru"
|
||||||
|
FILEBROWSER_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJGaWxlQnJvd3NlciBRdWFudHVtIiwiZXhwIjoyMzAyNzM1NzA3LCJpYXQiOjE3ODQzMzU3MDcsImJlbG9uZ3NUbyI6MSwiUGVybWlzc2lvbnMiOnsiYXBpIjp0cnVlLCJhZG1pbiI6dHJ1ZSwibW9kaWZ5Ijp0cnVlLCJzaGFyZSI6dHJ1ZSwicmVhbHRpbWUiOmZhbHNlLCJkZWxldGUiOnRydWUsImNyZWF0ZSI6dHJ1ZSwiZG93bmxvYWQiOnRydWV9fQ.QBKlh3NQJeh4aeMiKLkhg0tF8hxk-Oh2KLsXLSbuvm8"
|
||||||
|
SOURCE_NAME = "SSD-Storage"
|
||||||
|
|
||||||
|
|
||||||
|
def run_ssh(command):
|
||||||
|
"""Выполнить команду на сервере через SSH."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["ssh", "-i", SSH_KEY, "-o", "StrictHostKeyChecking=no",
|
||||||
|
SSH_USER, command],
|
||||||
|
capture_output=True, text=True, timeout=30
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def scp_download(remote_path, local_path):
|
||||||
|
"""Скачать файл с сервера."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["scp", "-i", SSH_KEY, "-o", "StrictHostKeyChecking=no",
|
||||||
|
f"{SSH_USER}:{remote_path}", local_path],
|
||||||
|
capture_output=True, text=True, timeout=60
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def fix_headings(content):
|
||||||
|
"""Выровнять иерархию заголовков: все на уровень ##."""
|
||||||
|
content = re.sub(r'^(#+)\s+(.+)$', r'## \2', content, flags=re.MULTILINE)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def add_dividers(content):
|
||||||
|
"""Добавить разделители между основными разделами."""
|
||||||
|
# Разделитель перед Оглавлением
|
||||||
|
content = re.sub(
|
||||||
|
r'(\n)(## )(Оглавление)',
|
||||||
|
r'\1\n---\n\1\2\3',
|
||||||
|
content
|
||||||
|
)
|
||||||
|
# Разделители перед разделами 1-9
|
||||||
|
content = re.sub(
|
||||||
|
r'(\n)(## )(\s*(?:1\.|2\.|3\.|4\.|5\.|6\.|7\.|8\.|9\.))',
|
||||||
|
r'\1\n---\n\1\2\3',
|
||||||
|
content
|
||||||
|
)
|
||||||
|
# Разделители перед Приложениями
|
||||||
|
content = re.sub(
|
||||||
|
r'(\n)(## )(Приложение)',
|
||||||
|
r'\1\n---\n\1\2\3',
|
||||||
|
content
|
||||||
|
)
|
||||||
|
# Разделитель перед Листом согласований
|
||||||
|
content = re.sub(
|
||||||
|
r'(\n)(## )(Лист согласований)',
|
||||||
|
r'\1\n---\n\1\2\3',
|
||||||
|
content
|
||||||
|
)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def fix_tables(content):
|
||||||
|
"""Привести все таблицы к единому виду с thead/tbody."""
|
||||||
|
def fix_table(table):
|
||||||
|
if '<thead>' in table and '<tbody>' in table:
|
||||||
|
return table
|
||||||
|
rows = re.findall(r'<tr>(.*?)</tr>', table, re.DOTALL)
|
||||||
|
if not rows:
|
||||||
|
return table
|
||||||
|
|
||||||
|
header = '<thead><tr>' + rows[0] + '</tr></thead><tbody>'
|
||||||
|
body = ''
|
||||||
|
for row in rows[1:]:
|
||||||
|
body += '<tr>' + row + '</tr>'
|
||||||
|
body += '</tbody>'
|
||||||
|
|
||||||
|
return '<table>' + header + body + '</table>'
|
||||||
|
|
||||||
|
return re.sub(r'<table>.*?</table>', lambda m: fix_table(m.group(0)), content, flags=re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_footers(content):
|
||||||
|
"""Удалить футеры страниц."""
|
||||||
|
# Удалить "АО «ХК «Сибцем»\nТип документа: ...\nНаименование документа: ..."
|
||||||
|
content = re.sub(
|
||||||
|
r'АО «ХК «Сибцем»\nТип документа:.*?\nНаименование документа:.*?\n\n',
|
||||||
|
'',
|
||||||
|
content,
|
||||||
|
flags=re.DOTALL
|
||||||
|
)
|
||||||
|
# Удалить "Стр. X из Y"
|
||||||
|
content = re.sub(r'Стр\. \d+ из \d+\n\n', '', content)
|
||||||
|
# Удалить "Дата утверждения: DD.MM.YYYY"
|
||||||
|
content = re.sub(r'Дата утверждения: \d{2}\.\d{2}\.\d{4}\n\n', '', content)
|
||||||
|
# Удалить "Ведущее подразделение: ..."
|
||||||
|
content = re.sub(r'Ведущее подразделение:.*?\n\n', '', content)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def fix_approval_block(content):
|
||||||
|
"""Форматировать блок УТВЕРЖДАЮ."""
|
||||||
|
# Паттерн 1: "УТВЕРЖДАЮ\nПрезидент\n\nО.В. Шарыкин\n\n«XX» месяц год г."
|
||||||
|
approval_patterns = [
|
||||||
|
r'(УТВЕРЖДАУ\nПрезидент\n\nО\.В\. Шарыкин\n\n«\d+» \w+ \d{4} г\.)',
|
||||||
|
r'(УТВЕРЖДАУ\nПрезидент\n\nО\.В\. Шарыкин\n\n«\d+» \w+ \d{4} г\.)',
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in approval_patterns:
|
||||||
|
match = re.search(pattern, content)
|
||||||
|
if match:
|
||||||
|
old_text = match.group(0)
|
||||||
|
new_text = f'''<div align="right">
|
||||||
|
<strong>УТВЕРЖДАУ</strong><br>
|
||||||
|
Президент<br>
|
||||||
|
О.В. Шарыкин<br>
|
||||||
|
{old_text.split("«")[1].split("г.")[0]} г.
|
||||||
|
</div>'''
|
||||||
|
content = content.replace(old_text, new_text)
|
||||||
|
break
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def merge_terms_tables(content):
|
||||||
|
"""Объединить разбитые таблицы терминов."""
|
||||||
|
# Поиск таблиц терминов
|
||||||
|
terms_pattern = r'(<table>.*?Термины.*?</table>)'
|
||||||
|
matches = re.findall(terms_pattern, content, re.DOTALL)
|
||||||
|
|
||||||
|
if len(matches) > 1:
|
||||||
|
# Объединяем все таблицы терминов в одну
|
||||||
|
merged = '<table><thead><tr><th>Термин</th><th>Определение</th></tr></thead><tbody>'
|
||||||
|
for table in matches:
|
||||||
|
rows = re.findall(r'<tr>(.*?)</tr>', table, re.DOTALL)
|
||||||
|
for row in rows[1:]: # Пропускаем заголовок
|
||||||
|
merged += row
|
||||||
|
merged += '</tbody></table>'
|
||||||
|
content = content.replace(matches[0], merged)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def optimize_document(md_path):
|
||||||
|
"""Основная функция оптимизации документа."""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
print(f"📄 Обработка документа: {md_path}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Шаг 1: Скачать MD с сервера
|
||||||
|
print("\n📥 Шаг 1: Скачивание MD...")
|
||||||
|
result = run_ssh(f"cat '{md_path}'")
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"✗ Ошибка при скачивании: {result.stderr}")
|
||||||
|
return
|
||||||
|
|
||||||
|
md_content = result.stdout
|
||||||
|
print(f"✓ MD скачан: {len(md_content)} символов")
|
||||||
|
|
||||||
|
# Шаг 2: Применить паттерны
|
||||||
|
print("\n🔧 Шаг 2: Применение паттернов...")
|
||||||
|
|
||||||
|
# 2.1 Удалить футеры
|
||||||
|
content = remove_footers(md_content)
|
||||||
|
print(" ✓ Футеры удалены")
|
||||||
|
|
||||||
|
# 2.2 Выровнять иерархию заголовков
|
||||||
|
content = fix_headings(content)
|
||||||
|
print(" ✓ Иерархия заголовков выровнена")
|
||||||
|
|
||||||
|
# 2.3 Добавить разделители
|
||||||
|
content = add_dividers(content)
|
||||||
|
print(" ✓ Разделители добавлены")
|
||||||
|
|
||||||
|
# 2.4 Привести таблицы к единому виду
|
||||||
|
content = fix_tables(content)
|
||||||
|
print(" ✓ Таблицы приведены к единому виду")
|
||||||
|
|
||||||
|
# 2.5 Объединить таблицы терминов
|
||||||
|
content = merge_terms_tables(content)
|
||||||
|
print(" ✓ Таблицы терминов объединены")
|
||||||
|
|
||||||
|
# 2.6 Форматировать блок УТВЕРЖДАЮ
|
||||||
|
content = fix_approval_block(content)
|
||||||
|
print(" ✓ Блок УТВЕРЖДАЮ отформатирован")
|
||||||
|
|
||||||
|
# Сохранить улучшенную версию
|
||||||
|
improved_path = "/tmp/improved_document.md"
|
||||||
|
with open(improved_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print(f"\n📊 Результаты:")
|
||||||
|
print(f" Исходный размер: {len(md_content)} символов")
|
||||||
|
print(f" Улучшенный размер: {len(content)} символов")
|
||||||
|
|
||||||
|
# Показать структуру
|
||||||
|
sections = re.findall(r'^(##)\s+(.+)$', content, re.MULTILINE)
|
||||||
|
print(f" Разделов: {len(sections)}")
|
||||||
|
|
||||||
|
tables = re.findall(r'<table>', content)
|
||||||
|
print(f" Таблиц: {len(tables)}")
|
||||||
|
|
||||||
|
# Показать первые 300 символов
|
||||||
|
print(f"\n📝 Начало файла:")
|
||||||
|
print(content[:300].replace('\n', '\\n'))
|
||||||
|
|
||||||
|
# Загрузить в filebrowser
|
||||||
|
print("\n📤 Шаг 3: Загрузка в filebrowser...")
|
||||||
|
upload_to_filebrowser(content, md_path)
|
||||||
|
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print(f"✅ Готово! Время выполнения: {elapsed:.1f} сек")
|
||||||
|
print(f"{'=' * 60}")
|
||||||
|
|
||||||
|
|
||||||
|
def upload_to_filebrowser(content, md_path):
|
||||||
|
"""Загрузить файл в filebrowser."""
|
||||||
|
# Извлечь имя файла из пути
|
||||||
|
filename = md_path.split('/')[-1]
|
||||||
|
# Добавить "_improved" перед расширением
|
||||||
|
if filename.endswith('.md'):
|
||||||
|
improved_filename = filename[:-3] + '_improved.md'
|
||||||
|
else:
|
||||||
|
improved_filename = filename + '_improved'
|
||||||
|
|
||||||
|
session = requests.Session()
|
||||||
|
session.cookies.set("filebrowser_quantum_jwt", FILEBROWSER_TOKEN, domain="smb.dddennnisss.ru")
|
||||||
|
|
||||||
|
url = f"{FILEBROWSER_URL}/api/resources"
|
||||||
|
params = {"path": improved_filename, "source": SOURCE_NAME}
|
||||||
|
|
||||||
|
r = session.put(url, params=params, data=content)
|
||||||
|
|
||||||
|
if r.status_code == 200:
|
||||||
|
print(f"✓ Файл загружен: {improved_filename}")
|
||||||
|
|
||||||
|
# Проверить
|
||||||
|
r = session.get(f"{FILEBROWSER_URL}/api/resources", params={"path": "/", "source": SOURCE_NAME})
|
||||||
|
if r.status_code == 200:
|
||||||
|
data = r.json()
|
||||||
|
items = data.get("files", []) + data.get("folders", [])
|
||||||
|
for item in items:
|
||||||
|
if improved_filename in item["name"]:
|
||||||
|
print(f"✓ Размер: {item['size']} байт")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print(f"✗ Ошибка загрузки: {r.status_code} - {r.text[:200]}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Использование:")
|
||||||
|
print(" python3 optimize_document.py <путь_к_оригиналу_на_сервере>")
|
||||||
|
print("\nПример:")
|
||||||
|
print(" python3 optimize_document.py \"/opt/documents_xk/АОХКСибцем/07 Служба.../08 Положение Управление ИТ-инцидентами от 20.02.2024 № ПОЛ-123-2.md\"")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
md_path = sys.argv[1]
|
||||||
|
optimize_document(md_path)
|
||||||
Reference in New Issue
Block a user