#!/usr/bin/env python3 """ RAG System for Local Ollama Конвертирует html-файлы в markdown, очищая от лишней разметки Скрипт сгенерирован claude-sonnet-4 """ import os import re import json from bs4 import BeautifulSoup from pathlib import Path def clean_confluence_html(soup): """ Удаляет Confluence-специфичные элементы и очищает HTML. """ # Удаляем Confluence макросы (structured-macro) for macro in soup.find_all('ac:structured-macro'): macro_name = macro.get('ac:name', '') # Сохраняем содержимое некоторых макросов if macro_name == 'note': # Преобразуем заметки в блоки внимания rich_text = macro.find('ac:rich-text-body') if rich_text: note_content = rich_text.get_text(strip=True) note_tag = soup.new_tag('div', class_='note') note_tag.string = f"📝 **Примечание:** {note_content}" macro.replace_with(note_tag) else: macro.decompose() elif macro_name == 'toc': # Заменяем TOC на текст toc_tag = soup.new_tag('div') toc_tag.string = "**Содержание** (автогенерируется)" macro.replace_with(toc_tag) elif macro_name == 'drawio': # Заменяем диаграммы на заглушку diagram_name = macro.find('ac:parameter', {'ac:name': 'diagramName'}) if diagram_name: diagram_text = diagram_name.get_text() else: diagram_text = 'Диаграмма' diagram_tag = soup.new_tag('div') diagram_tag.string = f"🖼️ **Диаграмма:** {diagram_text}" macro.replace_with(diagram_tag) else: # Удаляем остальные макросы macro.decompose() # Удаляем другие Confluence элементы for element in soup.find_all(True): if element.name and element.name.startswith('ac:'): element.decompose() return soup def convert_table_to_markdown(table): """ Конвертирует HTML таблицу в Markdown формат. """ rows = table.find_all('tr') if not rows: return "" markdown_lines = [] # Обработка первой строки как заголовка first_row = rows[0] header_cells = first_row.find_all(['th', 'td']) if not header_cells: return "" # Заголовок таблицы header_line = "|" separator_line = "|" for cell in header_cells: # Получаем текст и очищаем его cell_text = cell.get_text(separator=' ', strip=True) cell_text = re.sub(r'\s+', ' ', cell_text) # Заменяем множественные пробелы cell_text = cell_text.replace('|', '\\|') # Экранируем pipe символы header_line += f" {cell_text} |" separator_line += " --- |" markdown_lines.append(header_line) markdown_lines.append(separator_line) # Обработка остальных строк for row in rows[1:]: data_cells = row.find_all(['td', 'th']) if not data_cells: continue data_line = "|" for i, cell in enumerate(data_cells): if i >= len(header_cells): # Не больше столбцов чем в заголовке break cell_text = cell.get_text(separator=' ', strip=True) cell_text = re.sub(r'\s+', ' ', cell_text) cell_text = cell_text.replace('|', '\\|') data_line += f" {cell_text} |" # Дополняем недостающие столбцы missing_cols = len(header_cells) - len(data_cells) for _ in range(missing_cols): data_line += " |" markdown_lines.append(data_line) return "\n".join(markdown_lines) def extract_json_blocks(soup): """ Извлекает и форматирует JSON блоки. """ json_blocks = [] # Ищем потенциальные JSON блоки в pre, code и script тегах for element in soup.find_all(['pre', 'code', 'script']): text_content = element.get_text(strip=True) # Простая проверка на JSON if text_content and ( (text_content.startswith('{') and text_content.endswith('}')) or (text_content.startswith('[') and text_content.endswith(']')) ): try: # Пытаемся парсить как JSON json_data = json.loads(text_content) formatted_json = json.dumps(json_data, indent=2, ensure_ascii=False) # Заменяем элемент на форматированный JSON блок json_tag = soup.new_tag('pre') json_tag.string = f"```json\n{formatted_json}\n```" element.replace_with(json_tag) json_blocks.append(formatted_json) except json.JSONDecodeError: # Если не JSON, оставляем как code block if element.name in ['pre', 'code']: element.string = f"```\n{text_content}\n```" return json_blocks def html_to_markdown(html_content): """ Основная функция конвертации HTML в Markdown. """ soup = BeautifulSoup(html_content, 'html.parser') # Удаляем скрипты и стили for element in soup(['script', 'style']): element.decompose() # Очищаем Confluence элементы soup = clean_confluence_html(soup) # Извлекаем JSON блоки json_blocks = extract_json_blocks(soup) # Конвертируем таблицы for table in soup.find_all('table'): markdown_table = convert_table_to_markdown(table) if markdown_table: # Заменяем таблицу на Markdown table_div = soup.new_tag('div', class_='markdown-table') table_div.string = f"\n{markdown_table}\n" table.replace_with(table_div) # Обработка заголовков for level in range(1, 7): for header in soup.find_all(f'h{level}'): header_text = header.get_text(strip=True) markdown_header = '#' * level + ' ' + header_text header.string = markdown_header # Обработка списков for ul in soup.find_all('ul'): list_items = ul.find_all('li', recursive=False) if list_items: markdown_list = [] for li in list_items: item_text = li.get_text(strip=True) markdown_list.append(f"- {item_text}") ul.string = '\n'.join(markdown_list) for ol in soup.find_all('ol'): list_items = ol.find_all('li', recursive=False) if list_items: markdown_list = [] for i, li in enumerate(list_items, 1): item_text = li.get_text(strip=True) markdown_list.append(f"{i}. {item_text}") ol.string = '\n'.join(markdown_list) # Обработка жирного и курсивного текста for strong in soup.find_all(['strong', 'b']): text = strong.get_text() strong.string = f"**{text}**" for em in soup.find_all(['em', 'i']): text = em.get_text() em.string = f"*{text}*" # Получаем финальный текст text = soup.get_text(separator='\n', strip=True) # Постобработка lines = [] for line in text.split('\n'): line = line.strip() if line: lines.append(line) # Убираем лишние пустые строки result_lines = [] prev_empty = False for line in lines: if not line: if not prev_empty: result_lines.append('') prev_empty = True else: result_lines.append(line) prev_empty = False return '\n'.join(result_lines) def process_files(input_dir, output_dir): """ Обрабатывает все HTML-файлы в директории. """ input_path = Path(input_dir) output_path = Path(output_dir) if not input_path.exists(): print(f"❌ Директория {input_dir} не найдена") return # Создаем выходную директорию output_path.mkdir(parents=True, exist_ok=True) html_files = list(input_path.glob('*.html')) if not html_files: print(f"❌ HTML файлы не найдены в {input_dir}") return print(f"📁 Найдено {len(html_files)} HTML файлов") successful = 0 failed = 0 failed_files = [] for html_file in html_files: print(f"🔄 Обработка: {html_file.name}") try: # Читаем HTML файл with open(html_file, 'r', encoding='utf-8') as f: html_content = f.read() # Проверяем, что файл не пустой if not html_content.strip(): print(f"⚠️ Пропущен: {html_file.name} (пустой файл)") continue # Конвертируем в Markdown markdown_content = html_to_markdown(html_content) # Проверяем результат конвертации if not markdown_content.strip(): print(f"⚠️ Предупреждение: {html_file.name} - результат конвертации пустой") # Сохраняем результат md_filename = html_file.stem + '.md' md_filepath = output_path / md_filename with open(md_filepath, 'w', encoding='utf-8') as f: f.write(markdown_content) print(f"✅ Сохранено: {md_filename}") successful += 1 except UnicodeDecodeError as e: print(f"❌ Ошибка кодировки в {html_file.name}: {str(e)}") failed += 1 failed_files.append((html_file.name, f"Ошибка кодировки: {str(e)}")) except Exception as e: print(f"❌ Ошибка при обработке {html_file.name}: {str(e)}") failed += 1 failed_files.append((html_file.name, str(e))) print(f"\n📊 Результат:") print(f"✅ Успешно обработано: {successful}") print(f"❌ Ошибок: {failed}") if failed_files: print(f"\n📋 Список файлов с ошибками:") for filename, error in failed_files: print(f" • {filename}: {error}") print(f"📂 Результаты сохранены в: {output_dir}") if __name__ == "__main__": input_directory = "./input_html" output_directory = "./output_md" print("🚀 Запуск конвертера HTML → Markdown") print("=" * 50) process_files(input_directory, output_directory)