Week 3 Development Summary: - Implement negative sign normalization (6 Unicode variants) - Enhance T-test validation with smart sample size extraction - Enhance SE triangle and CI-P consistency validation with subrow support - Add precise sub-cell highlighting for P-values in multi-line cells - Add frontend issue type Chinese translations (6 new types) - Add file format tips for PDF/DOC uploads Technical improvements: - Add _clean_statistical_text() in extractor.py - Add _safe_float() wrapper in validator.py - Add ForensicsReport.tsx component - Update ISSUE_TYPE_LABELS translations Documentation: - Add 2026-02-18 development record - Update RVW module status (v5.1) - Update system status (v5.2) Status: Week 3 complete, ready for Week 4 testing Co-authored-by: Cursor <cursoragent@cursor.com>
137 lines
6.4 KiB
Markdown
137 lines
6.4 KiB
Markdown
# **RVW V2.0 表格提取疑难杂症专项解决方案**
|
||
|
||
**问题焦点:** Word 表格“假行”现象(单元格内多段落)导致的提取错位
|
||
|
||
**核心策略:** 从“视觉模型”回归“DOM 深度解析”
|
||
|
||
**技术栈:** Python (python-docx)
|
||
|
||
## **1\. 核心判断:为什么不建议全量上视觉模型?**
|
||
|
||
您提到用视觉模型(Vision Model,如 GPT-4V, Qwen-VL)来识别,这听起来很诱人(所见即所得),但在**数据侦探**场景下有致命缺陷:
|
||
|
||
| 维度 | 视觉模型 (VLM/OCR) | 原生解析 (python-docx) | 结论 |
|
||
| :---- | :---- | :---- | :---- |
|
||
| **数值准确性** | **95%\~99%** (存在幻觉风险) | **100%** (直接读取 XML) | ❌ 审计场景不能有 1% 的误差 |
|
||
| **小数点敏感度** | 可能漏读小数点 (0.05 \-\> 005\) | 绝对精准 | ❌ P 值验证的核心 |
|
||
| **对齐能力** | 强 (能看懂视觉对齐) | 弱 (需算法辅助) | ✅ 视觉模型优势 |
|
||
| **成本/速度** | 高/慢 (需 GPU 推理) | 极低/极快 (CPU 解析) | ❌ 影响并发性能 |
|
||
|
||
**决策:**
|
||
|
||
**“数据”必须信赖 XML(代码),“结构”可以用算法还原。** 我们不需要视觉模型来看数字,我们只需要一段更聪明的 Python 代码来拆解段落。
|
||
|
||
## **2\. 现象诊断:什么是“隐性多行”?**
|
||
|
||
在您的截图中,Word 表格的一行(Row)内部,用户使用了 **回车键 (Enter)** 或 **软回车 (Shift+Enter)** 进行了换行。
|
||
|
||
**python-docx 的默认行为:**
|
||
|
||
cell.text 会把这些段落拼接成一个字符串,例如 "DNT时间段\\n\<45 min\\n45\~60 min"。前端 HTML 渲染时,如果没有处理 \\n,或者对应列的行数不匹配,就会导致错位。
|
||
|
||
## **3\. 解决方案:行分裂算法 (Row Explosion)**
|
||
|
||
我们需要在提取阶段,检测这种情况,并将“逻辑上的一行”分裂成“视觉上的多行”。
|
||
|
||
### **3.1 算法逻辑**
|
||
|
||
1. **扫描 (Scan)**:遍历表格的每一行。
|
||
2. **检测 (Detect)**:检查该行每一列的 **段落数量 (Paragraph Count)**。
|
||
* 例如:Col 1 有 4 个段落,Col 2 有 4 个段落,Col 3 只有 1 个段落(如 P 值)。
|
||
3. **分裂 (Explode)**:
|
||
* 取最大段落数 max\_para (如 4)。
|
||
* 如果 max\_para \> 1,则将此行**分裂**为 4 个新行。
|
||
4. **填充 (Fill)**:
|
||
* 对于原本有多段落的列:按顺序填充到新行。
|
||
* 对于只有 1 个段落的列(如 P 值 0.001):
|
||
* *策略 A(重复)*:每行都填 0.001。
|
||
* *策略 B(首行/合并)*:只填第一行,后面留空(前端处理为合并单元格)。
|
||
|
||
### **3.2 代码实现 Demo**
|
||
|
||
请让 Python 工程师在 DocxTableExtractor 中加入以下逻辑:
|
||
|
||
from docx import Document
|
||
import pandas as pd
|
||
|
||
def explode\_word\_table\_rows(table):
|
||
"""
|
||
高级表格提取:处理单元格内的多段落(隐性多行)
|
||
"""
|
||
structured\_data \= \[\]
|
||
|
||
for row in table.rows:
|
||
\# 1\. 获取该行每一列的段落内容列表
|
||
\# cells\_content 结构: \[ \['DNT时间段', '\<45min', ...\], \['1299', '881', ...\], \['X2=..'\] \]
|
||
cells\_content \= \[\]
|
||
for cell in row.cells:
|
||
\# 过滤掉空段落,获取真实文本行
|
||
paras \= \[p.text.strip() for p in cell.paragraphs if p.text.strip()\]
|
||
if not paras:
|
||
paras \= \[""\] \# 保持占位
|
||
cells\_content.append(paras)
|
||
|
||
\# 2\. 计算该行“分裂”的最大高度
|
||
max\_height \= max(len(c) for c in cells\_content)
|
||
|
||
\# 3\. 如果是标准单行,直接添加
|
||
if max\_height \<= 1:
|
||
flat\_row \= \[c\[0\] if c else "" for c in cells\_content\]
|
||
structured\_data.append(flat\_row)
|
||
continue
|
||
|
||
\# 4\. 执行分裂 (Row Explosion)
|
||
\# 针对每一层(visual\_row\_index),构建一行数据
|
||
for i in range(max\_height):
|
||
new\_row \= \[\]
|
||
for col\_idx, cell\_paras in enumerate(cells\_content):
|
||
\# 策略:如何填充?
|
||
if len(cell\_paras) \> 1:
|
||
\# 情况 A:该列有多行,按顺序取
|
||
\# 如果当前层级超过了该列的行数,填空(或填最后一行)
|
||
val \= cell\_paras\[i\] if i \< len(cell\_paras) else ""
|
||
else:
|
||
\# 情况 B:该列只有一行(通常是统计值 P值)
|
||
\# 只有第一行填值,模拟“合并单元格”的视觉效果
|
||
\# 或者:val \= cell\_paras\[0\] (全部重复填充) \-\> 方便后续计算
|
||
val \= cell\_paras\[0\] if i \== 0 else ""
|
||
|
||
new\_row.append(val)
|
||
structured\_data.append(new\_row)
|
||
|
||
return pd.DataFrame(structured\_data)
|
||
|
||
\# 使用示例
|
||
\# doc \= Document("sample.docx")
|
||
\# df \= explode\_word\_table\_rows(doc.tables\[0\])
|
||
\# print(df)
|
||
|
||
## **4\. 前端渲染的配合**
|
||
|
||
为了让“数据侦探”的高亮定位准确,后端返回的数据结构必须包含**分裂后的坐标映射**。
|
||
|
||
**推荐的数据结构升级:**
|
||
|
||
{
|
||
"row\_id": "r4\_exploded\_0", // 原始第4行,分裂后的第0子行
|
||
"is\_virtual": true, // 标记这是分裂出来的行
|
||
"cells": \[
|
||
{ "text": "\<45 min", "source\_cell": "R4C1", "paragraph\_index": 1 },
|
||
{ "text": "881 (46.59)", "source\_cell": "R4C2", "paragraph\_index": 1 },
|
||
{ "text": "", "source\_cell": "R4C3", "is\_merged\_placeholder": true } // P值列留空
|
||
\]
|
||
}
|
||
|
||
**前端展示逻辑:**
|
||
|
||
* 当后端返回 is\_merged\_placeholder: true 时,前端渲染时不显示内容,或者通过 CSS 渲染为合并单元格的样式(即不画上边框)。
|
||
|
||
## **5\. 总结**
|
||
|
||
1. **别用视觉模型**:准确率风险太大,得不偿失。
|
||
2. **用代码“分裂”段落**:Word 的 cell.paragraphs 是您的救星。
|
||
3. **对齐策略**:通常临床表格中,如果一列有多行,另一列只有一行(如 P 值),那一行 P 值通常是对齐第一行或者居中的。在做\*\*数据验证(L1/L2)\*\*时,我们需要编写逻辑:*“如果检测到分裂行,且 P 值列为空,自动向上寻找最近的一个 P 值作为本行的验证依据。”*
|
||
|
||
**实施建议:**
|
||
|
||
请 Python 工程师立即测试上述 explode\_word\_table\_rows 逻辑。这能解决您 90% 的“HTML 只有一行”的问题。 |