Введение
Многие команды накапливают пользовательскую документацию в корпоративных wiki-системах (Confluence, MediaWiki, Notion) задолго до решения встроить справку прямо в приложение — переносить весь этот объём контента вручную в HTML и .qhp непрактично при значительном объёме существующих страниц. В статье разберём практический подход к автоматизированной миграции экспортированного wiki-контента в рабочий .qch, включая типичные проблемы такого переноса.
Концепция
Большинство wiki-систем поддерживают экспорт контента в распространённые промежуточные форматы — Confluence экспортирует пространство в HTML или XML (через REST API или встроенный экспорт), MediaWiki предоставляет дамп в собственном XML-формате с разметкой в синтаксисе MediaWiki. Стратегия миграции — написать одноразовый (или регулярно запускаемый, если решено поддерживать оба источника параллельно некоторое время) конвертер, который разбирает экспортированный формат, нормализует ссылки между страницами (внутренние wiki-ссылки обычно ведут на ID страницы или специфичный wiki-URL, не совпадающий со структурой будущего .qch) и генерирует чистый HTML и .qhp по уже знакомой из предыдущих статей схеме.
Пример кода: Миграция существующей wiki/Confluence-документации
#!/usr/bin/env python3
# migrate_confluence_export.py — конвертация экспорта Confluence (HTML) в структуру для .qch
import os
import re
import json
from bs4 import BeautifulSoup
def load_page_id_mapping(export_dir):
"""Confluence экспортирует файл index.html со списком страниц и их ID."""
mapping = {}
with open(os.path.join(export_dir, "index.html"), encoding="utf-8") as f:
soup = BeautifulSoup(f.read(), "html.parser")
for link in soup.select("a[href]"):
href = link["href"]
match = re.search(r"(\d+)\.html$", href)
if match:
page_id = match.group(1)
mapping[page_id] = {"filename": href, "title": link.get_text(strip=True)}
return mapping
def normalize_page(filepath, page_mapping, output_dir):
with open(filepath, encoding="utf-8") as f:
soup = BeautifulSoup(f.read(), "html.parser")
# Confluence оборачивает реальный контент в специфичную обёртку —
# извлекаем только содержательную часть, отбрасывая навигацию/футер Confluence
content = soup.select_one("#main-content") or soup.body
# Переписываем внутренние ссылки на страницы по ID в относительные пути
for link in content.select("a[href]"):
match = re.search(r"pageId=(\d+)", link.get("href", ""))
if match and match.group(1) in page_mapping:
target = page_mapping[match.group(1)]["filename"]
link["href"] = target
# Удаляем макросы и элементы, специфичные для Confluence UI
for unwanted in content.select(".confluence-information-macro, .page-metadata"):
unwanted.decompose()
clean_html = f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>{soup.title.string if soup.title else ""}</title></head>
<body>{content}</body></html>"""
out_filename = os.path.basename(filepath)
with open(os.path.join(output_dir, out_filename), "w", encoding="utf-8") as f:
f.write(clean_html)
return out_filename
def main(export_dir, output_dir):
os.makedirs(output_dir, exist_ok=True)
page_mapping = load_page_id_mapping(export_dir)
converted_files = []
for page_id, info in page_mapping.items():
source_path = os.path.join(export_dir, info["filename"])
if os.path.isfile(source_path):
converted_files.append(normalize_page(source_path, page_mapping, output_dir))
else:
print(f"Предупреждение: страница не найдена при экспорте: {info['filename']}")
print(f"Сконвертировано страниц: {len(converted_files)}")
with open(os.path.join(output_dir, "_manifest.json"), "w", encoding="utf-8") as f:
json.dump(converted_files, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
import sys
main(sys.argv[1], sys.argv[2])
Пояснения к коду
load_page_id_mapping() разбирает индексный файл, который Confluence создаёт при полном экспорте пространства, и строит соответствие между внутренними идентификаторами страниц Confluence и именами файлов в экспорте — это нужно, поскольку внутренние ссылки внутри экспортированных HTML-страниц обычно ссылаются на страницы через pageId=12345, а не через прямое имя файла, и без этого соответствия ссылки между перенесёнными страницами будут битыми в новой структуре.
normalize_page() выполняет основную работу очистки: извлекает содержательную часть страницы (#main-content — типичный идентификатор основного контента в экспорте Confluence, может отличаться в зависимости от используемой темы), переписывает внутренние ссылки на новую, относительную схему путей, и удаляет элементы, специфичные для интерфейса Confluence (информационные макросы, метаданные страницы), которые не несут смысловой нагрузки для конечного читателя справки внутри приложения. Сгенерированный _manifest.json со списком успешно сконвертированных файлов облегчает последующий шаг — программную генерацию .qhp на основе этого списка, аналогично подходу из статьи 170.
Подводные камни
- Структура экспорта Confluence (и других wiki-систем) может меняться между версиями платформы, делая жёстко закодированные селекторы (
#main-content, классы CSS) хрупкими — конвертер, написанный и протестированный для конкретной версии экспорта, может потребовать корректировки селекторов при следующем крупном обновлении wiki-платформы или при экспорте из другого пространства с иначе настроенной темой оформления. - Вложенные макросы и динамический контент Confluence (включённые таблицы из других страниц, виджеты с диаграммами, построенными через сторонние плагины) часто не экспортируются как статичный HTML вовсе, оставляя пустые «дырки» в контенте на месте такого встроенного материала — миграция таких страниц требует либо ручной доработки после автоматической конвертации, либо явного выявления и помечания «проблемных» страниц для последующей ручной проверки, как показано предупреждением о ненайденных файлах в примере.
- Постоянная синхронизация двух источников документации (wiki и встроенная справка) после однократной миграции — если команда продолжает редактировать контент в исходной wiki после переноса, не имея процесса регулярной повторной синхронизации, встроенная справка быстро устаревает относительно живого источника; нужно явно решить, становится ли .qch единственным источником истины после миграции (с прекращением редактирования в wiki) или процесс конвертации должен быть автоматизирован и запускаться регулярно как часть пайплайна сборки.
- Потеря структуры навигации и иерархии разделов wiki-пространства при простом плоском экспорте. Wiki-системы обычно имеют свою иерархию страниц (родитель-потомок), которая не всегда тривиально извлекается из экспорта в виде, готовом для прямого построения TOC — без явного извлечения этой иерархии (через API wiki-системы, а не только статический HTML-экспорт) результирующий .qhp может получить плоскую, не структурированную навигацию вместо осмысленного дерева содержания.