信捷PLCSkill
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

241 lines
8.2 KiB

  1. import argparse
  2. import csv
  3. import datetime as dt
  4. import html
  5. import json
  6. import pathlib
  7. import re
  8. import time
  9. import urllib.parse
  10. import urllib.request
  11. from collections import Counter
  12. BASE = "https://www.xinje.com"
  13. DOWNLOAD_ENDPOINT = "/web/downloadCenter/file"
  14. DOCUMENT_CATEGORIES = {
  15. 16: "产品手册",
  16. 19: "彩页/样本",
  17. 31: "产品图纸",
  18. 33: "EPLAN",
  19. 49: "认证证书",
  20. }
  21. PLC_HMI_KEYWORDS = [
  22. "PLC",
  23. "可编程",
  24. "控制器",
  25. "XDP",
  26. "XDPPro",
  27. "TouchWin",
  28. "人机界面",
  29. "HMI",
  30. "XD",
  31. "XL",
  32. "XG",
  33. "XC",
  34. "XS",
  35. "TS",
  36. "TG",
  37. ]
  38. def fetch(params: dict[str, str | int]) -> str:
  39. query = urllib.parse.urlencode(params)
  40. url = f"{BASE}{DOWNLOAD_ENDPOINT}?{query}"
  41. request = urllib.request.Request(
  42. url,
  43. headers={
  44. "User-Agent": "Mozilla/5.0 (Codex Xinje catalog)",
  45. "X-Requested-With": "XMLHttpRequest",
  46. },
  47. )
  48. with urllib.request.urlopen(request, timeout=40) as response:
  49. return response.read().decode("utf-8", "ignore")
  50. def clean_text(value: str) -> str:
  51. value = re.sub(r"<[^>]+>", "", value)
  52. return html.unescape(re.sub(r"\s+", " ", value).strip())
  53. def text_between(pattern: str, block: str) -> str:
  54. match = re.search(pattern, block, flags=re.I | re.S)
  55. return clean_text(match.group(1)) if match else ""
  56. def parse_results(markup: str) -> list[dict[str, str]]:
  57. blocks = re.findall(r"(<li>\s*<div class=\"con\">.*?</li>)", markup, flags=re.I | re.S)
  58. results = []
  59. for block in blocks:
  60. href_match = re.search(r'href=["\']([^"\']+)["\']', block, flags=re.I)
  61. href = html.unescape(href_match.group(1)) if href_match else ""
  62. title = text_between(r"<h3[^>]*>(.*?)</h3>", block)
  63. download_name = text_between(r'download=["\']([^"\']+)["\']', block)
  64. results.append(
  65. {
  66. "id": text_between(r'id=["\']file-(\d+)["\']', block),
  67. "title": title or download_name,
  68. "download_name": download_name,
  69. "url": href,
  70. "size": text_between(r'class=["\']filesize["\'][^>]*>.*?<em>(.*?)</em>', block),
  71. "version": text_between(r'class=["\']filever["\'][^>]*>.*?<em>(.*?)</em>', block),
  72. "file_type": text_between(r'class=["\']filetype["\'][^>]*>.*?<em>(.*?)</em>', block),
  73. "update_date": text_between(r'class=["\']filedate["\'][^>]*>.*?<em>(.*?)</em>', block),
  74. }
  75. )
  76. return results
  77. def parse_max_page(markup: str, one_id: int) -> int:
  78. pages = [1]
  79. pattern = rf"changePage\((\d+),\s*0,\s*{one_id},\s*0\)"
  80. pages.extend(int(value) for value in re.findall(pattern, markup))
  81. jump_match = re.search(r'id=["\']pageJump["\'][^>]+max=["\'](\d+)["\']', markup, flags=re.I)
  82. if jump_match:
  83. pages.append(int(jump_match.group(1)))
  84. return max(pages)
  85. def collect_category(one_id: int, delay: float) -> list[dict[str, str]]:
  86. first_markup = fetch({"seriesId": 0, "oneId": one_id, "twoId": 0, "page": 1, "fileName": ""})
  87. max_page = parse_max_page(first_markup, one_id)
  88. items = []
  89. for page in range(1, max_page + 1):
  90. markup = first_markup if page == 1 else fetch(
  91. {"seriesId": 0, "oneId": one_id, "twoId": 0, "page": page, "fileName": ""}
  92. )
  93. for item in parse_results(markup):
  94. item["category_id"] = str(one_id)
  95. item["category"] = DOCUMENT_CATEGORIES.get(one_id, str(one_id))
  96. item["source"] = f"{BASE}/web/downloadCenter/index"
  97. items.append(item)
  98. if delay:
  99. time.sleep(delay)
  100. return items
  101. def write_csv(path: pathlib.Path, rows: list[dict[str, str]]) -> None:
  102. fieldnames = [
  103. "id",
  104. "category_id",
  105. "category",
  106. "title",
  107. "download_name",
  108. "file_type",
  109. "size",
  110. "version",
  111. "update_date",
  112. "url",
  113. "source",
  114. ]
  115. with path.open("w", encoding="utf-8-sig", newline="") as handle:
  116. writer = csv.DictWriter(handle, fieldnames=fieldnames)
  117. writer.writeheader()
  118. for row in rows:
  119. writer.writerow({field: row.get(field, "") for field in fieldnames})
  120. def write_json(path: pathlib.Path, rows: list[dict[str, str]]) -> None:
  121. payload = {
  122. "cataloged_at": dt.datetime.now(dt.timezone.utc).isoformat(),
  123. "source": f"{BASE}/web/downloadCenter/index",
  124. "scope": "Xinje download-center document-like categories; metadata only, no file downloads",
  125. "categories": DOCUMENT_CATEGORIES,
  126. "total_items": len(rows),
  127. "items": rows,
  128. }
  129. path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
  130. def write_markdown(path: pathlib.Path, rows: list[dict[str, str]]) -> None:
  131. by_category = Counter(row["category"] for row in rows)
  132. by_type = Counter((row.get("file_type") or "unknown").lower() for row in rows)
  133. plc_hmi = [
  134. row for row in rows
  135. if any(keyword.lower() in (row.get("title", "") + row.get("download_name", "")).lower()
  136. for keyword in PLC_HMI_KEYWORDS)
  137. ]
  138. recent = sorted(rows, key=lambda row: row.get("update_date", ""), reverse=True)[:20]
  139. lines = [
  140. "# Xinje Official Document Catalog",
  141. "",
  142. f"Cataloged at: {dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
  143. "",
  144. "Scope: metadata from Xinje download-center document-like categories. Files are not downloaded by this script.",
  145. "",
  146. "## Counts By Category",
  147. "",
  148. "| Category | Count |",
  149. "|---|---:|",
  150. ]
  151. for category, count in by_category.most_common():
  152. lines.append(f"| {category} | {count} |")
  153. lines.extend(["", "## Counts By File Type", "", "| File type | Count |", "|---|---:|"])
  154. for file_type, count in by_type.most_common():
  155. lines.append(f"| {file_type} | {count} |")
  156. lines.extend([
  157. "",
  158. "## Recent Items",
  159. "",
  160. "| ID | Category | Title | Type | Version | Update date |",
  161. "|---|---|---|---|---|---|",
  162. ])
  163. for row in recent:
  164. lines.append(
  165. f"| {row.get('id','')} | {row.get('category','')} | {row.get('title','')} | "
  166. f"{row.get('file_type','')} | {row.get('version','')} | {row.get('update_date','')} |"
  167. )
  168. lines.extend([
  169. "",
  170. "## PLC/HMI Candidates",
  171. "",
  172. "Use the full CSV/JSON catalog for exact URLs. This section highlights likely PLC/HMI-related entries.",
  173. "",
  174. "| ID | Category | Title | Type | Version | Update date |",
  175. "|---|---|---|---|---|---|",
  176. ])
  177. for row in plc_hmi[:120]:
  178. lines.append(
  179. f"| {row.get('id','')} | {row.get('category','')} | {row.get('title','')} | "
  180. f"{row.get('file_type','')} | {row.get('version','')} | {row.get('update_date','')} |"
  181. )
  182. path.write_text("\n".join(lines) + "\n", encoding="utf-8")
  183. def main() -> None:
  184. parser = argparse.ArgumentParser(description="Catalog Xinje download-center document metadata.")
  185. parser.add_argument("--output-dir", default="assets/catalogs", help="Directory for JSON/CSV/MD outputs.")
  186. parser.add_argument("--delay", type=float, default=0.15, help="Delay between page requests.")
  187. parser.add_argument("--categories", default=",".join(str(key) for key in DOCUMENT_CATEGORIES))
  188. args = parser.parse_args()
  189. rows = []
  190. seen = set()
  191. for one_id in [int(value) for value in args.categories.split(",") if value.strip()]:
  192. for item in collect_category(one_id, args.delay):
  193. key = item.get("id") or item.get("url")
  194. if key in seen:
  195. continue
  196. seen.add(key)
  197. rows.append(item)
  198. rows.sort(key=lambda row: (row.get("category_id", ""), row.get("update_date", ""), row.get("id", "")), reverse=True)
  199. output_dir = pathlib.Path(args.output_dir)
  200. output_dir.mkdir(parents=True, exist_ok=True)
  201. write_json(output_dir / "xinje-official-document-catalog.json", rows)
  202. write_csv(output_dir / "xinje-official-document-catalog.csv", rows)
  203. write_markdown(output_dir / "xinje-official-document-catalog-summary.md", rows)
  204. print(f"cataloged {len(rows)} items into {output_dir}")
  205. if __name__ == "__main__":
  206. main()