信捷PLCSkill
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

174 řádky
6.8 KiB

  1. import argparse
  2. import datetime as dt
  3. import json
  4. import pathlib
  5. import re
  6. import sqlite3
  7. import sys
  8. from pypdf import PdfReader
  9. TOC_MARKERS = ("目录", "目 录", "contents", "table of contents")
  10. TOC_LINE = re.compile(r"^(?P<title>.{2,160}?)(?:[..·…]{2,}|\s{2,})(?P<page>-?\d{1,4})\s*$")
  11. def compact(value: str) -> str:
  12. return re.sub(r"\s+", " ", value or "").strip()
  13. def text_lines(value: str) -> list[str]:
  14. return [compact(line) for line in (value or "").splitlines() if compact(line)]
  15. def is_toc_page(text: str) -> bool:
  16. lowered = text.lower()
  17. if any(marker in lowered for marker in TOC_MARKERS):
  18. return True
  19. return sum(1 for line in text_lines(text) if TOC_LINE.match(line)) >= 4
  20. def parse_toc_entries(text: str, pdf_page: int) -> list[dict[str, object]]:
  21. entries = []
  22. for line in text_lines(text):
  23. match = TOC_LINE.match(line)
  24. if not match:
  25. continue
  26. title = compact(match.group("title")).strip("..·… ")
  27. if len(title) < 2 or title in TOC_MARKERS:
  28. continue
  29. entries.append(
  30. {
  31. "title": title,
  32. "printed_page": int(match.group("page")),
  33. "toc_pdf_page": pdf_page,
  34. }
  35. )
  36. return entries
  37. def open_index(path: pathlib.Path) -> sqlite3.Connection:
  38. path.parent.mkdir(parents=True, exist_ok=True)
  39. if path.exists():
  40. path.unlink()
  41. connection = sqlite3.connect(path)
  42. connection.execute("PRAGMA journal_mode=OFF")
  43. connection.execute("PRAGMA synchronous=OFF")
  44. connection.execute("PRAGMA temp_store=MEMORY")
  45. connection.execute(
  46. "CREATE TABLE manuals ("
  47. "filename TEXT PRIMARY KEY, page_count INTEGER NOT NULL, size_bytes INTEGER NOT NULL, "
  48. "toc_pdf_pages TEXT NOT NULL)"
  49. )
  50. connection.execute(
  51. "CREATE VIRTUAL TABLE page_search USING fts5("
  52. "manual UNINDEXED, filename UNINDEXED, pdf_page UNINDEXED, text, tokenize='trigram')"
  53. )
  54. return connection
  55. def build_index(manual_dir: pathlib.Path, database_path: pathlib.Path, toc_scan_pages: int) -> dict:
  56. connection = open_index(database_path)
  57. manuals = []
  58. page_total = 0
  59. try:
  60. for pdf_path in sorted(manual_dir.glob("*.pdf"), key=lambda item: item.name.casefold()):
  61. reader = PdfReader(str(pdf_path))
  62. toc_pages = []
  63. toc_entries = []
  64. print(f"indexing {pdf_path.name}: {len(reader.pages)} pages", flush=True)
  65. for page_number, page in enumerate(reader.pages, start=1):
  66. text = page.extract_text() or ""
  67. normalized = compact(text)
  68. connection.execute(
  69. "INSERT INTO page_search(manual, filename, pdf_page, text) VALUES (?, ?, ?, ?)",
  70. (pdf_path.stem, pdf_path.name, page_number, normalized),
  71. )
  72. if page_number <= toc_scan_pages and is_toc_page(text):
  73. toc_pages.append(page_number)
  74. toc_entries.extend(parse_toc_entries(text, page_number))
  75. toc_entries = toc_entries[:300]
  76. connection.execute(
  77. "INSERT INTO manuals(filename, page_count, size_bytes, toc_pdf_pages) VALUES (?, ?, ?, ?)",
  78. (pdf_path.name, len(reader.pages), pdf_path.stat().st_size, json.dumps(toc_pages)),
  79. )
  80. manuals.append(
  81. {
  82. "filename": pdf_path.name,
  83. "page_count": len(reader.pages),
  84. "size_bytes": pdf_path.stat().st_size,
  85. "toc_pdf_pages": toc_pages,
  86. "toc_entries": toc_entries,
  87. }
  88. )
  89. page_total += len(reader.pages)
  90. connection.commit()
  91. connection.execute("VACUUM")
  92. finally:
  93. connection.close()
  94. return {
  95. "generated_at": dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat(),
  96. "manual_count": len(manuals),
  97. "page_count": page_total,
  98. "manuals": manuals,
  99. }
  100. def write_toc_markdown(index: dict, path: pathlib.Path) -> None:
  101. lines = [
  102. "# Manual Chapter Index",
  103. "",
  104. "This generated index covers all PDFs currently under `assets/manuals/`. `手册页` is the printed page number extracted from the table of contents; `目录 PDF 页` is the physical PDF page where the entry was found. Use `search_xinje_manuals.py` for exact PDF-page results.",
  105. "",
  106. f"Generated: `{index['generated_at']}`. Manuals: `{index['manual_count']}`. PDF pages: `{index['page_count']}`.",
  107. "",
  108. ]
  109. for manual in index["manuals"]:
  110. lines.append(f"## {manual['filename']}")
  111. toc_pages = ", ".join(str(item) for item in manual["toc_pdf_pages"]) or "not detected"
  112. lines.append("")
  113. lines.append(f"PDF pages: `{manual['page_count']}`. Detected TOC PDF pages: `{toc_pages}`.")
  114. entries = manual["toc_entries"]
  115. if not entries:
  116. lines.extend(["", "No reliable TOC lines were extracted. Use the full-text index or inspect the first 40 PDF pages.", ""])
  117. continue
  118. lines.extend(["", "| 手册页 | 章节/条目 | 目录 PDF 页 |", "|---:|---|---:|"])
  119. seen = set()
  120. for entry in entries:
  121. key = (entry["title"], entry["printed_page"])
  122. if key in seen:
  123. continue
  124. seen.add(key)
  125. title = str(entry["title"]).replace("|", "\\|")
  126. lines.append(f"| {entry['printed_page']} | {title} | {entry['toc_pdf_page']} |")
  127. lines.append("")
  128. path.parent.mkdir(parents=True, exist_ok=True)
  129. path.write_text("\n".join(lines), encoding="utf-8")
  130. def main() -> None:
  131. parser = argparse.ArgumentParser(description="Build a page-level full-text and table-of-contents index for Xinje manuals.")
  132. parser.add_argument("--manual-dir", required=True, type=pathlib.Path)
  133. parser.add_argument("--output-db", required=True, type=pathlib.Path)
  134. parser.add_argument("--output-toc-json", required=True, type=pathlib.Path)
  135. parser.add_argument("--output-toc-md", required=True, type=pathlib.Path)
  136. parser.add_argument("--toc-scan-pages", type=int, default=40)
  137. args = parser.parse_args()
  138. if args.toc_scan_pages < 1:
  139. raise SystemExit("--toc-scan-pages must be positive")
  140. index = build_index(args.manual_dir, args.output_db, args.toc_scan_pages)
  141. args.output_toc_json.parent.mkdir(parents=True, exist_ok=True)
  142. args.output_toc_json.write_text(json.dumps(index, ensure_ascii=False, indent=2), encoding="utf-8")
  143. write_toc_markdown(index, args.output_toc_md)
  144. print(
  145. f"indexed {index['manual_count']} manuals and {index['page_count']} pages; "
  146. f"database: {args.output_db}",
  147. flush=True,
  148. )
  149. if __name__ == "__main__":
  150. if hasattr(sys.stdout, "reconfigure"):
  151. sys.stdout.reconfigure(encoding="utf-8")
  152. main()