Files
llm/scripts/discover_1c_its_start_links.py
T

185 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import argparse
import json
import os
import re
import time
import urllib.parse
import urllib.request
from html.parser import HTMLParser
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_OUTPUT = ROOT / "plugins" / "1c" / "rag" / "official-docs" / "start-links.json"
DEFAULT_ROOT_URL = "https://its.1c.ru/"
class AnchorParser(HTMLParser):
def __init__(self) -> None:
super().__init__(convert_charrefs=True)
self.title_parts: list[str] = []
self.links: list[dict[str, str]] = []
self._in_title = False
self._current: dict[str, Any] | None = None
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
tag = tag.lower()
if tag == "title":
self._in_title = True
if tag != "a":
return
href = ""
for name, value in attrs:
if name.lower() == "href" and value:
href = value
break
if href:
self._current = {"href": href, "text": []}
def handle_endtag(self, tag: str) -> None:
tag = tag.lower()
if tag == "title":
self._in_title = False
if tag == "a" and self._current:
self.links.append({"href": str(self._current["href"]), "text": " ".join(self._current["text"]).strip()})
self._current = None
def handle_data(self, data: str) -> None:
text = data.strip()
if not text:
return
if self._in_title:
self.title_parts.append(text)
if self._current is not None:
self._current["text"].append(text)
@property
def title(self) -> str:
return " ".join(self.title_parts).strip()
def read_cookie(cookie_file: Path | None) -> str:
if cookie_file:
return cookie_file.read_text(encoding="utf-8").strip()
return os.environ.get("ONEC_ITS_COOKIE", "").strip()
def charset_from_content_type(content_type: str) -> str:
match = re.search(r"charset=([^;\s]+)", content_type, flags=re.IGNORECASE)
return match.group(1).strip("\"'") if match else "utf-8"
def fetch_html(url: str, *, cookie: str, timeout: int, user_agent: str) -> tuple[str, str, int]:
headers = {"User-Agent": user_agent}
if cookie:
headers["Cookie"] = cookie
request = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(request, timeout=timeout) as response:
body = response.read()
encoding = charset_from_content_type(response.headers.get("Content-Type", ""))
return body.decode(encoding, errors="replace"), response.geturl(), int(response.status)
def normalize_url(base_url: str, href: str) -> str | None:
if href.startswith(("mailto:", "tel:", "javascript:")):
return None
absolute = urllib.parse.urljoin(base_url, href)
parsed = urllib.parse.urlparse(absolute)
if parsed.scheme not in {"http", "https"} or not parsed.netloc.endswith("its.1c.ru"):
return None
return urllib.parse.urlunparse(parsed._replace(fragment=""))
def parse_page(url: str, *, cookie: str, timeout: int, user_agent: str) -> dict[str, Any]:
html, final_url, status = fetch_html(url, cookie=cookie, timeout=timeout, user_agent=user_agent)
parser = AnchorParser()
parser.feed(html)
links = []
seen = set()
for item in parser.links:
next_url = normalize_url(final_url, item["href"])
if not next_url or next_url in seen:
continue
seen.add(next_url)
links.append({"url": next_url, "text": item["text"]})
return {"url": url, "final_url": final_url, "status": status, "title": parser.title, "bytes": len(html.encode("utf-8")), "links": links}
def classify_link(url: str, text: str) -> str | None:
path = urllib.parse.urlparse(url).path
lowered = text.casefold()
if path == "/section/dev":
return "dev_section"
if path.startswith("/section/dev/"):
if any(marker in path for marker in ("/doc_dev", "/method_dev", "/doc_edt", "/doc_bsp", "/doc_fresh")):
return "dev_section_index"
return None
if re.fullmatch(r"/db/v8\d*doc", path) or re.fullmatch(r"/db/v83\d+doc", path) or re.fullmatch(r"/db/v85\d+doc", path):
return "platform_doc"
if path.startswith("/db/v8devgloss"):
return "developer_glossary"
if path.startswith("/db/metod8dev"):
return "methodical_support"
if path.startswith("/db/v8std"):
return "development_standards"
if path.startswith("/db/fresh") or path.startswith("/db/sdadmin"):
return "platform_related_doc"
if path.startswith("/db/bsp") or path.startswith("/db/bid") or path.startswith("/db/bia"):
return "library_doc"
if path.startswith("/db/pub") and any(word in lowered for word in ("разработ", "1с:предприятие", "мобильн", "расширен", "интеграц", "отчет")):
return "developer_book"
if path.startswith("/db/intgr83") or path.startswith("/db/coldev"):
return "developer_book"
return None
def discover(root_url: str, *, cookie: str, timeout: int, user_agent: str) -> dict[str, Any]:
root = parse_page(root_url, cookie=cookie, timeout=timeout, user_agent=user_agent)
dev_url = next((item["url"] for item in root["links"] if item["url"].rstrip("/") == "https://its.1c.ru/section/dev"), "https://its.1c.ru/section/dev")
dev = parse_page(dev_url, cookie=cookie, timeout=timeout, user_agent=user_agent)
candidates_by_url: dict[str, dict[str, str]] = {}
for page in (root, dev):
for item in page["links"]:
category = classify_link(item["url"], item["text"])
if not category:
continue
candidates_by_url[item["url"]] = {"url": item["url"], "text": item["text"], "category": category}
candidates = sorted(candidates_by_url.values(), key=lambda item: (item["category"], item["url"]))
categories = sorted({item["category"] for item in candidates})
return {
"schema": "onec_its_start_links.v1",
"created_at_unix": int(time.time()),
"root": {"url": root["url"], "title": root["title"], "link_count": len(root["links"])},
"dev_section": {"url": dev["url"], "title": dev["title"], "link_count": len(dev["links"])},
"counts": {
"candidates": len(candidates),
"by_category": {category: sum(1 for item in candidates if item["category"] == category) for category in categories},
},
"start_links": candidates,
}
def main() -> int:
parser = argparse.ArgumentParser(description="Discover useful 1C:ITS start links from https://its.1c.ru/ and /section/dev.")
parser.add_argument("--root-url", default=DEFAULT_ROOT_URL)
parser.add_argument("--cookie-file", type=Path)
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
parser.add_argument("--timeout", type=int, default=30)
parser.add_argument("--user-agent", default="Codex 1C ITS start-link discovery")
parser.add_argument("--print", action="store_true", dest="print_report")
args = parser.parse_args()
report = discover(args.root_url, cookie=read_cookie(args.cookie_file), timeout=args.timeout, user_agent=args.user_agent)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
payload = report if args.print_report else {"output": str(args.output), "counts": report["counts"]}
print(json.dumps(payload, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())