bid_reader.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. """
  2. bid_reader.py - 招标文件读取模块
  3. 负责读取招标 PDF,提取文本和表格内容,返回结构化的 BidDocument 对象。
  4. """
  5. import logging
  6. import os
  7. from typing import Optional
  8. from bid_proposal.models import BidDocument, BidPage
  9. logger = logging.getLogger(__name__)
  10. class BidReadError(Exception):
  11. """招标文件读取失败异常"""
  12. pass
  13. def read_bid_pdf(pdf_path: str) -> BidDocument:
  14. """读取招标 PDF,提取文本和表格。
  15. 实现步骤:
  16. 1. 使用 pymupdf (import fitz) 打开 PDF,逐页提取文本
  17. 2. 使用 pdf_table_to_docx 提取表格
  18. 3. 组合成 BidDocument 返回
  19. Args:
  20. pdf_path: 招标文件 PDF 路径
  21. Returns:
  22. BidDocument: 结构化的招标文件内容
  23. Raises:
  24. FileNotFoundError: PDF 文件不存在
  25. BidReadError: PDF 读取/解析失败(如文件损坏、加密等)
  26. """
  27. if not os.path.exists(pdf_path):
  28. raise FileNotFoundError(f"招标文件不存在: {pdf_path}")
  29. logger.info("开始读取招标文件: %s", pdf_path)
  30. # 1. 使用 pymupdf 提取文本
  31. pages = _extract_text(pdf_path)
  32. # 2. 使用 pdf_table_to_docx 提取表格
  33. tables = _extract_tables(pdf_path)
  34. # 3. 构建 BidDocument
  35. doc = BidDocument(
  36. pdf_path=os.path.abspath(pdf_path),
  37. pages=pages,
  38. tables=tables,
  39. )
  40. logger.info("读取完成: %d 页, %d 个表格, %d 字符", doc.total_pages, doc.table_count, len(doc.full_text))
  41. return doc
  42. def _extract_text(pdf_path: str) -> list[BidPage]:
  43. """使用 pymupdf 从 PDF 中逐页提取文本。
  44. Args:
  45. pdf_path: PDF 文件路径
  46. Returns:
  47. list[BidPage]: 每页的文本内容列表
  48. Raises:
  49. BidReadError: PDF 打开或读取失败
  50. """
  51. try:
  52. import fitz # pymupdf
  53. except ImportError:
  54. raise ImportError("请安装 pymupdf: pip install pymupdf")
  55. try:
  56. doc = fitz.open(pdf_path)
  57. except Exception as e:
  58. raise BidReadError(f"无法打开 PDF 文件: {e}")
  59. pages: list[BidPage] = []
  60. try:
  61. for page_num in range(len(doc)):
  62. page = doc[page_num]
  63. text = page.get_text().strip()
  64. bid_page = BidPage(page_num=page_num + 1, text=text)
  65. pages.append(bid_page)
  66. logger.debug("第 %d 页: %d 字符", page_num + 1, len(text))
  67. finally:
  68. doc.close()
  69. if not pages:
  70. raise BidReadError(f"PDF 文件为空或无法提取任何文本: {pdf_path}")
  71. return pages
  72. def _extract_tables(pdf_path: str) -> list:
  73. """使用 pdf_table_to_docx 从 PDF 中提取表格。
  74. Args:
  75. pdf_path: PDF 文件路径
  76. Returns:
  77. list[TableInfo]: 表格列表
  78. Note:
  79. 如果表格提取失败,记录警告并返回空列表,不影响文本提取结果。
  80. """
  81. try:
  82. from pdf_table_to_docx.extractor import PDFTableExtractor
  83. extractor = PDFTableExtractor(pdf_path)
  84. extracted_tables = extractor.extract()
  85. if extracted_tables:
  86. tables = list(extracted_tables)
  87. logger.info("提取到 %d 个表格", len(tables))
  88. return tables
  89. else:
  90. logger.info("PDF 中未发现表格")
  91. return []
  92. except Exception as e:
  93. logger.warning("表格提取失败,仅使用文本内容继续: %s", e)
  94. return []