import argparse import csv import datetime as dt import html import json import pathlib import re import time import urllib.parse import urllib.request from collections import Counter BASE = "https://www.xinje.com" DOWNLOAD_ENDPOINT = "/web/downloadCenter/file" DOCUMENT_CATEGORIES = { 16: "产品手册", 19: "彩页/样本", 31: "产品图纸", 33: "EPLAN", 49: "认证证书", } PLC_HMI_KEYWORDS = [ "PLC", "可编程", "控制器", "XDP", "XDPPro", "TouchWin", "人机界面", "HMI", "XD", "XL", "XG", "XC", "XS", "TS", "TG", ] def fetch(params: dict[str, str | int]) -> str: query = urllib.parse.urlencode(params) url = f"{BASE}{DOWNLOAD_ENDPOINT}?{query}" request = urllib.request.Request( url, headers={ "User-Agent": "Mozilla/5.0 (Codex Xinje catalog)", "X-Requested-With": "XMLHttpRequest", }, ) with urllib.request.urlopen(request, timeout=40) as response: return response.read().decode("utf-8", "ignore") def clean_text(value: str) -> str: value = re.sub(r"<[^>]+>", "", value) return html.unescape(re.sub(r"\s+", " ", value).strip()) def text_between(pattern: str, block: str) -> str: match = re.search(pattern, block, flags=re.I | re.S) return clean_text(match.group(1)) if match else "" def parse_results(markup: str) -> list[dict[str, str]]: blocks = re.findall(r"(
  • \s*
    .*?
  • )", markup, flags=re.I | re.S) results = [] for block in blocks: href_match = re.search(r'href=["\']([^"\']+)["\']', block, flags=re.I) href = html.unescape(href_match.group(1)) if href_match else "" title = text_between(r"]*>(.*?)", block) download_name = text_between(r'download=["\']([^"\']+)["\']', block) results.append( { "id": text_between(r'id=["\']file-(\d+)["\']', block), "title": title or download_name, "download_name": download_name, "url": href, "size": text_between(r'class=["\']filesize["\'][^>]*>.*?(.*?)', block), "version": text_between(r'class=["\']filever["\'][^>]*>.*?(.*?)', block), "file_type": text_between(r'class=["\']filetype["\'][^>]*>.*?(.*?)', block), "update_date": text_between(r'class=["\']filedate["\'][^>]*>.*?(.*?)', block), } ) return results def parse_max_page(markup: str, one_id: int) -> int: pages = [1] pattern = rf"changePage\((\d+),\s*0,\s*{one_id},\s*0\)" pages.extend(int(value) for value in re.findall(pattern, markup)) jump_match = re.search(r'id=["\']pageJump["\'][^>]+max=["\'](\d+)["\']', markup, flags=re.I) if jump_match: pages.append(int(jump_match.group(1))) return max(pages) def collect_category(one_id: int, delay: float) -> list[dict[str, str]]: first_markup = fetch({"seriesId": 0, "oneId": one_id, "twoId": 0, "page": 1, "fileName": ""}) max_page = parse_max_page(first_markup, one_id) items = [] for page in range(1, max_page + 1): markup = first_markup if page == 1 else fetch( {"seriesId": 0, "oneId": one_id, "twoId": 0, "page": page, "fileName": ""} ) for item in parse_results(markup): item["category_id"] = str(one_id) item["category"] = DOCUMENT_CATEGORIES.get(one_id, str(one_id)) item["source"] = f"{BASE}/web/downloadCenter/index" items.append(item) if delay: time.sleep(delay) return items def write_csv(path: pathlib.Path, rows: list[dict[str, str]]) -> None: fieldnames = [ "id", "category_id", "category", "title", "download_name", "file_type", "size", "version", "update_date", "url", "source", ] with path.open("w", encoding="utf-8-sig", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=fieldnames) writer.writeheader() for row in rows: writer.writerow({field: row.get(field, "") for field in fieldnames}) def write_json(path: pathlib.Path, rows: list[dict[str, str]]) -> None: payload = { "cataloged_at": dt.datetime.now(dt.timezone.utc).isoformat(), "source": f"{BASE}/web/downloadCenter/index", "scope": "Xinje download-center document-like categories; metadata only, no file downloads", "categories": DOCUMENT_CATEGORIES, "total_items": len(rows), "items": rows, } path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") def write_markdown(path: pathlib.Path, rows: list[dict[str, str]]) -> None: by_category = Counter(row["category"] for row in rows) by_type = Counter((row.get("file_type") or "unknown").lower() for row in rows) plc_hmi = [ row for row in rows if any(keyword.lower() in (row.get("title", "") + row.get("download_name", "")).lower() for keyword in PLC_HMI_KEYWORDS) ] recent = sorted(rows, key=lambda row: row.get("update_date", ""), reverse=True)[:20] lines = [ "# Xinje Official Document Catalog", "", f"Cataloged at: {dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", "", "Scope: metadata from Xinje download-center document-like categories. Files are not downloaded by this script.", "", "## Counts By Category", "", "| Category | Count |", "|---|---:|", ] for category, count in by_category.most_common(): lines.append(f"| {category} | {count} |") lines.extend(["", "## Counts By File Type", "", "| File type | Count |", "|---|---:|"]) for file_type, count in by_type.most_common(): lines.append(f"| {file_type} | {count} |") lines.extend([ "", "## Recent Items", "", "| ID | Category | Title | Type | Version | Update date |", "|---|---|---|---|---|---|", ]) for row in recent: lines.append( f"| {row.get('id','')} | {row.get('category','')} | {row.get('title','')} | " f"{row.get('file_type','')} | {row.get('version','')} | {row.get('update_date','')} |" ) lines.extend([ "", "## PLC/HMI Candidates", "", "Use the full CSV/JSON catalog for exact URLs. This section highlights likely PLC/HMI-related entries.", "", "| ID | Category | Title | Type | Version | Update date |", "|---|---|---|---|---|---|", ]) for row in plc_hmi[:120]: lines.append( f"| {row.get('id','')} | {row.get('category','')} | {row.get('title','')} | " f"{row.get('file_type','')} | {row.get('version','')} | {row.get('update_date','')} |" ) path.write_text("\n".join(lines) + "\n", encoding="utf-8") def main() -> None: parser = argparse.ArgumentParser(description="Catalog Xinje download-center document metadata.") parser.add_argument("--output-dir", default="assets/catalogs", help="Directory for JSON/CSV/MD outputs.") parser.add_argument("--delay", type=float, default=0.15, help="Delay between page requests.") parser.add_argument("--categories", default=",".join(str(key) for key in DOCUMENT_CATEGORIES)) args = parser.parse_args() rows = [] seen = set() for one_id in [int(value) for value in args.categories.split(",") if value.strip()]: for item in collect_category(one_id, args.delay): key = item.get("id") or item.get("url") if key in seen: continue seen.add(key) rows.append(item) rows.sort(key=lambda row: (row.get("category_id", ""), row.get("update_date", ""), row.get("id", "")), reverse=True) output_dir = pathlib.Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) write_json(output_dir / "xinje-official-document-catalog.json", rows) write_csv(output_dir / "xinje-official-document-catalog.csv", rows) write_markdown(output_dir / "xinje-official-document-catalog-summary.md", rows) print(f"cataloged {len(rows)} items into {output_dir}") if __name__ == "__main__": main()