#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ kdoc — навигация по справке SDK КОМПАС-3D (Help & Manual WebHelp) для агента. Справка — это ~26 000 HTML-страниц в docs/KOMPAS_SDK_ru-RU/ со «зашумлённой» вёрсткой (шапка, подвал, JS). Листать их напрямую неэффективно. Этот инструмент: * build-index — один раз строит компактный индекс docs/kompas_sdk_index.tsv (файлзаголовоккраткое описание) из zoom_pageinfo.js; * search — ищет страницы по подстроке в заголовке/описании; * read — печатает чистый текст одной страницы (тело топика без вёрстки). Типичный сценарий агента: python tools/kdoc.py search "выдавливание" # найти нужный интерфейс python tools/kdoc.py read ibossextrusiondefinition.html Поиск по индексу можно вести и встроенным Grep по docs/kompas_sdk_index.tsv — формат строки: <имя_файла>\t<заголовок>\t<описание>. """ import sys import os import re import json import html import argparse from html.parser import HTMLParser # На Windows консоль часто cp1251 — принудительно выводим UTF-8, # чтобы кириллица не искажалась при вызове из агента/любого шелла. for _stream in (sys.stdout, sys.stderr): try: _stream.reconfigure(encoding="utf-8") except (AttributeError, ValueError): pass HERE = os.path.dirname(os.path.abspath(__file__)) DOC_DIR = os.environ.get( "KDOC_DIR", os.path.join(HERE, "..", "docs", "KOMPAS_SDK_ru-RU") ) DOC_DIR = os.path.abspath(DOC_DIR) INDEX_PATH = os.path.abspath(os.path.join(DOC_DIR, "..", "kompas_sdk_index.tsv")) PAGEINFO = os.path.join(DOC_DIR, "zoom_pageinfo.js") # Текстовый «мусор» навигации/подвала, попадающий в тело топика. NOISE = { "please enable javascript to view this site.", "scroll", "предыдущий", "вверх", "следующий", "содержание", "содержане", "указатель", "поиск", "подразделы:", "(отсутствуют)", } FOOTER_RE = re.compile(r"©?\s*ООО\s*«?АСКОН") def build_index() -> int: """Разобрать массив pagedata из zoom_pageinfo.js в TSV-индекс.""" if not os.path.isfile(PAGEINFO): sys.exit(f"Не найден {PAGEINFO}. Проверьте KDOC_DIR / путь к справке.") with open(PAGEINFO, "r", encoding="utf-8", errors="replace") as f: text = f.read() marker = "pagedata = " pos = text.find(marker) if pos < 0: sys.exit("В zoom_pageinfo.js не найден массив pagedata.") payload = text[pos + len(marker):].strip() payload = payload.rstrip(";").strip() data = json.loads(payload) # [["./file.html","title","desc","img"], ...] rows = [] for entry in data: if not entry or not entry[0]: continue fname = entry[0].lstrip("./").strip() title = html.unescape((entry[1] or "").strip()) desc = html.unescape((entry[2] or "").strip()) title = re.sub(r"\s+", " ", title) desc = re.sub(r"\s+", " ", desc)[:300] rows.append(f"{fname}\t{title}\t{desc}") with open(INDEX_PATH, "w", encoding="utf-8", newline="\n") as f: f.write("\n".join(rows) + "\n") print(f"Индекс записан: {INDEX_PATH} ({len(rows)} страниц)") return len(rows) def search(terms, limit=40): """Грубый поиск по индексу: AND по всем подстрокам, без регистра.""" if not os.path.isfile(INDEX_PATH): print("Индекс отсутствует — строю...", file=sys.stderr) build_index() needles = [t.lower() for t in terms if t.strip()] hits = 0 with open(INDEX_PATH, "r", encoding="utf-8") as f: for line in f: low = line.lower() if all(n in low for n in needles): parts = line.rstrip("\n").split("\t") fname = parts[0] title = parts[1] if len(parts) > 1 else "" print(f"{fname}\t{title}") hits += 1 if hits >= limit: print(f"... (показаны первые {limit}; уточните запрос)") break if hits == 0: print("Ничего не найдено.") class _TopicExtractor(HTMLParser): """Собирает текст из
, пропуская script/style.""" BLOCK = {"p", "div", "br", "tr", "li", "h1", "h2", "h3", "h4", "table", "pre"} def __init__(self): super().__init__(convert_charrefs=True) self.depth = 0 # глубина вложенности внутри topicbody self.capturing = False self.skip = 0 # внутри script/style self.out = [] def handle_starttag(self, tag, attrs): attrd = dict(attrs) if not self.capturing and attrd.get("id") == "topicbody": self.capturing = True self.depth = 1 return if not self.capturing: return if tag in ("script", "style"): self.skip += 1 if tag in self.BLOCK: self.out.append("\n") if tag != "br": self.depth += 1 def handle_endtag(self, tag): if not self.capturing: return if tag in ("script", "style") and self.skip: self.skip -= 1 if tag != "br": self.depth -= 1 if self.depth <= 0: self.capturing = False def handle_data(self, data): if self.capturing and not self.skip: self.out.append(data) def read(fname): path = os.path.join(DOC_DIR, os.path.basename(fname)) if not path.endswith(".html"): path += ".html" if not os.path.isfile(path): sys.exit(f"Нет файла: {path}") with open(path, "r", encoding="utf-8", errors="replace") as f: raw = f.read() p = _TopicExtractor() p.feed(raw) text = "".join(p.out) cleaned = [] for line in text.splitlines(): s = re.sub(r"[ \t ]+", " ", line).strip() if not s: continue if s.lower() in NOISE: continue if FOOTER_RE.search(s): break # подвал — дальше только копирайт/телефон cleaned.append(s) print(os.path.basename(path)) print("=" * len(os.path.basename(path))) print("\n".join(cleaned).strip()) def main(): ap = argparse.ArgumentParser( prog="kdoc", description="Навигация по справке SDK КОМПАС-3D для агента." ) sub = ap.add_subparsers(dest="cmd", required=True) sub.add_parser("build-index", help="построить docs/kompas_sdk_index.tsv") sp = sub.add_parser("search", help="искать по заголовку/описанию") sp.add_argument("terms", nargs="+") sp.add_argument("--limit", type=int, default=40) rp = sub.add_parser("read", help="печать чистого текста страницы") rp.add_argument("file") args = ap.parse_args() if args.cmd == "build-index": build_index() elif args.cmd == "search": search(args.terms, args.limit) elif args.cmd == "read": read(args.file) if __name__ == "__main__": main()