docx_processor.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. """
  2. Word文档处理模块
  3. 提供Word文档的处理和变量替换功能
  4. """
  5. import os
  6. import time
  7. from docx import Document
  8. from docx.oxml.ns import qn # 导入qn函数用于处理中文字体
  9. from .logger import logger
  10. def check_variables_in_document(docx_path, variables):
  11. """
  12. 检查文档中是否包含指定的变量,并输出详细信息用于调试
  13. 参数:
  14. docx_path: Word文档路径
  15. variables: 要检查的变量字典 {变量名: 替换值}
  16. """
  17. logger.debug(f"检查文档变量: {docx_path}")
  18. doc = Document(docx_path)
  19. # 收集文档中的所有文本
  20. all_text = []
  21. # 检查段落中的变量(简化输出)
  22. for i, paragraph in enumerate(doc.paragraphs):
  23. all_text.append(paragraph.text)
  24. if paragraph.text.strip(): # 只记录非空段落
  25. logger.debug(f"段落 {i}: {paragraph.text}")
  26. # 检查表格中的变量(简化输出)
  27. for t_idx, table in enumerate(doc.tables):
  28. for r_idx, row in enumerate(table.rows):
  29. for c_idx, cell in enumerate(row.cells):
  30. for p_idx, paragraph in enumerate(cell.paragraphs):
  31. all_text.append(paragraph.text)
  32. if paragraph.text.strip(): # 只记录非空段落
  33. logger.debug(f"表格 {t_idx}, 行 {r_idx}, 列 {c_idx}, 段落 {p_idx}: {paragraph.text}")
  34. # 检查是否找到所有变量
  35. for var_name in variables.keys():
  36. found = False
  37. for text in all_text:
  38. if var_name in text:
  39. found = True
  40. logger.info(f"变量 '{var_name}' 在文档中找到!")
  41. break
  42. if not found:
  43. logger.warning(f"变量 '{var_name}' 在文档中未找到! 将被替换为空字符串。")
  44. # 检查文档中可能存在但未提供的变量
  45. potential_vars = set()
  46. for text in all_text:
  47. # 查找形如 {xxx} 的模式
  48. start = 0
  49. while True:
  50. start = text.find('{', start)
  51. if start == -1:
  52. break
  53. end = text.find('}', start)
  54. if end == -1:
  55. break
  56. potential_var = text[start:end+1]
  57. potential_vars.add(potential_var)
  58. start = end + 1
  59. # 检查是否有未提供的变量
  60. for var in potential_vars:
  61. if var not in variables:
  62. logger.warning(f"文档中存在变量 '{var}',但未提供替换值!")
  63. def verify_replacement(docx_path, variables):
  64. """
  65. 验证变量是否已被成功替换
  66. 参数:
  67. docx_path: 处理后的Word文档路径
  68. variables: 要验证的变量字典 {变量名: 替换值}
  69. """
  70. logger.debug(f"验证变量替换: {docx_path}")
  71. doc = Document(docx_path)
  72. # 收集文档中的所有文本
  73. all_text = []
  74. # 检查段落
  75. for paragraph in doc.paragraphs:
  76. all_text.append(paragraph.text)
  77. # 检查表格
  78. for table in doc.tables:
  79. for row in table.rows:
  80. for cell in row.cells:
  81. for paragraph in cell.paragraphs:
  82. all_text.append(paragraph.text)
  83. # 检查是否所有变量都已被替换
  84. replacement_failed = False
  85. for var_name in variables.keys():
  86. for text in all_text:
  87. if var_name in text:
  88. logger.warning(f"变量 '{var_name}' 在处理后的文档中仍然存在! 替换可能失败。")
  89. replacement_failed = True
  90. break
  91. if not replacement_failed:
  92. logger.info("所有变量都已成功替换")
  93. def replace_text_in_paragraph(paragraph, variables):
  94. """
  95. 在段落中替换变量,同时保留文本格式
  96. 此方法通过以下步骤工作:
  97. 1. 收集段落中的所有runs(文本片段)
  98. 2. 清空段落
  99. 3. 处理每个run中的变量
  100. 4. 创建新的run,保留原始格式
  101. 5. 将处理后的文本添加回段落
  102. 参数:
  103. paragraph: 要处理的段落对象
  104. variables: 变量替换字典 {变量名: 替换值}
  105. """
  106. # 检查段落是否包含任何变量
  107. contains_variable = False
  108. found_variables = []
  109. # 记录原始段落文本
  110. original_text = paragraph.text
  111. for var_name in variables.keys():
  112. if var_name in original_text:
  113. contains_variable = True
  114. found_variables.append(var_name)
  115. if not contains_variable:
  116. return
  117. logger.info(f"在段落中找到变量: {found_variables}")
  118. logger.info(f"原始段落文本: {original_text}")
  119. # 存储原始的运行对象
  120. runs = [run for run in paragraph.runs]
  121. paragraph.clear()
  122. # 对每个运行进行处理
  123. for i, run in enumerate(runs):
  124. text = run.text
  125. original_run_text = text
  126. # 替换所有变量
  127. for var_name, var_value in variables.items():
  128. if var_name in text:
  129. logger.info(f"在run {i}中替换变量 '{var_name}' 为 '{var_value}'")
  130. logger.info(f"替换前文本: '{text}'")
  131. text = text.replace(var_name, var_value)
  132. logger.info(f"替换后文本: '{text}'")
  133. # 如果文本没有变化,记录一下
  134. if original_run_text == text:
  135. logger.debug(f"Run {i} 文本未变化: '{text}'")
  136. # 创建新的运行,保留原始格式
  137. new_run = paragraph.add_run(text)
  138. # 复制格式
  139. new_run.bold = run.bold
  140. new_run.italic = run.italic
  141. new_run.underline = run.underline
  142. new_run.font.name = run.font.name
  143. new_run.font.size = run.font.size
  144. # 专门处理中文字体,确保东亚字体(如仿宋、宋体等)能够正确保留
  145. if hasattr(run._element, 'rPr') and run._element.rPr is not None:
  146. # 检查是否有rFonts元素
  147. rfonts = run._element.rPr.xpath('./w:rFonts')
  148. if rfonts and hasattr(rfonts[0], 'get'):
  149. # 获取东亚字体属性
  150. east_asia_font = rfonts[0].get(qn('w:eastAsia'))
  151. if east_asia_font:
  152. # 设置新run的东亚字体
  153. new_run._element.rPr.rFonts.set(qn('w:eastAsia'), east_asia_font)
  154. logger.debug(f"设置东亚字体: {east_asia_font}")
  155. if run.font.color.rgb is not None:
  156. new_run.font.color.rgb = run.font.color.rgb
  157. # 记录处理后的段落文本
  158. logger.info(f"处理后段落文本: {paragraph.text}")
  159. def process_word_template(template_path, output_path, variables):
  160. """
  161. 处理Word文档,替换其中的模板变量
  162. 参数:
  163. template_path: 模板文档路径
  164. output_path: 输出文档路径
  165. variables: 变量替换字典 {变量名: 替换值}
  166. """
  167. # 记录处理开始
  168. start_time = time.time()
  169. logger.info(f"开始处理文档: {os.path.basename(template_path)}")
  170. logger.info(f"需要替换的变量: {list(variables.keys())}")
  171. # 只处理docx文件
  172. if not template_path.lower().endswith('.docx'):
  173. logger.error("只支持.docx格式的文件")
  174. raise ValueError("只支持.docx格式的文件")
  175. doc = Document(template_path)
  176. # 统计替换次数
  177. replacement_count = 0
  178. # 处理段落中的变量
  179. logger.info("开始处理文档段落...")
  180. paragraph_count = 0
  181. for i, paragraph in enumerate(doc.paragraphs):
  182. has_var = any(var_name in paragraph.text for var_name in variables.keys())
  183. if has_var:
  184. paragraph_count += 1
  185. replace_text_in_paragraph_improved(paragraph, variables)
  186. replacement_count += 1
  187. # 处理表格中的变量
  188. logger.info("开始处理文档表格...")
  189. table_cell_count = 0
  190. for t_idx, table in enumerate(doc.tables):
  191. for r_idx, row in enumerate(table.rows):
  192. for c_idx, cell in enumerate(row.cells):
  193. for p_idx, paragraph in enumerate(cell.paragraphs):
  194. has_var = any(var_name in paragraph.text for var_name in variables.keys())
  195. if has_var:
  196. table_cell_count += 1
  197. replace_text_in_paragraph_improved(paragraph, variables)
  198. replacement_count += 1
  199. logger.info(f"处理完成: 共处理了 {paragraph_count} 个段落和 {table_cell_count} 个表格单元格")
  200. # 保存生成的文档
  201. try:
  202. doc.save(output_path)
  203. logger.info(f"文档已保存: {os.path.basename(output_path)}")
  204. except PermissionError:
  205. # 如果文件被占用,尝试使用新的文件名
  206. dir_name = os.path.dirname(output_path)
  207. base_name = os.path.basename(output_path)
  208. new_output_path = os.path.join(dir_name, f"new_{base_name}")
  209. logger.warning(f"文件被占用,尝试保存到新位置: {os.path.basename(new_output_path)}")
  210. doc.save(new_output_path)
  211. # 重命名原始路径,以便后续代码能正确引用
  212. os.rename(new_output_path, output_path)
  213. logger.info(f"文件已重命名为: {os.path.basename(output_path)}")
  214. # 记录处理时间
  215. process_time = time.time() - start_time
  216. logger.info(f"文档处理完成,耗时: {process_time:.2f}秒")
  217. def replace_text_in_paragraph_improved(paragraph, variables):
  218. """
  219. 改进的段落变量替换方法,处理变量可能被分割在多个runs的情况
  220. 参数:
  221. paragraph: 要处理的段落对象
  222. variables: 变量替换字典 {变量名: 替换值}
  223. """
  224. # 记录原始段落文本
  225. original_text = paragraph.text
  226. # 检查段落是否包含任何变量
  227. found_variables = []
  228. for var_name in variables.keys():
  229. if var_name in original_text:
  230. found_variables.append(var_name)
  231. if not found_variables:
  232. return
  233. logger.info(f"发现需要替换的变量: {found_variables}")
  234. logger.info(f"原始文本: {original_text}")
  235. # 尝试使用原始的替换方法
  236. try:
  237. # 先尝试使用原始替换方法
  238. replace_text_in_paragraph(paragraph, variables)
  239. # 检查是否所有变量都被替换
  240. all_replaced = True
  241. for var_name in found_variables:
  242. if var_name in paragraph.text:
  243. all_replaced = False
  244. logger.warning(f"变量 '{var_name}' 未被替换,尝试使用备用方法")
  245. break
  246. if all_replaced:
  247. logger.info("所有变量已成功替换")
  248. return
  249. except Exception as e:
  250. logger.warning(f"原始替换方法失败: {str(e)},尝试使用备用方法")
  251. # 如果原始方法失败或未替换所有变量,使用备用方法
  252. logger.info("使用备用替换方法")
  253. # 记录原始格式信息
  254. original_runs = []
  255. for i, run in enumerate(paragraph.runs):
  256. original_runs.append({
  257. 'text': run.text,
  258. 'bold': run.bold,
  259. 'italic': run.italic,
  260. 'underline': run.underline,
  261. 'font_name': run.font.name,
  262. 'font_size': run.font.size,
  263. 'font_color': run.font.color.rgb,
  264. 'east_asia_font': None
  265. })
  266. # 获取东亚字体信息
  267. if hasattr(run._element, 'rPr') and run._element.rPr is not None:
  268. rfonts = run._element.rPr.xpath('./w:rFonts')
  269. if rfonts and hasattr(rfonts[0], 'get'):
  270. east_asia_font = rfonts[0].get(qn('w:eastAsia'))
  271. if east_asia_font:
  272. original_runs[-1]['east_asia_font'] = east_asia_font
  273. # 创建一个新的段落文本,替换所有变量
  274. new_text = original_text
  275. for var_name, var_value in variables.items():
  276. if var_name in new_text:
  277. logger.info(f"替换变量 '{var_name}' 为 '{var_value}'")
  278. new_text = new_text.replace(var_name, var_value)
  279. # 如果文本没有变化,不需要进一步处理
  280. if new_text == original_text:
  281. logger.info("文本未变化,跳过处理")
  282. return
  283. # 记录处理后的段落文本
  284. logger.info(f"替换后文本: {new_text}")
  285. # 清空段落
  286. paragraph.clear()
  287. # 使用最简单的方法:使用第一个run的格式,但不应用下划线
  288. # 这样可以确保文本不会全部带有下划线
  289. default_format = original_runs[0] if original_runs else None
  290. if default_format:
  291. new_run = paragraph.add_run(new_text)
  292. new_run.bold = default_format['bold']
  293. new_run.italic = default_format['italic']
  294. new_run.underline = False # 明确设置为不使用下划线
  295. new_run.font.name = default_format['font_name']
  296. if default_format['font_size'] is not None:
  297. new_run.font.size = default_format['font_size']
  298. if default_format['font_color'] is not None:
  299. new_run.font.color.rgb = default_format['font_color']
  300. # 设置东亚字体
  301. if default_format['east_asia_font']:
  302. new_run._element.rPr.rFonts.set(qn('w:eastAsia'), default_format['east_asia_font'])
  303. else:
  304. # 如果没有原始格式信息,直接添加文本
  305. paragraph.add_run(new_text)