docx_writer.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. """
  2. docx_writer.py - DOCX 文档生成模块
  3. 负责:
  4. - 使用 python-docx 创建 Word 文档
  5. - 根据 TableInfo 在文档中重建表格
  6. - 设置合并单元格(跨行/跨列)
  7. - 设置单元格对齐、边框等基本格式
  8. """
  9. import logging
  10. from typing import Optional
  11. from docx import Document
  12. from docx.enum.text import WD_ALIGN_PARAGRAPH
  13. from docx.oxml.ns import qn, nsdecls
  14. from docx.oxml import parse_xml
  15. from docx.shared import Emu, Pt, Cm
  16. from docx.table import _Cell
  17. from .table_parser import TableInfo
  18. logger = logging.getLogger(__name__)
  19. def _get_alignment(text: str) -> int:
  20. """从文本内容中猜测对齐方式。"""
  21. if not text or not text.strip():
  22. return WD_ALIGN_PARAGRAPH.LEFT
  23. stripped = text.strip()
  24. if stripped.replace(",", "").replace(".", "").replace("-", "").isdigit():
  25. return WD_ALIGN_PARAGRAPH.RIGHT
  26. if len(stripped) <= 5:
  27. return WD_ALIGN_PARAGRAPH.CENTER
  28. return WD_ALIGN_PARAGRAPH.LEFT
  29. def _convert_cm_to_emu(cm_value: float) -> int:
  30. """将厘米转换为 EMU(docx 内部单位)。"""
  31. return int(round(cm_value * 360000))
  32. def _merge_cells(table, r: int, c: int, row_span: int, col_span: int):
  33. """合并表格中的单元格。"""
  34. if row_span <= 1 and col_span <= 1:
  35. return
  36. end_r = r + row_span - 1
  37. end_c = c + col_span - 1
  38. start_cell = table.cell(r, c)
  39. end_cell = table.cell(end_r, end_c)
  40. start_cell.merge(end_cell)
  41. def write_table_to_doc(doc, table_info: TableInfo, body_width_cm: float):
  42. """将单个 TableInfo 写入到已打开的 doc 对象中。
  43. Args:
  44. doc: Document 对象
  45. table_info: 要写入的表格
  46. body_width_cm: 正文宽度(厘米),用于计算列宽
  47. """
  48. if table_info.rows == 0 or table_info.cols == 0:
  49. return
  50. doc_table = doc.add_table(
  51. rows=table_info.rows,
  52. cols=table_info.cols,
  53. )
  54. doc_table.autofit = False
  55. doc_table.allow_autofit = False
  56. # ---- 设置列宽 ----
  57. usable_width = body_width_cm * 0.95
  58. if table_info.col_widths:
  59. for c_idx, ratio in enumerate(table_info.col_widths):
  60. width_emu = _convert_cm_to_emu(usable_width * ratio)
  61. for row in doc_table.rows:
  62. row.cells[c_idx].width = Emu(width_emu)
  63. # ---- 填充内容、合并单元格 ----
  64. for r in range(table_info.rows):
  65. for c in range(table_info.cols):
  66. cell_info = table_info.cells[r][c]
  67. if cell_info.row_span == 0 or cell_info.col_span == 0:
  68. continue
  69. _merge_cells(doc_table, r, c, cell_info.row_span, cell_info.col_span)
  70. cell = doc_table.cell(r, c)
  71. text = cell_info.text or ""
  72. cell.text = ""
  73. lines = text.split("\n")
  74. for li, line in enumerate(lines):
  75. if li > 0:
  76. cell.add_paragraph()
  77. run = cell.paragraphs[li].add_run(line.strip())
  78. run.font.size = Pt(10)
  79. run.font.name = "宋体"
  80. run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
  81. alignment = _get_alignment(text)
  82. for para in cell.paragraphs:
  83. para.alignment = alignment
  84. cell.vertical_alignment = 1 # CENTER
  85. # ---- 设置表格边框 ----
  86. tbl = doc_table._tbl
  87. tblPr = (
  88. tbl.tblPr
  89. if tbl.tblPr is not None
  90. else parse_xml(f'<w:tblPr {nsdecls("w")}/>')
  91. )
  92. borders = parse_xml(
  93. f'<w:tblBorders {nsdecls("w")}>'
  94. ' <w:top w:val="single" w:sz="4" w:space="0" w:color="000000"/>'
  95. ' <w:left w:val="single" w:sz="4" w:space="0" w:color="000000"/>'
  96. ' <w:bottom w:val="single" w:sz="4" w:space="0" w:color="000000"/>'
  97. ' <w:right w:val="single" w:sz="4" w:space="0" w:color="000000"/>'
  98. ' <w:insideH w:val="single" w:sz="4" w:space="0" w:color="000000"/>'
  99. ' <w:insideV w:val="single" w:sz="4" w:space="0" w:color="000000"/>'
  100. '</w:tblBorders>'
  101. )
  102. tblPr.append(borders)
  103. logger.info(
  104. f"写入表格: {table_info.rows} 行 x {table_info.cols} 列(含合并单元格)"
  105. )
  106. def build_document(
  107. tables: list,
  108. output_path: str,
  109. page_width_cm: float = 21.0,
  110. page_height_cm: float = 29.7,
  111. margin_cm: float = 2.0,
  112. ) -> str:
  113. """将所有表格依次写入一个 DOCX 文件。
  114. Args:
  115. tables: list[TableInfo]
  116. output_path: 输出路径
  117. page_width_cm: 页面宽度
  118. page_height_cm: 页面高度
  119. margin_cm: 页边距
  120. Returns:
  121. str: 输出文件路径
  122. """
  123. doc = Document()
  124. section = doc.sections[0]
  125. section.page_width = Cm(page_width_cm)
  126. section.page_height = Cm(page_height_cm)
  127. section.top_margin = Cm(margin_cm)
  128. section.bottom_margin = Cm(margin_cm)
  129. section.left_margin = Cm(margin_cm)
  130. section.right_margin = Cm(margin_cm)
  131. body_width = page_width_cm - 2 * margin_cm
  132. for t_idx, table_info in enumerate(tables):
  133. if table_info.rows == 0 or table_info.cols == 0:
  134. continue
  135. if t_idx > 0:
  136. doc.add_paragraph()
  137. write_table_to_doc(doc, table_info, body_width)
  138. doc.save(output_path)
  139. logger.info(f"文档已保存至: {output_path}")
  140. return output_path
  141. def build_single_table_document(
  142. table_info: TableInfo,
  143. output_path: str,
  144. page_width_cm: float = 21.0,
  145. page_height_cm: float = 29.7,
  146. margin_cm: float = 2.0,
  147. table_label: str = "",
  148. ) -> str:
  149. """将单个表格写入独立的 DOCX 文件。
  150. Args:
  151. table_info: 要写入的表格
  152. output_path: 输出路径
  153. page_width_cm: 页面宽度
  154. page_height_cm: 页面高度
  155. margin_cm: 页边距
  156. table_label: 可选标签,写入文档标题
  157. Returns:
  158. str: 输出文件路径
  159. """
  160. doc = Document()
  161. section = doc.sections[0]
  162. section.page_width = Cm(page_width_cm)
  163. section.page_height = Cm(page_height_cm)
  164. section.top_margin = Cm(margin_cm)
  165. section.bottom_margin = Cm(margin_cm)
  166. section.left_margin = Cm(margin_cm)
  167. section.right_margin = Cm(margin_cm)
  168. body_width = page_width_cm - 2 * margin_cm
  169. if table_label:
  170. p = doc.add_paragraph()
  171. run = p.add_run(table_label)
  172. run.font.size = Pt(12)
  173. run.font.bold = True
  174. write_table_to_doc(doc, table_info, body_width)
  175. doc.save(output_path)
  176. logger.info(f"单表文档已保存至: {output_path}")
  177. return output_path