From f9ab2ffce05c7195f715e07c4447367a8b11966e Mon Sep 17 00:00:00 2001
From: cxs <2282302055@qq.com>
Date: Tue, 20 May 2025 19:21:58 +0800
Subject: [PATCH] =?UTF-8?q?=E8=A1=A8=E6=A0=BC=E6=8F=90=E5=8F=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 81 ++
cxs/temp/outputs/response_图片_output.md | 24 -
cxs/temp/outputs/response_图片_output.txt | 1 -
table/table_cleaner.py | 1380 +++++++++++++++++++
table/table_to_html.py | 444 ++++++
5 files changed, 1905 insertions(+), 25 deletions(-)
delete mode 100644 cxs/temp/outputs/response_图片_output.md
delete mode 100644 cxs/temp/outputs/response_图片_output.txt
create mode 100644 table/table_cleaner.py
create mode 100644 table/table_to_html.py
diff --git a/README.md b/README.md
index 53d233d..05af61e 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,87 @@ pip install -r requirements.txt
## 最近更新
+### 2024年6月11日
+- **同时支持HTML标签显示和HTML文件生成**
+ - 优化表格处理功能,兼顾多种输出需求
+ - 移除Word文档中自动添加的表格标题,保持文档结构简洁
+ - Word文档中直接以HTML标签形式显示所有表格,方便查看表格结构
+ - 同时生成独立的HTML文件,提供完整的表格视图,支持交互和打印
+ - 在Word文档中添加蓝色超链接提示,指引用户查看对应的HTML文件
+ - 改进HTML样式,增强响应式布局和打印支持
+ - 优化表格HTML标签生成过程,确保标签规范性和一致性
+ - 增强错误处理,即使某些表格转换失败也能保持系统稳定
+ - 提升整体文档处理流程的健壮性和用户体验
+
+### 2024年6月10日
+- **采用HTML标签形式输出表格**
+ - 改进表格处理机制,直接输出HTML标签形式的表格,而非创建Word表格
+ - 精确保留所有表格结构信息,包括表头、主体和合并单元格属性
+ - 自动为表格生成符合HTML规范的标签,包括class和id属性
+ - 正确处理表格中的垂直和水平合并单元格,添加rowspan和colspan属性
+ - 将表格标签以等宽字体显示,提高可读性和直观性
+ - 优化标签生成过程,严格遵循HTML表格标准
+ - 自动区分表头和数据行,使用正确的thead和tbody标签
+ - 简化表格处理流程,提高效率和准确性
+
+### 2024年6月9日
+- **改进Word文档表格显示方式**
+ - 修改表格处理机制,直接在Word文档中显示表格,不再需要外部HTML文件
+ - 准确复制原始表格的结构、内容和合并单元格信息
+ - 保留表格样式并自动设置表头格式
+ - 正确处理垂直和水平合并的单元格
+ - 改进表格位置控制,保持与原始文档的一致性
+ - 优化表格边框和样式,提供更专业的外观
+ - 简化处理流程,提高文档生成效率
+ - 修复合并单元格时的潜在错误
+
+### 2024年6月8日
+- **修复Word文档打开问题并改进表格处理**
+ - 解决了清洗后Word文档无法打开的关键问题
+ - 优化HTML表格生成方式,确保文档处理的稳定性
+ - 在Word文档中添加醒目的HTML表格文件引用提示
+ - 保留表格的文本格式作为备用显示方式
+ - 改进错误处理,提供更详细的诊断信息
+ - 简化文档处理流程,提高代码可维护性
+ - 增强HTML表格文件的样式,提供更好的打印支持
+ - 改进文档处理日志,便于追踪处理过程
+
+### 2024年6月7日
+- **表格直接HTML输出功能增强**
+ - 修改表格处理机制,现在所有表格都将以HTML格式输出而非文本格式
+ - 彻底解决复杂表格的显示问题,包括多层表头和合并单元格
+ - 自动为每个表格生成独特的HTML标识符,确保正确引用
+ - 提供更美观的表格样式,包括悬停效果和自适应宽度
+ - 改进表格边框和单元格间距,提升阅读体验
+ - 保留单元格格式化内容(如换行符)并在HTML中正确显示
+ - 针对打印场景优化表格样式,确保打印输出质量
+ - 技术说明:由于Word文档格式限制,HTML表格将保存在独立的HTML文件中
+
+### 2024年6月6日
+- **增强复杂表格识别与处理能力**
+ - 优化表格类型自动识别算法,通过多维度特征分析提高复杂表格的识别精度
+ - 增强表格结构分析能力,支持更精确地识别垂直和水平合并单元格
+ - 改进多级表头处理,提高复杂表头的识别和解析能力
+ - 引入表格宽高比分析,自动识别宽表格和复杂结构表格
+ - 新增单元格数一致性检查机制,提高对不规则表格的处理能力
+ - 优化垂直合并单元格的内容填充算法,改进空单元格的值传播机制
+ - 添加更详细的表格处理日志,便于诊断和调试复杂表格处理问题
+ - 完善异常处理,提高处理复杂表格时的稳定性和鲁棒性
+
+### 2024年6月5日
+- **模块化表格处理系统升级**
+ - 重构了表格处理架构,将不同类型的表格处理逻辑拆分为专门的处理器
+ - 实现了针对五种特殊表格类型的专用处理器:
+ - 多级表头表格处理器:处理具有复杂多级表头结构的表格
+ - 合并单元格密集型表格处理器:优化处理含有大量合并单元格的表格
+ - 带计算功能的表格处理器:识别并处理包含公式和计算的表格
+ - 嵌套表格处理器:处理表格内嵌套的子表格结构
+ - 跨页长表格处理器:正确识别和处理跨越多页的长表格
+ - 优化表格识别机制,智能匹配最适合的处理器
+ - 提高表格转文本的准确性和可读性
+ - 保持与原有系统的兼容性,同时提高处理复杂表格的能力
+ - 增强了系统对特殊表格结构的识别率和处理精度
+
### 2024年6月2日
- 改进Markdown表格转换功能:
- 修复了合并单元格内容重复显示的问题
diff --git a/cxs/temp/outputs/response_图片_output.md b/cxs/temp/outputs/response_图片_output.md
deleted file mode 100644
index eee9d3f..0000000
--- a/cxs/temp/outputs/response_图片_output.md
+++ /dev/null
@@ -1,24 +0,0 @@
-1111
-
-【图片识别文本】
-“ 完 善 了 异 常 处 理 , 防 止 惑 时 目 录 券 除 失 败 导 致 程 序 崖
-澎
-澎
-
-4. 更 新 README.md
-* 在 暨 近 更 新 部 分 记 录 了 临 时 文 件 处 理 机 制 的 改 进
-。 添 加 了 Excel 文 件 句 柄 管 理
-
-使 用 说 明
-这 东 改 进 不 需 要 您 做 任 何 额 外 操 作 , 系 统 会 自 动 -
-1. 在 处 理 Excel 文 件 时 正 球 关 闭 文 件 句 柄
-
-2 当 尝 试 删 除 文 件 通 刨 “ 文 件 被 占 用 “ 错 误 时 , 自 动 等 待
-并 重 试
-
-3 即 使 无 法 券 除 临 时 文 件 , 也 不 影 响 处 #
-如 果 仍 然 通 到 惧 时 文 件 问 题 , 系 统 会 在 下 次 启 动 时 自 动
-清 理 所 有 临 时 文 件 , 不 会 影 响 系 统 功 能 。
-
-以 上 优 化 星 觞 失 了 临 时 文 伟 删 除 问 题 , 又 保 持 了 系 统 的
-稳 定 性 , 让 您 能 雪 顺 畅 地 处 理 Bxcel 文 件 。
\ No newline at end of file
diff --git a/cxs/temp/outputs/response_图片_output.txt b/cxs/temp/outputs/response_图片_output.txt
deleted file mode 100644
index 7a6184d..0000000
--- a/cxs/temp/outputs/response_图片_output.txt
+++ /dev/null
@@ -1 +0,0 @@
-1111 【图片识别文本】 “ 完 善 了 异 常 处 理 , 防 止 惑 时 目 录 券 除 失 败 导 致 程 序 崖 澎 澎 4. 更 新 README.md * 在 暨 近 更 新 部 分 记 录 了 临 时 文 件 处 理 机 制 的 改 进 。 添 加 了 Excel 文 件 句 柄 管 理 使 用 说 明 这 东 改 进 不 需 要 您 做 任 何 额 外 操 作 , 系 统 会 自 动 - 1. 在 处 理 Excel 文 件 时 正 球 关 闭 文 件 句 柄 2 当 尝 试 删 除 文 件 通 刨 “ 文 件 被 占 用 “ 错 误 时 , 自 动 等 待 并 重 试 3 即 使 无 法 券 除 临 时 文 件 , 也 不 影 响 处 # 如 果 仍 然 通 到 惧 时 文 件 问 题 , 系 统 会 在 下 次 启 动 时 自 动 清 理 所 有 临 时 文 件 , 不 会 影 响 系 统 功 能 。 以 上 优 化 星 觞 失 了 临 时 文 伟 删 除 问 题 , 又 保 持 了 系 统 的 稳 定 性 , 让 您 能 雪 顺 畅 地 处 理 Bxcel 文 件 。
\ No newline at end of file
diff --git a/table/table_cleaner.py b/table/table_cleaner.py
new file mode 100644
index 0000000..24088a1
--- /dev/null
+++ b/table/table_cleaner.py
@@ -0,0 +1,1380 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import os
+import re
+import docx
+import numpy as np
+import requests
+from sklearn.feature_extraction.text import TfidfVectorizer
+from sklearn.metrics.pairwise import cosine_similarity
+from typing import List, Tuple, Dict, Optional
+from docx.shared import Pt
+from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
+from docx.enum.table import WD_TABLE_ALIGNMENT
+import subprocess
+import tempfile
+import json
+from docx.table import Table, _Cell
+from docx.text.paragraph import Paragraph
+from copy import deepcopy
+from docx.oxml import parse_xml
+from docx.oxml.ns import nsdecls
+
+class DocCleaner:
+ def __init__(self, ollama_host: str = "http://192.168.1.24:11434"):
+ """
+ 初始化文档清理器
+
+ Args:
+ ollama_host: Ollama服务器地址
+ """
+ # 页眉页脚模式
+ self.header_footer_patterns = [
+ r'页码\s*\d+-\d+', # 页码格式:页码1-1, 页码2-1等
+ r'第\s*\d+\s*页\s*共\s*\d+\s*页', # 中文页码(第X页共Y页)
+ r'Page\s*\d+\s*of\s*\d+', # 英文页码
+ ]
+
+ # 特殊符号模式
+ self.special_char_patterns = [
+ r'©\s*\d{4}.*?版权所有', # 版权信息
+ r'confidential', # 机密标记
+ r'draft|草稿', # 草稿标记
+ r'watermark', # 水印标记
+ ]
+
+ # 附录和参考文献标题模式
+ self.appendix_patterns = [
+ r'^附录\s*[A-Za-z]?[\s::]',
+ r'^Appendix\s*[A-Za-z]?[\s::]',
+ r'^参考文献$',
+ r'^References$',
+ r'^Bibliography$'
+ ]
+
+ # 初始化TF-IDF向量化器
+ self.vectorizer = TfidfVectorizer(
+ min_df=1,
+ stop_words='english'
+ )
+
+ self.ollama_host = ollama_host
+ self.embedding_model = "bge-m3:latest" # 使用nomic-embed-text模型进行文本嵌入
+
+ def _convert_doc_to_docx(self, doc_path: str) -> str:
+ """
+ 将doc格式转换为docx格式
+
+ Args:
+ doc_path: doc文件路径
+
+ Returns:
+ str: 转换后的docx文件路径
+ """
+ # 创建临时文件路径
+ temp_dir = tempfile.mkdtemp()
+ temp_docx = os.path.join(temp_dir, 'temp.docx')
+
+ try:
+ # 使用soffice(LibreOffice)进行转换
+ cmd = ['soffice', '--headless', '--convert-to', 'docx', '--outdir', temp_dir, doc_path]
+ subprocess.run(cmd, check=True, capture_output=True)
+
+ # 返回转换后的文件路径
+ return temp_docx
+ except subprocess.CalledProcessError as e:
+ raise Exception(f"转换doc文件失败: {str(e)}")
+
+ def clean_doc(self, file_path: str) -> Tuple[List[str], List[str], List[Table]]:
+ """
+ 清理文档并返回处理后的正文、附录和表格
+
+ Args:
+ file_path: 文档文件路径
+
+ Returns:
+ Tuple[List[str], List[str], List[Table]]: (清理后的正文段落列表, 附录段落列表, 表格列表)
+ """
+ print(f"\n开始处理文档: {file_path}")
+
+ # 检测文件类型
+ _, file_extension = os.path.splitext(file_path)
+ file_extension = file_extension.lower()
+
+ # 如果是doc格式,先转换为docx
+ if file_extension == '.doc':
+ temp_docx = self._convert_doc_to_docx(file_path)
+ doc = docx.Document(temp_docx)
+ # 清理临时文件
+ os.remove(temp_docx)
+ os.rmdir(os.path.dirname(temp_docx))
+ else:
+ doc = docx.Document(file_path)
+
+ # 提取所有内容(段落和表格)
+ content = []
+ tables = []
+ table_count = 0
+
+ try:
+ print("\n开始解析文档结构...")
+ # 遍历文档体中的所有元素
+ for element in doc._element.body:
+ if element.tag.endswith('p'):
+ try:
+ paragraph = docx.text.paragraph.Paragraph(element, doc)
+ text = paragraph.text.strip()
+
+ # 只添加非空段落
+ if text:
+ # 检查是否是附录标题
+ is_appendix = any(re.match(pattern, text, re.IGNORECASE)
+ for pattern in self.appendix_patterns)
+ content.append({
+ 'type': 'paragraph',
+ 'content': text,
+ 'is_appendix_start': is_appendix
+ })
+ if is_appendix:
+ print(f"发现附录标题: {text}")
+ except Exception as e:
+ print(f"警告:处理段落时出错: {str(e)}")
+ continue
+
+ elif element.tag.endswith('tbl'):
+ try:
+ table = docx.table.Table(element, doc)
+ # 验证表格是否有效
+ if hasattr(table, 'rows') and hasattr(table, 'columns'):
+ tables.append(table)
+ content.append({
+ 'type': 'table',
+ 'index': table_count
+ })
+ print(f"发现表格 {table_count}: {len(table.rows)}行 x {len(table.columns)}列")
+ table_count += 1
+ except Exception as e:
+ print(f"警告:处理表格时出错: {str(e)}")
+ continue
+
+ except Exception as e:
+ print(f"警告:遍历文档内容时出错: {str(e)}")
+
+ print(f"\n文档结构解析完成:")
+ print(f"- 总元素数: {len(content)}")
+ print(f"- 表格数量: {len(tables)}")
+
+ # 分离正文和附录
+ main_content = []
+ appendix = []
+ is_appendix = False
+
+ print("\n开始分离正文和附录...")
+ for item in content:
+ if item['type'] == 'paragraph':
+ if item['is_appendix_start']:
+ is_appendix = True
+ print("进入附录部分")
+
+ if is_appendix:
+ appendix.append(item['content'])
+ else:
+ main_content.append(item['content'])
+
+ elif item['type'] == 'table':
+ table_placeholder = f'TABLE_PLACEHOLDER_{item["index"]}'
+ if is_appendix:
+ appendix.append(table_placeholder)
+ print(f"添加表格到附录: {table_placeholder}")
+ else:
+ main_content.append(table_placeholder)
+ print(f"添加表格到正文: {table_placeholder}")
+
+ print(f"\n分离完成:")
+ print(f"- 正文元素数: {len(main_content)}")
+ print(f"- 附录元素数: {len(appendix)}")
+
+ # 清理正文(保留表格标记)
+ cleaned_content = []
+ print("\n开始清理正文...")
+ for item in main_content:
+ if item.startswith('TABLE_PLACEHOLDER_'):
+ cleaned_content.append(item)
+ print(f"保留表格标记: {item}")
+ else:
+ cleaned_text = self._clean_text([item])[0]
+ if cleaned_text:
+ cleaned_content.append(cleaned_text)
+
+ print(f"\n清理完成:")
+ print(f"- 清理后元素数: {len(cleaned_content)}")
+ print("- 表格标记位置:")
+ for i, item in enumerate(cleaned_content):
+ if item.startswith('TABLE_PLACEHOLDER_'):
+ print(f" 位置 {i}: {item}")
+
+ return cleaned_content, appendix, tables
+
+ def _clean_text(self, text: List[str]) -> List[str]:
+ """
+ 清理文本内容
+
+ Args:
+ text: 待清理的文本段落列表
+
+ Returns:
+ List[str]: 清理后的文本段落列表
+ """
+ cleaned = []
+ for paragraph in text:
+ # 如果是表格标记,直接保留
+ if paragraph.startswith('TABLE_PLACEHOLDER_'):
+ cleaned.append(paragraph)
+ continue
+
+ # 跳过空段落
+ if not paragraph.strip():
+ continue
+
+ # 检查是否是目录项(包含数字序号的行)
+ is_toc_item = bool(re.match(r'^\s*(?:\d+\.)*\d+\s+.*', paragraph))
+
+ if not is_toc_item:
+ # 移除页眉页脚
+ for pattern in self.header_footer_patterns:
+ paragraph = re.sub(pattern, '', paragraph, flags=re.IGNORECASE)
+
+ # 移除特殊符号
+ for pattern in self.special_char_patterns:
+ paragraph = re.sub(pattern, '', paragraph, flags=re.IGNORECASE)
+
+ # 如果段落不为空,添加到结果中
+ if paragraph.strip():
+ cleaned.append(paragraph.strip())
+
+ return cleaned
+
+ def _split_content(self, paragraphs: List[str]) -> Tuple[List[str], List[str]]:
+ """
+ 分离正文与附录/参考文献
+
+ Args:
+ paragraphs: 文档段落列表
+
+ Returns:
+ Tuple[List[str], List[str]]: (正文段落列表, 附录段落列表)
+ """
+ main_content = []
+ appendix = []
+ is_appendix = False
+
+ for p in paragraphs:
+ # 检查是否是附录开始
+ if any(re.match(pattern, p, re.IGNORECASE) for pattern in self.appendix_patterns):
+ is_appendix = True
+
+ if is_appendix:
+ appendix.append(p)
+ else:
+ main_content.append(p)
+
+ return main_content, appendix
+
+ def _get_embeddings(self, texts: List[str]) -> np.ndarray:
+ """
+ 使用Ollama获取文本嵌入向量
+
+ Args:
+ texts: 文本列表
+
+ Returns:
+ np.ndarray: 嵌入向量矩阵
+ """
+ embeddings = []
+
+ for text in texts:
+ try:
+ response = requests.post(
+ f"{self.ollama_host}/api/embeddings",
+ json={
+ "model": self.embedding_model,
+ "prompt": text
+ }
+ )
+ response.raise_for_status()
+ embedding = response.json()["embedding"]
+ embeddings.append(embedding)
+ except Exception as e:
+ print(f"获取文本嵌入失败: {str(e)}")
+ # 如果获取嵌入失败,使用零向量
+ embeddings.append([0.0] * 768) # nomic-embed-text 模型输出维度为768
+
+ return np.array(embeddings)
+
+ def _remove_duplicates(self, paragraphs: List[str], similarity_threshold: float = 0.92) -> List[str]:
+ """
+ 删除重复段落,保持表格占位符的位置不变
+
+ Args:
+ paragraphs: 段落列表
+ similarity_threshold: 相似度阈值,使用嵌入模型后可以设置更高的阈值
+
+ Returns:
+ List[str]: 去重后的段落列表
+ """
+ if not paragraphs:
+ return []
+
+ # 分离表格占位符和普通段落
+ table_placeholders = {}
+ text_paragraphs = []
+ for i, p in enumerate(paragraphs):
+ if p.startswith('TABLE_PLACEHOLDER_'):
+ table_placeholders[i] = p
+ else:
+ text_paragraphs.append((i, p))
+
+ try:
+ # 只对非表格段落进行去重
+ if text_paragraphs:
+ # 获取文本嵌入
+ text_only = [p[1] for p in text_paragraphs]
+ embeddings = self._get_embeddings(text_only)
+
+ # 计算余弦相似度矩阵
+ similarity_matrix = cosine_similarity(embeddings)
+
+ # 标记要保留的段落
+ keep_indices = []
+ for i in range(len(text_paragraphs)):
+ # 如果当前段落没有与之前的段落高度相似,则保留
+ if not any(similarity_matrix[i][j] > similarity_threshold for j in keep_indices):
+ keep_indices.append(i)
+
+ # 保留的非表格段落
+ kept_paragraphs = [(text_paragraphs[i][0], text_only[i]) for i in keep_indices]
+ else:
+ kept_paragraphs = []
+
+ # 合并表格占位符和保留的段落,按原始位置排序
+ all_kept = list(table_placeholders.items()) + kept_paragraphs
+ all_kept.sort(key=lambda x: x[0])
+
+ return [p[1] for p in all_kept]
+
+ except Exception as e:
+ print(f"使用Ollama嵌入模型失败,回退到TF-IDF方法: {str(e)}")
+ # 如果使用Ollama失败,回退到原来的TF-IDF方法
+ return self._remove_duplicates_tfidf(paragraphs)
+
+ def _remove_duplicates_tfidf(self, paragraphs: List[str], similarity_threshold: float = 0.85) -> List[str]:
+ """
+ 使用TF-IDF方法删除重复段落(作为备选方案)
+
+ Args:
+ paragraphs: 段落列表
+ similarity_threshold: 相似度阈值
+
+ Returns:
+ List[str]: 去重后的段落列表
+ """
+ if not paragraphs:
+ return []
+
+ # 分离表格占位符和普通段落
+ table_placeholders = {}
+ text_paragraphs = []
+ for i, p in enumerate(paragraphs):
+ if p.startswith('TABLE_PLACEHOLDER_'):
+ table_placeholders[i] = p
+ else:
+ text_paragraphs.append((i, p))
+
+ if text_paragraphs:
+ # 计算TF-IDF矩阵
+ text_only = [p[1] for p in text_paragraphs]
+ tfidf_matrix = self.vectorizer.fit_transform(text_only)
+
+ # 计算余弦相似度矩阵
+ similarity_matrix = cosine_similarity(tfidf_matrix)
+
+ # 标记要保留的段落
+ keep_indices = []
+ for i in range(len(text_paragraphs)):
+ # 如果当前段落没有与之前的段落高度相似,则保留
+ if not any(similarity_matrix[i][j] > similarity_threshold for j in keep_indices):
+ keep_indices.append(i)
+
+ # 保留的非表格段落
+ kept_paragraphs = [(text_paragraphs[i][0], text_only[i]) for i in keep_indices]
+ else:
+ kept_paragraphs = []
+
+ # 合并表格占位符和保留的段落,按原始位置排序
+ all_kept = list(table_placeholders.items()) + kept_paragraphs
+ all_kept.sort(key=lambda x: x[0])
+
+ return [p[1] for p in all_kept]
+
+ def save_as_docx(self, cleaned_content: List[str], appendix: List[str], tables: List[Table], output_path: str):
+ """
+ 将清理后的内容保存为docx格式和txt格式
+
+ Args:
+ cleaned_content: 清理后的正文段落列表
+ appendix: 附录段落列表
+ tables: 表格列表
+ output_path: 输出文件路径
+ """
+ print(f"\n开始保存文档: {output_path}")
+ print(f"- 正文元素数: {len(cleaned_content)}")
+ print(f"- 附录元素数: {len(appendix)}")
+ print(f"- 表格总数: {len(tables)}")
+
+ # 创建新文档
+ doc = docx.Document()
+
+ # 创建文本输出内容列表(用于保存txt文件)
+ text_output = []
+
+ # 生成HTML表格文件
+ html_file_path = os.path.splitext(output_path)[0] + '_tables.html'
+ html_tables = []
+
+ # 添加正文内容和表格,保持它们的相对位置
+ print("\n处理正文内容...")
+
+ for i, content in enumerate(cleaned_content):
+ try:
+ # 检查是否是表格占位符
+ table_match = re.match(r'TABLE_PLACEHOLDER_(\d+)', content)
+ if table_match:
+ table_index = int(table_match.group(1))
+ print(f"正在处理表格占位符: {content} (索引: {table_index})")
+ if table_index < len(tables):
+ source_table = tables[table_index]
+ try:
+ # 生成表格的HTML标签
+ html_tags = self._generate_table_html_tags(source_table, f"table_{table_index}")
+
+ # 添加HTML标签作为普通文本
+ p = doc.add_paragraph()
+ run = p.add_run(html_tags)
+ run.font.name = 'Courier New' # 使用等宽字体
+ run.font.size = Pt(10) # 设置字体大小
+
+ # 保存HTML到列表,用于生成HTML文件
+ try:
+ from table.table_to_html import TableToHtml
+ converter = TableToHtml(debug=False)
+ html_code = converter.table_to_html(source_table)
+ html_tables.append(html_code)
+ except Exception as e:
+ print(f"警告:生成HTML表格时出错: {str(e)}")
+ html_tables.append(f"
表格 {table_index + 1} 处理失败: {str(e)}
")
+
+ # 添加到文本输出
+ text_output.append(f"表格 {table_index + 1} 开始:")
+
+ # 获取表格文本用于txt输出
+ table_text = self._convert_table_to_text(source_table)
+ text_output.append(table_text)
+ text_output.append(f"表格 {table_index + 1} 结束:")
+
+ # 添加空行
+ doc.add_paragraph()
+
+ except Exception as e:
+ print(f"警告:处理表格时出错: {str(e)}")
+ doc.add_paragraph(f"【表格处理失败: {str(e)}】")
+ text_output.append("【表格处理失败】")
+ else:
+ # 添加普通段落
+ p = doc.add_paragraph(content)
+ p.alignment = WD_PARAGRAPH_ALIGNMENT.JUSTIFY
+ # 添加到文本输出
+ text_output.append(content)
+ except Exception as e:
+ print(f"警告:处理段落或表格时出错: {str(e)}")
+ continue
+
+ # 如果有附录,添加分隔符和附录内容
+ if appendix:
+ print("\n处理附录内容...")
+ try:
+ # 添加分页符
+ doc.add_page_break()
+
+ # 添加附录标题
+ title = doc.add_paragraph("附录")
+ title.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
+
+ # 添加到文本输出
+ text_output.append("附录")
+
+ # 添加附录内容
+ for content in appendix:
+ # 检查是否是表格占位符
+ table_match = re.match(r'TABLE_PLACEHOLDER_(\d+)', content)
+ if table_match:
+ table_index = int(table_match.group(1))
+ print(f"正在处理附录中的表格占位符: {content} (索引: {table_index})")
+ if table_index < len(tables):
+ source_table = tables[table_index]
+ try:
+ # 生成表格的HTML标签
+ html_tags = self._generate_table_html_tags(source_table, f"table_appendix_{table_index}")
+
+ # 添加HTML标签作为普通文本
+ p = doc.add_paragraph()
+ run = p.add_run(html_tags)
+ run.font.name = 'Courier New' # 使用等宽字体
+ run.font.size = Pt(10) # 设置字体大小
+
+ # 保存HTML到列表,用于生成HTML文件
+ try:
+ from table.table_to_html import TableToHtml
+ converter = TableToHtml(debug=False)
+ html_code = converter.table_to_html(source_table)
+ html_tables.append(html_code)
+ except Exception as e:
+ print(f"警告:生成HTML表格时出错: {str(e)}")
+ html_tables.append(f"附录表格 {table_index + 1} 处理失败: {str(e)}
")
+
+ # 添加到文本输出
+ text_output.append(f"附录表格 {table_index + 1} 开始:")
+
+ # 获取表格文本用于txt输出
+ table_text = self._convert_table_to_text(source_table)
+ text_output.append(table_text)
+ text_output.append(f"附录表格 {table_index + 1} 结束:")
+
+ except Exception as e:
+ print(f"警告:处理附录表格时出错: {str(e)}")
+ doc.add_paragraph(f"【表格处理失败: {str(e)}】")
+ text_output.append("【表格处理失败】")
+ else:
+ p = doc.add_paragraph(content)
+ p.alignment = WD_PARAGRAPH_ALIGNMENT.JUSTIFY
+ # 添加到文本输出
+ text_output.append(content)
+
+ except Exception as e:
+ print(f"警告:处理附录时出错: {str(e)}")
+
+ # 保存HTML表格到文件
+ if html_tables:
+ try:
+ html_content = f'''
+
+
+
+ 表格预览
+
+
+
+
+ 文档中的表格
+ {' '.join(html_tables)}
+
+'''
+
+ with open(html_file_path, 'w', encoding='utf-8') as f:
+ f.write(html_content)
+ print(f"\nHTML表格文件已保存到: {html_file_path}")
+
+ # 添加HTML文件引用提示到Word文档
+ notice = doc.add_paragraph()
+ notice.add_run("表格完整HTML版本可查看文件: ").font.bold = True
+ run = notice.add_run(os.path.basename(html_file_path))
+ run.font.color.rgb = docx.shared.RGBColor(0, 0, 255) # 蓝色
+ run.font.underline = True # 下划线
+
+ except Exception as e:
+ print(f"警告:保存HTML表格文件时出错: {str(e)}")
+
+ # 保存docx文档和相关文件
+ try:
+ # 保存Word文档
+ doc.save(output_path)
+ print("\nWord文档保存成功!")
+
+ except Exception as e:
+ print(f"错误:保存Word文档时出错: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # 保存文本文件
+ try:
+ text_file_path = os.path.splitext(output_path)[0] + '.txt'
+ # 移除所有换行符并用空格连接
+ text_content = ' '.join([t.replace('\n', ' ').strip() for t in text_output if t.strip()])
+ with open(text_file_path, 'w', encoding='utf-8') as f:
+ f.write(text_content)
+ print(f"文本文件保存成功: {text_file_path}")
+ except Exception as e:
+ print(f"错误:保存文本文件时出错: {str(e)}")
+ raise
+
+ def _generate_table_html_tags(self, table: Table, table_id: str) -> str:
+ """
+ 生成表格的HTML标签字符串
+
+ Args:
+ table: 源表格
+ table_id: 表格的唯一ID
+
+ Returns:
+ str: HTML标签字符串
+ """
+ rows = len(table.rows)
+ cols = len(table.columns)
+
+ if rows == 0 or cols == 0:
+ return ""
+
+ # 分析表格结构(查找合并单元格)
+ merged_cells = {}
+ merged_v_cells = set() # 记录被垂直合并的单元格
+
+ # 检测合并单元格
+ for i in range(rows):
+ for j in range(cols):
+ try:
+ cell = table.cell(i, j)
+
+ # 检查是否是合并单元格
+ if cell._element.tcPr is not None:
+ # 检查垂直合并 (vMerge)
+ vmerge = cell._element.tcPr.xpath('.//w:vMerge')
+ if vmerge:
+ val = vmerge[0].get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val', 'continue')
+ if val == 'restart':
+ # 垂直合并的起始单元格
+ vspan = 1
+ for k in range(i+1, rows):
+ next_cell = table.cell(k, j)
+ if next_cell._element.tcPr is not None:
+ next_vmerge = next_cell._element.tcPr.xpath('.//w:vMerge')
+ if next_vmerge and next_vmerge[0].get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val', 'continue') == 'continue':
+ vspan += 1
+ merged_v_cells.add((k, j))
+ else:
+ break
+ else:
+ break
+
+ if vspan > 1:
+ merged_cells[(i, j)] = {'rowspan': vspan}
+
+ # 检查水平合并 (gridSpan)
+ gridspan = cell._element.tcPr.xpath('.//w:gridSpan')
+ if gridspan:
+ span = int(gridspan[0].get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val', '1'))
+ if span > 1:
+ if (i, j) in merged_cells:
+ merged_cells[(i, j)]['colspan'] = span
+ else:
+ merged_cells[(i, j)] = {'colspan': span}
+ except Exception as e:
+ print(f"警告: 分析单元格 [{i},{j}] 时出错: {str(e)}")
+
+ # 生成HTML标签
+ html_lines = []
+ html_lines.append(f'')
+
+ # 添加表头
+ html_lines.append('')
+ html_lines.append('')
+
+ # 检测第一行是否为表头
+ for j in range(cols):
+ cell_text = table.cell(0, j).text.strip() if rows > 0 else ""
+ th_attrs = []
+
+ # 添加合并属性
+ if (0, j) in merged_cells:
+ if 'rowspan' in merged_cells[(0, j)]:
+ th_attrs.append(f'rowspan="{merged_cells[(0, j)]["rowspan"]}"')
+ if 'colspan' in merged_cells[(0, j)]:
+ th_attrs.append(f'colspan="{merged_cells[(0, j)]["colspan"]}"')
+
+ attrs_str = " ".join(th_attrs)
+ if attrs_str:
+ html_lines.append(f'{cell_text} | ')
+ else:
+ html_lines.append(f'{cell_text} | ')
+
+ html_lines.append('
')
+ html_lines.append('')
+
+ # 添加表格主体
+ html_lines.append('')
+
+ # 从第二行开始添加数据行
+ for i in range(1, rows):
+ html_lines.append('')
+
+ for j in range(cols):
+ # 如果是被垂直合并的单元格,跳过
+ if (i, j) in merged_v_cells:
+ continue
+
+ cell_text = table.cell(i, j).text.strip()
+ td_attrs = []
+
+ # 添加合并属性
+ if (i, j) in merged_cells:
+ if 'rowspan' in merged_cells[(i, j)]:
+ td_attrs.append(f'rowspan="{merged_cells[(i, j)]["rowspan"]}"')
+ if 'colspan' in merged_cells[(i, j)]:
+ td_attrs.append(f'colspan="{merged_cells[(i, j)]["colspan"]}"')
+
+ attrs_str = " ".join(td_attrs)
+ if attrs_str:
+ html_lines.append(f'{cell_text} | ')
+ else:
+ html_lines.append(f'{cell_text} | ')
+
+ html_lines.append('
')
+
+ html_lines.append('')
+ html_lines.append('
')
+
+ return '\n'.join(html_lines)
+
+ def _copy_table_fallback(self, doc: docx.Document, table: Table):
+ """
+ 表格复制的备用方法
+
+ Args:
+ doc: 目标文档
+ table: 源表格
+ """
+ # 获取表格的行数和列数
+ rows = len(table.rows)
+ cols = len(table.columns)
+
+ # 创建新表格
+ new_table = doc.add_table(rows=rows, cols=cols)
+
+ # 复制表格样式
+ if table.style:
+ new_table.style = table.style
+
+ # 复制表格属性
+ new_table._element.tblPr = deepcopy(table._element.tblPr)
+
+ # 复制网格信息
+ new_table._element.tblGrid = deepcopy(table._element.tblGrid)
+
+ # 创建单元格映射以跟踪合并
+ cell_map = {}
+
+ # 第一遍:标记合并的单元格
+ for i in range(rows):
+ for j in range(cols):
+ try:
+ src_cell = table.cell(i, j)
+ # 检查是否是合并单元格的一部分
+ if src_cell._element.tcPr is not None:
+ # 检查垂直合并
+ vmerge = src_cell._element.tcPr.xpath('.//w:vMerge')
+ if vmerge:
+ val = vmerge[0].get(qn('w:val'), 'continue')
+ if val == 'restart':
+ # 这是合并的起始单元格
+ span = self._get_vertical_span(table, i, j)
+ cell_map[(i, j)] = ('vmerge', span)
+
+ # 检查水平合并
+ gridspan = src_cell._element.tcPr.xpath('.//w:gridSpan')
+ if gridspan:
+ span = int(gridspan[0].get(qn('w:val')))
+ if span > 1:
+ cell_map[(i, j)] = ('hmerge', span)
+ except Exception as e:
+ print(f"警告:处理合并单元格时出错 [{i},{j}]: {str(e)}")
+
+ # 第二遍:复制内容并执行合并
+ for i in range(rows):
+ for j in range(cols):
+ try:
+ src_cell = table.cell(i, j)
+ dst_cell = new_table.cell(i, j)
+
+ # 检查是否需要合并
+ if (i, j) in cell_map:
+ merge_type, span = cell_map[(i, j)]
+ if merge_type == 'vmerge':
+ # 垂直合并
+ for k in range(1, span):
+ if i + k < rows:
+ dst_cell.merge(new_table.cell(i + k, j))
+ elif merge_type == 'hmerge':
+ # 水平合并
+ for k in range(1, span):
+ if j + k < cols:
+ dst_cell.merge(new_table.cell(i, j + k))
+
+ # 复制单元格属性
+ if src_cell._element.tcPr is not None:
+ dst_cell._element.tcPr = deepcopy(src_cell._element.tcPr)
+
+ # 复制单元格内容
+ dst_cell.text = "" # 清除默认内容
+ for src_paragraph in src_cell.paragraphs:
+ dst_paragraph = dst_cell.add_paragraph()
+ # 复制段落属性
+ if src_paragraph._element.pPr is not None:
+ dst_paragraph._element.pPr = deepcopy(src_paragraph._element.pPr)
+
+ # 复制文本和格式
+ for src_run in src_paragraph.runs:
+ dst_run = dst_paragraph.add_run(src_run.text)
+ # 复制运行属性
+ if src_run._element.rPr is not None:
+ dst_run._element.rPr = deepcopy(src_run._element.rPr)
+
+ except Exception as e:
+ print(f"警告:复制单元格时出错 [{i},{j}]: {str(e)}")
+ continue
+
+ def _get_vmerge_value(self, cell_element) -> str:
+ """
+ 获取单元格的垂直合并属性
+
+ Args:
+ cell_element: 单元格元素
+
+ Returns:
+ str: 垂直合并属性值
+ """
+ vmerge = cell_element.xpath('.//w:vMerge')
+ if vmerge:
+ return vmerge[0].get(qn('w:val'), 'continue')
+ return None
+
+ def _get_gridspan_value(self, cell_element) -> int:
+ """
+ 获取单元格的水平合并数量
+
+ Args:
+ cell_element: 单元格元素
+
+ Returns:
+ int: 水平合并的列数
+ """
+ try:
+ gridspan = cell_element.xpath('.//w:gridSpan')
+ if gridspan and gridspan[0].get(qn('w:val')):
+ return int(gridspan[0].get(qn('w:val')))
+ except (ValueError, TypeError, AttributeError) as e:
+ print(f"警告:获取gridspan值时出错: {str(e)}")
+ return 1 # 默认返回1,表示没有合并
+
+ def _get_vertical_span(self, table: Table, start_row: int, col: int) -> int:
+ """
+ 计算垂直合并的行数
+
+ Args:
+ table: 表格对象
+ start_row: 起始行
+ col: 列号
+
+ Returns:
+ int: 垂直合并的行数
+ """
+ span = 1
+ for i in range(start_row + 1, len(table.rows)):
+ cell = table.cell(i, col)
+ if self._get_vmerge_value(cell._element) == 'continue':
+ span += 1
+ else:
+ break
+ return span
+
+ def _convert_table_to_text(self, table: Table) -> str:
+ """
+ 将表格转换为文本格式,智能处理简单和复杂表格结构
+
+ Args:
+ table: docx表格对象
+
+ Returns:
+ str: 表格的文本表示
+ """
+ try:
+ # 获取表格的行数和列数
+ rows = len(table.rows)
+ cols = len(table.columns)
+
+ print(f"开始处理表格: {rows}行 x {cols}列")
+
+ if rows == 0 or cols == 0:
+ return "【空表格】"
+
+ # 存储处理后的表格数据
+ processed_data = []
+
+ # 检查是否是复杂表格(具有合并单元格或多级表头)
+ is_complex_table = False
+ max_header_rows = min(4, rows) # 最多检查前4行,增加检测范围
+
+ # 表格类型检测增强
+ # 1. 检查表格宽高比 - 宽表格通常更复杂
+ aspect_ratio = cols / rows if rows > 0 else 0
+ if aspect_ratio > 3 or cols > 6:
+ print("表格检测: 宽表格(列数>6或宽高比>3),标记为复杂表格")
+ is_complex_table = True
+
+ # 2. 检查前几行是否存在合并单元格
+ if not is_complex_table:
+ merge_count = 0
+ for i in range(max_header_rows):
+ for j in range(cols):
+ try:
+ cell = table.cell(i, j)
+ if cell._element.tcPr is not None:
+ # 检查垂直合并
+ vmerge = cell._element.tcPr.xpath('.//w:vMerge')
+ if vmerge:
+ print(f"表格检测: 发现垂直合并单元格 at [{i},{j}]")
+ merge_count += 1
+ if merge_count >= 2: # 增加阈值判断
+ is_complex_table = True
+ break
+ # 检查水平合并
+ gridspan = cell._element.tcPr.xpath('.//w:gridSpan')
+ if gridspan:
+ span_val = self._get_gridspan_value(cell._element)
+ print(f"表格检测: 发现水平合并单元格 at [{i},{j}], 跨度: {span_val}")
+ if span_val > 1:
+ merge_count += 1
+ if merge_count >= 2: # 增加阈值判断
+ is_complex_table = True
+ break
+ except Exception as e:
+ print(f"表格检测: 检查单元格 [{i},{j}] 时出错: {str(e)}")
+ continue
+ if is_complex_table:
+ break
+
+ # 3. 检查每行的单元格数是否一致 - 不一致通常表示嵌套或特殊结构
+ if not is_complex_table:
+ cell_counts = []
+ for i in range(min(5, rows)): # 检查前5行
+ try:
+ actual_cells = 0
+ for j in range(cols):
+ cell = table.cell(i, j)
+ # 考虑水平合并
+ if cell._element.tcPr is not None:
+ gridspan = cell._element.tcPr.xpath('.//w:gridSpan')
+ if gridspan:
+ actual_cells += 1 # 只计算一次,不管跨度
+ else:
+ actual_cells += 1
+ else:
+ actual_cells += 1
+ cell_counts.append(actual_cells)
+ except Exception:
+ continue
+
+ # 检查单元格数是否一致
+ if len(cell_counts) > 1 and len(set(cell_counts)) > 1:
+ print(f"表格检测: 各行单元格数不一致 {cell_counts},标记为复杂表格")
+ is_complex_table = True
+
+ print(f"表格分类: {'复杂表格' if is_complex_table else '简单表格'}")
+
+ if is_complex_table:
+ # 使用复杂表格处理逻辑
+ # 第一步:分析表头结构
+ header_structure = [] # 存储表头的层级结构
+ header_merge_map = {} # 记录合并单元格的映射关系
+
+ # 分析每一列的表头结构
+ print("开始分析复杂表格表头结构...")
+ for j in range(cols):
+ column_headers = []
+ last_header = None
+ for i in range(max_header_rows):
+ try:
+ cell = table.cell(i, j)
+ text = cell.text.strip()
+
+ # 检查垂直合并
+ if cell._element.tcPr is not None:
+ vmerge = cell._element.tcPr.xpath('.//w:vMerge')
+ if vmerge:
+ val = vmerge[0].get(qn('w:val'), 'continue')
+ if val == 'continue':
+ # 使用上一个非空表头
+ if last_header:
+ print(f"表头分析: 垂直合并单元格 at [{i},{j}],使用上一行值: {last_header}")
+ text = last_header
+ # 记录合并关系
+ header_merge_map[(i, j)] = (i-1, j)
+ else:
+ # 向上查找第一个非continue的单元格
+ for k in range(i-1, -1, -1):
+ try:
+ prev_cell = table.cell(k, j)
+ prev_text = prev_cell.text.strip()
+ if prev_text:
+ text = prev_text
+ print(f"表头分析: 垂直合并单元格 at [{i},{j}],使用上方值 [{k},{j}]: {text}")
+ break
+ except Exception:
+ continue
+
+ # 检查水平合并
+ if cell._element.tcPr is not None:
+ gridspan = self._get_gridspan_value(cell._element)
+ if gridspan > 1:
+ # 标记这是一个跨列的表头
+ print(f"表头分析: 水平合并单元格 at [{i},{j}],跨度 {gridspan},值: {text}")
+ if text: # 只处理有内容的单元格
+ text = f"SPAN_{gridspan}_{text}"
+ # 记录水平合并影响的列
+ for k in range(1, gridspan):
+ if j + k < cols:
+ header_merge_map[(i, j+k)] = (i, j)
+
+ if text:
+ column_headers.append(text)
+ last_header = text
+
+ except Exception as e:
+ print(f"表头分析: 处理表头单元格 [{i},{j}] 时出错: {str(e)}")
+ continue
+
+ header_structure.append(column_headers)
+ print(f"列 {j} 的表头结构: {column_headers}")
+
+ # 第二步:构建完整的表头标识符
+ full_headers = []
+ print("开始构建完整表头标识符...")
+
+ # 处理跨行跨列的表头
+ # 先进行一次预处理,处理合并单元格
+ for j, headers in enumerate(header_structure):
+ if not headers:
+ # 检查是否是被合并的列
+ is_merged = False
+ for i in range(max_header_rows):
+ if (i, j) in header_merge_map:
+ src_i, src_j = header_merge_map[(i, j)]
+ src_cell = table.cell(src_i, src_j)
+ src_text = src_cell.text.strip()
+ if src_text and src_j != j: # 确保是水平合并
+ print(f"表头补全: 列 {j} 被列 {src_j} 合并,添加表头: {src_text}")
+ header_structure[j].append(src_text)
+ is_merged = True
+ break
+
+ if not is_merged:
+ print(f"表头补全: 列 {j} 无表头,使用默认值: 列{j+1}")
+ header_structure[j].append(f"列{j+1}")
+
+ # 构建每列的完整表头
+ for j, headers in enumerate(header_structure):
+ if not headers:
+ full_headers.append(f"列{j+1}")
+ continue
+
+ # 处理跨列的表头
+ header_text = []
+ current_prefix = ""
+ for h in headers:
+ if h.startswith('SPAN_'):
+ parts = h.split('_', 2)
+ span = int(parts[1])
+ text = parts[2]
+ # 将跨列的表头添加到后续的列
+ for k in range(span):
+ if j + k < cols:
+ if k == 0:
+ if text != current_prefix: # 避免重复前缀
+ header_text.append(text)
+ current_prefix = text
+ else:
+ if text not in header_structure[j + k]:
+ header_structure[j + k].insert(0, text)
+ else:
+ if h != current_prefix: # 避免重复前缀
+ header_text.append(h)
+ current_prefix = h
+
+ # 移除重复的表头部分
+ unique_headers = []
+ seen = set()
+ for h in header_text:
+ if h not in seen:
+ unique_headers.append(h)
+ seen.add(h)
+
+ # 构建完整表头,使用特殊分隔符
+ if unique_headers:
+ full_header = '_'.join(unique_headers)
+ print(f"列 {j} 的完整表头: {full_header}")
+ full_headers.append(full_header)
+ else:
+ full_headers.append(f"列{j+1}")
+
+ # 确定实际的表头行数
+ header_row_count = max(len(headers) for headers in header_structure)
+ if header_row_count == 0:
+ header_row_count = 1
+
+ print(f"表头行数: {header_row_count}")
+ print(f"开始处理数据行,从第 {header_row_count} 行开始...")
+
+ # 创建跟踪已处理垂直合并单元格的集合
+ processed_vmerge = set()
+
+ # 处理数据行
+ for i in range(header_row_count, rows):
+ try:
+ row_data = []
+ j = 0
+ while j < cols:
+ try:
+ cell = table.cell(i, j)
+ text = cell.text.strip()
+
+ # 处理垂直合并单元格
+ if not text and cell._element.tcPr is not None:
+ vmerge = cell._element.tcPr.xpath('.//w:vMerge')
+ if vmerge:
+ val = vmerge[0].get(qn('w:val'), 'continue')
+ if val == 'continue':
+ # 向上查找非continue的值
+ for k in range(i-1, header_row_count-1, -1):
+ if (k, j) in processed_vmerge:
+ continue
+ try:
+ src_cell = table.cell(k, j)
+ src_text = src_cell.text.strip()
+ if src_text:
+ text = src_text
+ print(f"数据行处理: 垂直合并单元格 at [{i},{j}],使用上方值 [{k},{j}]: {text}")
+ break
+ except Exception:
+ continue
+ processed_vmerge.add((i, j))
+
+ # 处理水平合并
+ gridspan = self._get_gridspan_value(cell._element)
+
+ # 将值复制到所有合并的列
+ for k in range(gridspan):
+ if j + k < len(full_headers):
+ # 使用冒号分隔表头和值
+ if text:
+ row_data.append(f"{full_headers[j+k]}:{text}")
+ else:
+ row_data.append(f"{full_headers[j+k]}:")
+
+ j += gridspan
+
+ except Exception as e:
+ print(f"数据行处理: 处理数据单元格 [{i},{j}] 时出错: {str(e)}")
+ if j < len(full_headers):
+ row_data.append(f"{full_headers[j]}:")
+ j += 1
+
+ # 确保行中至少有一个非空值
+ if any(len(data.split(':', 1)) > 1 and data.split(':', 1)[1].strip() for data in row_data):
+ processed_line = " ".join(row_data)
+ print(f"添加处理行 {i}: {processed_line[:100]}..." if len(processed_line) > 100 else f"添加处理行 {i}: {processed_line}")
+ processed_data.append(processed_line)
+
+ except Exception as e:
+ print(f"数据行处理: 处理数据行 {i} 时出错: {str(e)}")
+ continue
+
+ else:
+ # 使用简单表格处理逻辑
+ print("使用简单表格处理逻辑...")
+ # 获取表头
+ headers = []
+ for j in range(cols):
+ try:
+ header_text = table.cell(0, j).text.strip()
+ if not header_text: # 如果表头为空,使用默认值
+ header_text = f"列{j+1}"
+ headers.append(header_text)
+ print(f"简单表格表头 {j}: {header_text}")
+ except Exception as e:
+ print(f"简单表格处理: 处理表头单元格 [0,{j}] 时出错: {str(e)}")
+ headers.append(f"列{j+1}")
+
+ # 处理数据行
+ for i in range(1, rows):
+ try:
+ row_data = []
+ for j in range(cols):
+ try:
+ text = table.cell(i, j).text.strip()
+ row_data.append(f"{headers[j]}:{text}")
+ except Exception as e:
+ print(f"简单表格处理: 处理数据单元格 [{i},{j}] 时出错: {str(e)}")
+ row_data.append(f"{headers[j]}:")
+
+ # 确保行中至少有一个非空值
+ if any(len(data.split(':', 1)) > 1 and data.split(':', 1)[1].strip() for data in row_data):
+ processed_line = " ".join(row_data)
+ print(f"添加简单表格行 {i}: {processed_line[:100]}..." if len(processed_line) > 100 else f"添加简单表格行 {i}: {processed_line}")
+ processed_data.append(processed_line)
+
+ except Exception as e:
+ print(f"简单表格处理: 处理数据行 {i} 时出错: {str(e)}")
+ continue
+
+ # 返回处理后的表格文本
+ if processed_data:
+ final_text = " ".join(processed_data)
+ print(f"表格处理完成,生成 {len(processed_data)} 行数据")
+ print(f"表格文本示例: {final_text[:200]}..." if len(final_text) > 200 else f"表格文本: {final_text}")
+ return final_text
+ else:
+ print("表格无有效数据")
+ return "【表格无有效数据】"
+
+ except Exception as e:
+ print(f"表格处理失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return "【表格处理失败】"
+
+ def _extract_table_text(self, table: Table) -> str:
+ """
+ 提取表格中的文本内容,现在会返回格式化的文本表示
+
+ Args:
+ table: docx表格对象
+
+ Returns:
+ str: 表格内容的文本表示
+ """
+ return self._convert_table_to_text(table)
+
+def process_directory(input_dir: str, output_dir: str = None):
+ """
+ 处理指定目录下的所有文档文件
+
+ Args:
+ input_dir: 输入目录路径
+ output_dir: 输出目录路径,如果为None则使用输入目录
+ """
+ # 如果未指定输出目录,使用输入目录
+ if output_dir is None:
+ output_dir = input_dir
+
+ if not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+
+ cleaner = DocCleaner()
+
+ for root, _, files in os.walk(input_dir):
+ for file in files:
+ if file.endswith(('.doc', '.docx')):
+ input_path = os.path.join(root, file)
+
+ try:
+ # 清理文档
+ main_content, appendix, tables = cleaner.clean_doc(input_path)
+
+ # 创建输出文件名(统一使用docx扩展名)
+ base_name = os.path.splitext(file)[0]
+ output_path = os.path.join(output_dir, f"{base_name}_cleaned.docx")
+
+ # 保存为docx格式
+ cleaner.save_as_docx(main_content, appendix, tables, output_path)
+
+ except Exception as e:
+ print(f"处理文件 {file} 时出错: {str(e)}")
+ # 添加更详细的错误信息
+ if isinstance(e, subprocess.CalledProcessError):
+ print(f"命令执行错误: {e.output}")
+ elif isinstance(e, FileNotFoundError):
+ print("请确保已安装LibreOffice并将其添加到系统PATH中")
+
+def qn(tag: str) -> str:
+ """
+ 将标签转换为带命名空间的格式
+
+ Args:
+ tag: 原始标签
+
+ Returns:
+ str: 带命名空间的标签
+ """
+ prefix = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
+ return prefix + tag
+
+if __name__ == '__main__':
+ import argparse
+
+ # parser = argparse.ArgumentParser(description='文档清理工具')
+ # parser.add_argument('input_dir', help='输入目录路径')
+ # parser.add_argument('--output_dir', help='输出目录路径(可选,默认为输入目录)', default=None)
+ #
+ # args = parser.parse_args()
+
+ process_directory("D:\\rzData\\poject\\AI项目\\UDI智能体\\测试文档", "D:\\rzData\\poject\\AI项目\\UDI智能体\\测试文档")
+
+ # 确保目录存在,如果不存在则创建
+ # 创建基础目录(使用更安全的方式)
+ # base_dir = 'D:\Desktop\DEMO'
+ # text_dir = os.path.join(base_dir, "测试")
+ #
+ # os.makedirs(text_dir, exist_ok=True, mode=0o777)
+ #
+ # print(f"目录是否存在: {os.path.exists(text_dir)}")
+ # print(f"完整路径: {os.path.abspath(text_dir)}") # 或者直接 print(f"完整路径: {text_dir}")
\ No newline at end of file
diff --git a/table/table_to_html.py b/table/table_to_html.py
new file mode 100644
index 0000000..f5a64ac
--- /dev/null
+++ b/table/table_to_html.py
@@ -0,0 +1,444 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import os
+import docx
+import re
+from docx.table import Table, _Cell
+from docx.oxml import parse_xml
+from docx.oxml.ns import nsdecls
+from typing import List, Dict, Tuple, Optional, Union
+import uuid
+from bs4 import BeautifulSoup
+import html
+
+class TableToHtml:
+ def __init__(self, debug: bool = False):
+ """
+ 初始化表格到HTML转换器
+
+ Args:
+ debug: 是否启用调试模式,输出更多日志信息
+ """
+ self.debug = debug
+ # 为每个表格生成唯一ID
+ self.table_id = f"table_{uuid.uuid4().hex[:8]}"
+
+ def _log(self, message: str):
+ """
+ 输出调试日志
+
+ Args:
+ message: 日志消息
+ """
+ if self.debug:
+ print(f"[TableToHtml] {message}")
+
+ def _get_vmerge_value(self, cell_element) -> Optional[str]:
+ """
+ 获取单元格的垂直合并属性
+
+ Args:
+ cell_element: 单元格元素
+
+ Returns:
+ str: 垂直合并属性值
+ """
+ vmerge = cell_element.xpath('.//w:vMerge')
+ if vmerge:
+ return vmerge[0].get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val', 'continue')
+ return None
+
+ def _get_gridspan_value(self, cell_element) -> int:
+ """
+ 获取单元格的水平合并数量
+
+ Args:
+ cell_element: 单元格元素
+
+ Returns:
+ int: 水平合并的列数
+ """
+ try:
+ gridspan = cell_element.xpath('.//w:gridSpan')
+ if gridspan and gridspan[0].get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val'):
+ return int(gridspan[0].get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val'))
+ except (ValueError, TypeError, AttributeError) as e:
+ self._log(f"警告:获取gridspan值时出错: {str(e)}")
+ return 1 # 默认返回1,表示没有合并
+
+ def _get_cell_content(self, cell: _Cell) -> str:
+ """
+ 获取单元格的文本内容,并处理HTML特殊字符
+
+ Args:
+ cell: docx表格单元格对象
+
+ Returns:
+ str: 处理后的HTML内容
+ """
+ content = cell.text.strip()
+ # 转义HTML特殊字符
+ content = html.escape(content)
+ # 处理换行
+ content = content.replace('\n', '
')
+ return content
+
+ def _analyze_table_structure(self, table: Table) -> Dict:
+ """
+ 分析表格结构,包括合并单元格信息
+
+ Args:
+ table: docx表格对象
+
+ Returns:
+ Dict: 表格结构信息
+ """
+ rows = len(table.rows)
+ cols = len(table.columns)
+
+ # 存储合并单元格信息
+ merged_cells = {}
+ # 存储垂直合并的源单元格
+ vmerge_sources = {}
+
+ # 分析合并单元格
+ for i in range(rows):
+ for j in range(cols):
+ try:
+ cell = table.cell(i, j)
+
+ # 检查垂直合并
+ if cell._element.tcPr is not None:
+ vmerge = cell._element.tcPr.xpath('.//w:vMerge')
+ if vmerge:
+ val = self._get_vmerge_value(cell._element)
+ if val == 'restart':
+ # 这是垂直合并的起始单元格
+ # 计算合并的行数
+ rowspan = 1
+ for k in range(i+1, rows):
+ next_cell = table.cell(k, j)
+ if self._get_vmerge_value(next_cell._element) == 'continue':
+ rowspan += 1
+ # 标记此单元格为被合并
+ merged_cells[(k, j)] = {'merged': True, 'source': (i, j)}
+ else:
+ break
+
+ # 记录合并信息
+ vmerge_sources[(i, j)] = {'rowspan': rowspan}
+ elif val == 'continue':
+ # 这是被合并的单元格,稍后处理
+ pass
+
+ # 检查水平合并
+ if cell._element.tcPr is not None:
+ gridspan = self._get_gridspan_value(cell._element)
+ if gridspan > 1:
+ # 记录colspan
+ merged_cells[(i, j)] = {'colspan': gridspan}
+
+ # 标记被合并的单元格
+ for k in range(1, gridspan):
+ if j + k < cols:
+ merged_cells[(i, j+k)] = {'merged': True, 'source': (i, j)}
+
+ except Exception as e:
+ self._log(f"警告:分析单元格 [{i},{j}] 时出错: {str(e)}")
+ continue
+
+ # 将垂直合并信息合并到主合并字典
+ for pos, info in vmerge_sources.items():
+ if pos in merged_cells:
+ merged_cells[pos].update(info)
+ else:
+ merged_cells[pos] = info
+
+ return {
+ 'rows': rows,
+ 'cols': cols,
+ 'merged_cells': merged_cells
+ }
+
+ def _is_header_row(self, row_idx: int, table: Table, structure: Dict) -> bool:
+ """
+ 判断是否为表头行
+
+ Args:
+ row_idx: 行索引
+ table: 表格对象
+ structure: 表格结构信息
+
+ Returns:
+ bool: 是否为表头行
+ """
+ # 简单策略:第一行通常是表头
+ if row_idx == 0:
+ return True
+
+ # 检查是否有垂直合并从第一行开始的单元格
+ for j in range(structure['cols']):
+ cell_pos = (row_idx, j)
+ if cell_pos in structure['merged_cells'] and 'merged' in structure['merged_cells'][cell_pos]:
+ source = structure['merged_cells'][cell_pos]['source']
+ if source[0] == 0: # 合并源是第一行
+ return True
+
+ return False
+
+ def _detect_table_headers(self, table: Table, structure: Dict) -> List[int]:
+ """
+ 检测表格表头行
+
+ Args:
+ table: 表格对象
+ structure: 表格结构信息
+
+ Returns:
+ List[int]: 表头行索引列表
+ """
+ header_rows = []
+ rows = structure['rows']
+
+ # 检查前3行或所有行(如果行数少于3)
+ for i in range(min(3, rows)):
+ if self._is_header_row(i, table, structure):
+ header_rows.append(i)
+
+ # 如果没有检测到表头,默认第一行为表头
+ if not header_rows and rows > 0:
+ header_rows = [0]
+
+ self._log(f"检测到的表头行: {header_rows}")
+ return header_rows
+
+ def table_to_html(self, table: Table) -> str:
+ """
+ 将docx表格转换为HTML格式
+
+ Args:
+ table: docx表格对象
+
+ Returns:
+ str: HTML表格代码
+ """
+ try:
+ # 分析表格结构
+ structure = self._analyze_table_structure(table)
+ rows = structure['rows']
+ cols = structure['cols']
+ merged_cells = structure['merged_cells']
+
+ self._log(f"表格结构: {rows}行 x {cols}列,合并单元格: {len(merged_cells)}")
+
+ # 检测表头
+ header_rows = self._detect_table_headers(table, structure)
+
+ # 构建HTML表格
+ soup = BeautifulSoup('', 'html.parser')
+ table_tag = soup.table
+ table_tag['class'] = ['docx-table']
+ table_tag['id'] = self.table_id
+
+ # 添加表头部分(thead)
+ if header_rows:
+ thead = soup.new_tag('thead')
+ table_tag.append(thead)
+
+ for i in header_rows:
+ if i >= rows:
+ continue
+
+ tr = soup.new_tag('tr')
+ thead.append(tr)
+
+ j = 0
+ while j < cols:
+ cell_pos = (i, j)
+
+ # 检查是否被合并
+ if cell_pos in merged_cells and 'merged' in merged_cells[cell_pos]:
+ j += 1
+ continue
+
+ # 创建th元素
+ th = soup.new_tag('th')
+
+ # 处理合并
+ if cell_pos in merged_cells:
+ if 'rowspan' in merged_cells[cell_pos]:
+ th['rowspan'] = merged_cells[cell_pos]['rowspan']
+ if 'colspan' in merged_cells[cell_pos]:
+ th['colspan'] = merged_cells[cell_pos]['colspan']
+ j += merged_cells[cell_pos]['colspan'] - 1
+
+ # 设置单元格内容
+ cell = table.cell(i, j)
+ content = self._get_cell_content(cell)
+ th.string = content
+
+ tr.append(th)
+ j += 1
+
+ # 添加表格主体(tbody)
+ tbody = soup.new_tag('tbody')
+ table_tag.append(tbody)
+
+ # 计算数据行的起始索引
+ data_start = max(header_rows) + 1 if header_rows else 0
+
+ # 处理数据行
+ for i in range(data_start, rows):
+ tr = soup.new_tag('tr')
+ tbody.append(tr)
+
+ j = 0
+ while j < cols:
+ cell_pos = (i, j)
+
+ # 检查是否被合并
+ if cell_pos in merged_cells and 'merged' in merged_cells[cell_pos]:
+ j += 1
+ continue
+
+ # 创建td元素
+ td = soup.new_tag('td')
+
+ # 处理合并
+ if cell_pos in merged_cells:
+ if 'rowspan' in merged_cells[cell_pos]:
+ td['rowspan'] = merged_cells[cell_pos]['rowspan']
+ if 'colspan' in merged_cells[cell_pos]:
+ td['colspan'] = merged_cells[cell_pos]['colspan']
+ j += merged_cells[cell_pos]['colspan'] - 1
+
+ # 设置单元格内容
+ cell = table.cell(i, j)
+ content = self._get_cell_content(cell)
+ td.string = content
+
+ tr.append(td)
+ j += 1
+
+ # 添加基本的CSS样式
+ style = soup.new_tag('style')
+ style.string = f'''
+ #{self.table_id} {{
+ border-collapse: collapse;
+ width: 100%;
+ margin-bottom: 1em;
+ font-family: Arial, sans-serif;
+ }}
+ #{self.table_id} th, #{self.table_id} td {{
+ border: 1px solid #ddd;
+ padding: 8px;
+ text-align: left;
+ }}
+ #{self.table_id} th {{
+ background-color: #f2f2f2;
+ font-weight: bold;
+ }}
+ #{self.table_id} tr:nth-child(even) {{
+ background-color: #f9f9f9;
+ }}
+ #{self.table_id} tr:hover {{
+ background-color: #f5f5f5;
+ }}
+ '''
+
+ # 返回完整的HTML代码
+ html_code = str(style) + str(table_tag)
+ return html_code
+
+ except Exception as e:
+ self._log(f"转换表格到HTML时出错: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return f"表格处理失败: {str(e)}
"
+
+ def process_document_tables(self, doc_path: str) -> List[str]:
+ """
+ 处理文档中的所有表格并转换为HTML
+
+ Args:
+ doc_path: 文档文件路径
+
+ Returns:
+ List[str]: HTML表格代码列表
+ """
+ try:
+ # 打开文档
+ doc = docx.Document(doc_path)
+ html_tables = []
+
+ # 处理所有表格
+ for i, table in enumerate(doc.tables):
+ self._log(f"处理第 {i+1} 个表格")
+ self.table_id = f"table_{uuid.uuid4().hex[:8]}" # 为每个表格生成唯一ID
+ html_code = self.table_to_html(table)
+ html_tables.append(html_code)
+
+ return html_tables
+
+ except Exception as e:
+ self._log(f"处理文档表格时出错: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return [f"文档处理失败: {str(e)}
"]
+
+def convert_tables_to_html(doc_path: str, output_path: str = None, debug: bool = False):
+ """
+ 将文档中的表格转换为HTML并保存
+
+ Args:
+ doc_path: 文档文件路径
+ output_path: 输出HTML文件路径,如果为None则使用原文件名+.html
+ debug: 是否启用调试模式
+
+ Returns:
+ str: 输出文件路径
+ """
+ if output_path is None:
+ # 创建默认输出路径
+ base_name = os.path.splitext(doc_path)[0]
+ output_path = f"{base_name}_tables.html"
+
+ converter = TableToHtml(debug=debug)
+ html_tables = converter.process_document_tables(doc_path)
+
+ # 创建完整HTML文档
+ html_content = f'''
+
+
+
+ 表格预览
+
+
+
+ 文档中的表格
+ {' '.join(html_tables)}
+
+'''
+
+ # 保存HTML文件
+ with open(output_path, 'w', encoding='utf-8') as f:
+ f.write(html_content)
+
+ if debug:
+ print(f"HTML文件已保存到: {output_path}")
+
+ return output_path
+
+if __name__ == '__main__':
+ import argparse
+
+ parser = argparse.ArgumentParser(description='将Word文档中的表格转换为HTML')
+ parser.add_argument('input_file', help='输入文档文件路径')
+ parser.add_argument('-o', '--output', help='输出HTML文件路径', default=None)
+ parser.add_argument('-d', '--debug', action='store_true', help='启用调试模式')
+
+ args = parser.parse_args()
+
+ result_path = convert_tables_to_html(args.input_file, args.output, args.debug)
+ print(f"表格已转换为HTML,文件路径: {result_path}")
\ No newline at end of file