document_content_service.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. """不依赖AI或Office进程的PDF/DOCX正文提取与检索文本构建。"""
  2. from __future__ import annotations
  3. import logging
  4. from dataclasses import dataclass
  5. from datetime import datetime, timezone
  6. from pathlib import Path
  7. import fitz
  8. from docx import Document as WordDocument
  9. from dms.common.enums import ContentExtractStatus
  10. logger = logging.getLogger(__name__)
  11. @dataclass(frozen=True, slots=True)
  12. class ContentExtraction:
  13. text: str | None
  14. status: str
  15. extracted_at: datetime | None
  16. def build_search_text(
  17. name: str,
  18. summary: str | None,
  19. tags: list[str] | None,
  20. content_text: str | None,
  21. ) -> str:
  22. return " ".join(
  23. part for part in [name, summary or "", *(tags or []), content_text or ""]
  24. if part
  25. ).strip()
  26. def _word_text(path: Path) -> str:
  27. document = WordDocument(path)
  28. values: list[str] = []
  29. def add_paragraphs(paragraphs) -> None:
  30. values.extend(
  31. paragraph.text.strip()
  32. for paragraph in paragraphs
  33. if paragraph.text.strip()
  34. )
  35. def add_tables(tables) -> None:
  36. for table in tables:
  37. for row in table.rows:
  38. for cell in row.cells:
  39. add_paragraphs(cell.paragraphs)
  40. add_tables(cell.tables)
  41. add_paragraphs(document.paragraphs)
  42. add_tables(document.tables)
  43. for section in document.sections:
  44. add_paragraphs(section.header.paragraphs)
  45. add_tables(section.header.tables)
  46. add_paragraphs(section.footer.paragraphs)
  47. add_tables(section.footer.tables)
  48. return "\n".join(values).strip()
  49. def _pdf_text(path: Path) -> str:
  50. with fitz.open(stream=path.read_bytes(), filetype="pdf") as document:
  51. return "\n".join(page.get_text("text") for page in document).strip()
  52. def extract_document_content(path: Path, extension: str) -> ContentExtraction:
  53. normalized = extension.lower().lstrip(".")
  54. if normalized not in {"pdf", "docx"}:
  55. return ContentExtraction(
  56. text=None,
  57. status=ContentExtractStatus.UNSUPPORTED.value,
  58. extracted_at=None,
  59. )
  60. extracted_at = datetime.now(timezone.utc).replace(tzinfo=None)
  61. try:
  62. text = _pdf_text(path) if normalized == "pdf" else _word_text(path)
  63. except Exception:
  64. logger.exception("文档正文提取失败:extension=%s", normalized)
  65. return ContentExtraction(
  66. text=None,
  67. status=ContentExtractStatus.FAILED.value,
  68. extracted_at=extracted_at,
  69. )
  70. return ContentExtraction(
  71. text=text or None,
  72. status=(
  73. ContentExtractStatus.SUCCESS.value
  74. if text
  75. else ContentExtractStatus.EMPTY.value
  76. ),
  77. extracted_at=extracted_at,
  78. )
  79. __all__ = ["ContentExtraction", "build_search_text", "extract_document_content"]