feat(rvw): Complete V2.0 Week 3 - Statistical validation extension and UX improvements

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>
This commit is contained in:
2026-02-18 18:26:16 +08:00
parent 9f256c4a02
commit f9ed0c2528
36 changed files with 2790 additions and 501 deletions

View File

@@ -0,0 +1,184 @@
# RVW V2.0 开发记录 - 2026-02-18
> **日期:** 2026-02-18
> **阶段:** Week 3 - 统计验证扩展与用户体验优化
> **开发者:** AI Assistant
> **状态:** ✅ 完成
---
## 📋 今日完成内容
### 1. 负号归一化功能 ✅
**问题背景:**
- Word 文档中的负号可能是多种 Unicode 字符(数学减号 `\u2212`、En Dash `\u2013`、Em Dash `\u2014` 等)
- Python 的 `float()` 无法解析这些特殊字符,导致验证失败
**实现内容:**
| 文件 | 修改 |
|------|------|
| `extraction_service/forensics/extractor.py` | 新增 `_clean_statistical_text()` 方法,在提取单元格时自动清洗 |
| `extraction_service/forensics/validator.py` | 新增 `_clean_number_string()``_safe_float()` 辅助函数 |
**覆盖的特殊字符:**
| Unicode | 字符 | 名称 | 清洗为 |
|---------|------|------|--------|
| `\u2212` | | 数学减号 | `-` |
| `\u2013` | | En Dash | `-` |
| `\u2014` | — | Em Dash | `-` |
| `\u2264` | ≤ | 小于等于 | `<=` |
| `\u2265` | ≥ | 大于等于 | `>=` |
| `\u00d7` | × | 乘号 | `x` |
| `\u200b` | | Zero-Width Space | (删除) |
---
### 2. 统计验证方法扩展 ✅
#### 2.1 T 检验验证增强
**改进点:**
- 智能样本量提取:支持 `(n=50)``n=50``(50例)` 等多种格式
- 新增 `_extract_sample_sizes_from_header()``_extract_sample_sizes_from_row()` 方法
- 支持括号格式的 SD`45.2 (12.3)`
- 支持多行单元格 subrow 精确高亮
#### 2.2 SE 三角验证增强
**改进点:**
- 支持多行单元格的 subrow 精确定位
- 遍历 P 值列每一行,分别验证
- 显示友好的行描述(如变量名)
#### 2.3 CI vs P 值一致性验证增强
**改进点:**
- 支持多行单元格 subrow 精确定位
- 支持多个 CI/P 值对的验证
- 使用 `_parse_pvalue_flexible` 灵活解析
---
### 3. 前端翻译映射更新 ✅
**文件:** `frontend-v2/src/modules/rvw/components/ForensicsReport.tsx`
新增/完善的问题类型中文翻译:
| 代码 | 中文描述 |
|------|----------|
| `ARITHMETIC_TOTAL` | 总计行错误 |
| `STAT_CI_PVALUE_CONFLICT` | CI 与 P 值矛盾 |
| `STAT_SD_GREATER_MEAN` | SD 大于均值 |
| `STAT_REGRESSION_CI_P` | 回归 CI-P 不一致 |
| `EXTRACTION_WARNING` | 提取警告 |
| `TABLE_SKIPPED` | 表格跳过 |
---
### 4. 文件格式提示功能 ✅
**用户反馈:** 上传 PDF 文件后没有数据验证 Tab需要提示用户
**实现内容:**
| 文件 | 修改 |
|------|------|
| `Header.tsx` | 上传按钮下方添加蓝色提示框,推荐 .docx 格式 |
| `ReportDetail.tsx` | 非 docx 文件时显示黄色警告,解释为什么没有数据验证 |
| `TaskDetail.tsx` | 同上 |
**提示内容:**
- **上传时:** "推荐上传 .docx 格式文件可获得完整的数据验证功能。PDF 和 .doc 格式仅支持稿约和方法学评审。"
- **查看报告时:** "当前文件为 PDF/.doc 格式,无法进行数据验证。如需数据验证功能,请上传 .docx 格式文件。"
---
## 📊 当前统计验证能力总览
| 验证类型 | 方法 | 状态 |
|----------|------|------|
| **L1 算术** | 百分比 n(%) | ✅ |
| **L1 算术** | Sum/Total 校验 | ✅ |
| **L2 统计** | 卡方检验 P 值逆向验证 | ✅ + subrow |
| **L2 统计** | T 检验 P 值逆向验证 | ✅ + subrow |
| **L2 统计** | CI vs P 值逻辑一致性 | ✅ + subrow |
| **L2.5 取证** | SE 三角验证 | ✅ + subrow |
| **L2.5 取证** | SD > Mean 检查 | ✅ |
---
## 📁 修改的文件清单
### Python 后端
- `extraction_service/forensics/extractor.py` - 负号归一化
- `extraction_service/forensics/validator.py` - 统计验证扩展
### Node.js 后端
- (无修改)
### 前端
- `frontend-v2/src/modules/rvw/components/ForensicsReport.tsx` - 翻译映射
- `frontend-v2/src/modules/rvw/components/Header.tsx` - 上传提示
- `frontend-v2/src/modules/rvw/components/ReportDetail.tsx` - 格式提示
- `frontend-v2/src/modules/rvw/components/TaskDetail.tsx` - 格式提示
---
## 📋 待完成工作
### V2.0 MVP 剩余任务
| 任务 | 优先级 | 状态 |
|------|--------|------|
| Week 4 功能测试 | P0 | 📋 待开始 |
| Week 4 性能测试 | P1 | 📋 待开始 |
| Week 4 Bug 修复 | P0 | 📋 待开始 |
| Week 4 文档更新 | P1 | 📋 待开始 |
### V2.1 待开发功能
| 功能 | 说明 |
|------|------|
| ANOVA 验证 | 多组比较 P 值验证 |
| 配对 T 检验 | 配对样本验证 |
| 非参数检验 | Mann-Whitney, Wilcoxon |
| .doc 格式支持 | 评估 Pandoc 替代方案 |
| Profile 管理 UI | 期刊配置界面 |
---
## 💡 技术要点
### 负号归一化的重要性
```python
# 未清洗时 float() 会崩溃
float('1.5') # ValueError: could not convert string to float
# 清洗后正常工作
float('-1.5') # -1.5
```
### Subrow 高亮原理
Word 表格中一个单元格可能包含多行数据(用换行符分隔),例如:
```
| 变量 | P值 |
|------|-----|
| 年龄 | 0.82
性别 0.01 <- 问题在这里
BMI 0.95 |
```
通过 `data-subcoord="R2C2S2"` 属性可以精确定位到第 2 行第 2 列的第 2 个子行。
---
**文档版本:** v1.0
**创建日期:** 2026-02-18
**下次更新:** Week 4 测试完成后

View File

@@ -0,0 +1,137 @@
# **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 只有一行”的问题。

View File

@@ -0,0 +1,201 @@
# **临床统计特殊符号提取白皮书**
**用途:** 指导 Python (python-docx) 在提取 Word 表格时进行字符清洗和标准化。
**核心痛点:** 同一个数学含义,可能由多种不同的编码方式表示。
## **1\. 希腊字母类 (Greek Letters)**
这是最容易出现乱码或识别错误的重灾区。
|
| **符号** | **含义** | **常见 Unicode** | **Word 中的潜在坑 (Legacy Fonts)** | **处理建议** |
| ![][image1] | **卡方检验** | \\u03c7 (χ) \+ \\u00b2 (²) | 1\. 字体设为 "Symbol" 的 'c' 2\. 公式编辑器对象 | **正则匹配**\[\\u03c7\\u03a7\]2? **关键词**chi-square, chi |
| ![][image2] | 显著性水平 | \\u03b1 | 字体设为 "Symbol" 的 'a' | 替换为 alpha |
| ![][image3] | 回归系数/功效 | \\u03b2 | 字体设为 "Symbol" 的 'b' | 替换为 beta |
| ![][image4] | 总体均值 | \\u03bc | 字体设为 "Symbol" 的 'm' | 替换为 u 或 mean |
| ![][image5] | 总体标准差 | \\u03c3 | 字体设为 "Symbol" 的 's' | 替换为 std |
| ![][image6] | 变化量/差值 | \\u0394 (大写) | 字体设为 "Symbol" 的 'D' | 替换为 delta |
| ![][image7] | 相关系数 | \\u03c1 | 字体设为 "Symbol" 的 'r' | 替换为 rho |
**⚠️ 提取陷阱:** 很多老旧的 Word 文档(特别是中文期刊投稿)喜欢用 **Symbol 字体**。在 python-docx 提取 text 时,你可能会读到一个普通的英文字母 c但用户看到的是 ![][image8]。
* **解决方案**:检查 run.font.name。如果字体是 Symbol需要建立映射表c \-\> χ, a \-\> α)。
## **2\. 数学运算符类 (Operators)**
| **符号** | **含义** | **常见 Unicode** | **Word 变体** | **处理建议** |
| ![][image9] | **加减/标准差** | \\u00b1 | \+/-, \+ / \- | 统一标准化为 \\u00b1 |
| ![][image10] | 小于等于 | \\u2264 | \<=, \=\< | 统一为 \<= |
| ![][image11] | 大于等于 | \\u2265 | \>= | 统一为 \>= |
| ![][image12] | 不等于 | \\u2260 | \!=, \<\>, /= | 统一为 \!= |
| ![][image13] | 约等于 | \\u2248 | \~, \= | 统一为 \~= |
| ![][image14] | **负号/减号** | \\u2212 (Minus) | \\u002d (Hyphen), \\u2013 (En Dash) | **极高危!** 必须统一替换为标准连字符 \- (\\u002d),否则 float() 转换会报错 |
| ![][image15] | 乘号/交互项 | \\u00d7 | x, X, \* | 统一为 x |
**⚠️ 提取陷阱:** **“负号”是数据清洗中最大的坑**。Word 会自动把连字符Hyphen转成破折号Dash或数学减号Minus
* python 代码value.replace('\\u2212', '-').replace('\\u2013', '-')
## **3\. 统计学专用标记 (Statistical Notations)**
| **符号** | **含义** | **形式** | **提取难点** |
| ![][image16] | **样本均值** | x 上加横线 | 通常是 **Word 公式对象 (OMML)****域代码 (EQ)**python-docx 的 .text **读不出来横线**,只能读到 x。 |
| ![][image17] | 样本率 | p 上加尖帽 | 同上。 |
| ![][image18] | 决定系数 | R \+ 上标 2 | python-docx 默认读成 R2。**这通常可以接受**。 |
| ![][image19] | 下标 (如 ![][image20]) | 文本 \+ 下标 | python-docx 默认读成 Xsub。需要识别 font.subscript 属性。 |
**⚠️ 提取陷阱:** 对于 ![][image16] 这种带修饰符的字符python-docx 可能只能提取到底座字符 x。
* **策略**:对于数据侦探来说,通常我们关注的是表头里的 Mean 或 Average 关键词,而不是符号。如果表头只有 ![][image16],可能需要结合上下文推断。
## **4\. 拉丁字母的特殊含义 (Latin Context)**
虽然是普通字母,但在统计学上下文中具有特殊含义,通常以**斜体 (Italic)** 出现。
| **符号** | **含义** | **易混淆点** |
| ![][image21] | t 检验统计量 | 容易混淆为时间单位 t (time) 或 吨 (ton) |
| ![][image22] | F 检验统计量 | 女性 (Female) |
| ![][image23] | Z 检验统计量 | \- |
| ![][image24] | P 值 (概率) | 磷 (Phosphorus) |
| ![][image25] | 样本量 | 牛顿 (Newton) |
| ![][image26] | 相关系数 | 半径 (radius) |
| ![][image27] | 回归系数 | \- |
| ![][image28] | 优势比 | 手术室 (Operating Room), 或者 (or) |
| ![][image29] | 风险比 | 心率 (Heart Rate) |
| ![][image30] | 置信区间 | 心脏指数 (Cardiac Index) |
**⚠️ 提取策略:** 不能只看字符,要看**组合**。
* P 单独出现且数值在 0-1 之间 \-\> P 值。
* t 单独出现且数值 \> 0 \-\> t 值。
* CI 后面跟着括号 (1.2-3.4) \-\> 置信区间。
## **5\. Python 字符串清洗工具箱 (Cleaner Utils)**
建议在 DocxTableExtractor 中集成以下清洗函数:
import re
def clean\_statistical\_text(text):
if not text:
return ""
\# 1\. 归一化负号 (CRITICAL)
text \= text.replace('\\u2212', '-').replace('\\u2013', '-').replace('\\u2014', '-')
\# 2\. 归一化卡方 (Chi-square)
\# 处理 Symbol 字体的 'c'2 (需配合 run.font 检查,此处仅处理 Unicode)
text \= text.replace('\\u03c72', 'chi-square')
text \= text.replace('\\u03c7\\u00b2', 'chi-square')
text \= re.sub(r'\[Xxχ\]\\^?2', 'chi-square', text) \# 正则匹配常见变体
\# 3\. 归一化加减号
text \= text.replace('\\u00b1', '+/-')
\# 4\. 归一化比较符
text \= text.replace('≤', '\<=').replace('≥', '\>=')
\# 5\. 去除不可见字符 (Zero-width space 等)
text \= re.sub(r'\[\\u200b\\u200c\\u200d\\ufeff\]', '', text)
return text.strip()
## **6\. 总结**
在 Word 提取中,最大的“鬼怪”不是复杂的 ![][image1],而是:
1. **假的负号**(导致 float() 崩溃)。
2. **Symbol 字体**(导致 ![][image2] 变成 a
3. **多段落换行**(上一节已解决)。
只要处理好这三点99% 的统计表格都能被正确解析。
[image1]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAYAAAD6S912AAABVElEQVR4Xu2Tu0rFQBCGIygICqeKwSL3VCKCBAR9DjsbSwttPKDYaWFnYyt2FoL6CBaWwmlObSW+gLUIR7+R2RAHi2wEKz8YdjLz75+9JEHwTxfSNL0mPjRWbd+LLMuOq6oKJS+KYiCmZVkuWF1nMLgn3lrPkyRJDtuaX6HbXrf1XmA0ZHVPtt6LPM/xS0e23gu9jCv3TL7f7gcse4l44QbPvzVaMGlRxrquZ8jH6Dc19oi6EcrbMLpRoRxwc4MO2Z47K/oPqmsiDMN5O+cL+cZEEMfxsqvp9l7bOh+m9a2P8qCrfrciLzB4FlPSKcaJjFbjBSZb7mxkhbbvTRRFc2p4anu9wGikhr0vogGTW2KDuBNTbn3WajqDwRkG25LzzRW6yl0j64ZMxOzI1MRQbtkP/oI1Jl7YOi/YEVNWe0CekV9azY9guGJrDj3PMX4ntvdnfALEtFcZX4GOowAAAABJRU5ErkJggg==>
[image2]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAZCAYAAAAFbs/PAAAAvUlEQVR4XmNgGAWjgDCQl5dfB8T/gXgWiJaTk1uBrgYOgJJxIEXS0tIyID6QbQXii4qK8kDlJ4iLi3ODFQMlgqGKhZHMAIn/BuITUPYvmDgj1BmnEUohACQGkpOVldVRUFBogAlGQ93rgqocLHcAatgnuCBQ50KQINx9SACmQVFR0QxZsBUkiKQODoDiSzHkZGRkOEGCQFoFWRxo8ymg+BaoBkagk/OQJSWgboXh7SBFIDkg+zVUrAeuYaQBAHSNOPrqSBJoAAAAAElFTkSuQmCC>
[image3]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAYCAYAAADOMhxqAAABD0lEQVR4Xu2QPYrCUBSFI6PgoIVNjJCYl5BCrANWU86AC7BxI4L7cANWNrZiYyG6ATsrl2BpI/hzzuMF8+4kVlPOgUuS7557ct9znH+9lCRJVyl1QG08z2vIvqUoinaosfn8wNDD9/3AMmWCcRrH8SDPMLBFLfNMKwiCTzYlBzsjaC45GzOkewJXuRLO1BZcD1z45L5hGI6Q2gI74X0ivRSTVvhDj4mm7qibNGoh5Qv1LTkGjqiF5GwssUJdch42W9VSIXQ03xf1uP9GQsqcZWVB7q4K7h/8hwOu6zatBhN4G2ma1jKG3TsmvZ/3agFeaebTmFhrtCrSy6T6rx3fifsX3X+pVMn9lwrmoWR/qifuh0EraUB3jQAAAABJRU5ErkJggg==>
[image4]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAXCAYAAAA/ZK6/AAAA0klEQVR4XmNgGAUjEMjLy09TUFAwQBYTFxfnBopLIouBAVAhB1DiPxB7IosD+b+BeCuyGEyiFaQBWUxGRkYaJCYnJ6eELA4GQInnIIwmVoVuCBxAnTMJTewJEL9FFgMDkJUgDYqKivrI4siGAOnDyBJg90tLS8sgiQXDDAHSikBDy5E1gNwPMu00iA8MsQogexZIDKjQFYhvAYUZkTWAFLcCFSoAJUOMjY1ZoVKMQKEAIM0MVwxzP9agwwZg7kcXxwnkIUGHEv54AShJMCB7iAAAALSHNqYCnl/cAAAAAElFTkSuQmCC>
[image5]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAXCAYAAAA/ZK6/AAAAlklEQVR4XmNgGAUjFMjKykrJycmFoGMpKSkRFIXy8vKaQPwfD+6FK1ZQUDgFFPiKpB9kQDRIIbIYGCgqKoqDJIBO0UEWB4oFYdUAFJyETQIothWbOEjiABB/wiIOcvcUdHGQ+xuAEm+RxYChUgYU+4sshgJAkkBFMUAmI5CeDuR/BLHR1aEAoEJtoG0BxsbGrOhyww0AAPUbLAw2jOhAAAAAAElFTkSuQmCC>
[image6]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAXCAYAAAAC9s/ZAAAA2klEQVR4XmNgGAW0BfLy8v/l5OS00cWJAkDNOSADgPg1uhxRAKr5H4iWkZFRQZfHCxQUFDKAuAGII6AGPUFXgxeANCGzoVgRWQ1OALW1GcYHBmIM1IDbyOpwAqjtjOhiIKyoqCiOLI4BgIqCgDZOwCIOi5Hr6HIoAKjgHwOa7TAAc4WysrIYuhwYAG12BSqYgi4OA0D5cpABwDA6hS4HBkDJn0CKBV0cGcBcoaSkxI8uYQnEy1EEsQBQ2oAacgBFAmQ7zHRiMTB1coI1A6NGH12SSLwexRUjGAAAzuRaDctwFcUAAAAASUVORK5CYII=>
[image7]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAYCAYAAADDLGwtAAAAv0lEQVR4XmNgGAVDCMjJyYUA8SMFBYVGdDkwMDY2ZpWXl/8PVOQK4gMVVgL5P9HVMUAVxcD46urqvCAxRUVFfbgioO4GkCBcgAGsUROq2RdZ8B8QH0AoA4tVoZgoLi7ODRKQlZX1Q1P4HMUWkAKQgKioKA9MTEZGhhMkBsTFcIUgK6Em2iKJvQe6+xBcEVQQpBOk+BiU/R+oyB9FEcg6qGko7sMAQEWe6O7DCoCKtoIUootjAKCiJ0A8BV2cbAAA6UU1lUA45VQAAAAASUVORK5CYII=>
[image8]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAYCAYAAADOMhxqAAAA6UlEQVR4XmNgGAUjCMjJyWkB8SMFBYWJ6HIwIC8vLwlmKCkp8QMVrjI2NmYFCv4H4p9oahkUFRXlgQbeQhdnAGpsAGmSlZXVgYmBDASKvUdWhwxYoLacAHGgtv5GV4QCgAoegDQBmYxA+h+IRleDAoCKoqG2/AfZgC6PAcTFxbmhGlrR5bACoMLTUA04PQoHQEWrgdgKiNeANAFDjQNdDRwAFfQAFSSA2MAwV4LakoOmDAJAEkDFlWhiIA2gUEIFwFg0A0rMQhcHGpAB0gS0rQzIVgCy54AlgBr00NTCAdQ/F4DqG9HliAYAE5c3TLQwFisAAAAASUVORK5CYII=>
[image9]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAXCAYAAADUUxW8AAAASUlEQVR4XmNgGAWDACgqKorLycmVo4sTBeTl5SVHNWMB4uLi3ECF/0nBCgoK8ejmoAB5YmzGBYazZmDIcaCHJiEMNDQM3ZyRAgBYPjDZl5qDigAAAABJRU5ErkJggg==>
[image10]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAXCAYAAADUUxW8AAAApUlEQVR4XmNgGAWDGKirq/OiixEE8vLyTkD8X05OLhddDicAakgCaVJQUPBHl8MJgBpqoJrM0eVwAqCz5oM0AbEiuhwuwAhUfByIf0lLSwujS+IFMH+BAgVdjmgA9F8E1J8R6HJEA1AgQaMlH12OaAAKNKh3pqLLEQ1UVFT4gAZ8A+J16HJEAxkZGU6gAVfRxQcIiIqK8gCdI0kMVlZWFqOeZnIBAHeIKM/15BGyAAAAAElFTkSuQmCC>
[image11]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAXCAYAAADUUxW8AAAAuElEQVR4XmNgGAV0BlJSUlzoYkQDRUVFdXl5+f9A3I0uRzSQlpYWBhrwC4i3A7mM6PJEAXFxcW6gAR+B+DyQy4wuTxQwNjZmBRrwEIhfycjIcKLLEwsYgQYcB7kG5Cp0SYJARUWFD6j5GxBvQ5fDCYCKFUExIScnNx9dDicARp8eNPpq0OVwAgUFBX+oTXHocjgBUEMS1CYndDm8AGjLAlAqQxcf5EBUVJQH6FdJYrCysrIY9TSTCwAyxylNVwCyZAAAAABJRU5ErkJggg==>
[image12]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAXCAYAAADUUxW8AAAAwElEQVR4Xu2TLQ7CQBSESwICjUDsPxa5GknCETgCN+AaaIJB4TBcgBNU4TgECUkFwTBFEDKhdAyOLxnzZee9Npstij+/I4RwZScRY5wjM/YS2HpnJ4GNC+/9lL0Ett7YSWDj0jk3YS+BrRW7dzo4sG5IiezZG2MsD2HqoReWEvjXFTJmzzR9dvXBPYOhIx7yAgc2SGKv0EXxzFICxV1Kaci+lZxzD+UTewkUD7jDAftWrLV9lI/sJfBytvUA9t94AERtLzqe3MJWAAAAAElFTkSuQmCC>
[image13]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAXCAYAAADUUxW8AAAAyElEQVR4XmNgGAWjgBSgqKhoJisrq4Mujg6kpKRE4BygBhN5efn/QPwOiv/LycmVIamHAwUFBQ6gfDRcAMh5jyQPE5sGMkRaWloGTfyukpISP5gjKirKA7TZFFkBDMjIyHACFf+CugqMgWr90NVRBoCmHoeZDvTXKXR5ZACUT4dzgBquA3EykmQC1JBIuCIoAAakKzxGoKGXg6YGDEAuABkC1BAD9L8qkL1fHjlwQZqB8SuPpAcFgAINqHkHUNNrIC5Glx8FJAAAcQAtugT4oBQAAAAASUVORK5CYII=>
[image14]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAXCAYAAADUUxW8AAAAJUlEQVR4XmNgGAWjYBRgB6Kiojzy8vKSxGBlZWUx6mkeBcMeAAA77grNb59DWgAAAABJRU5ErkJggg==>
[image15]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAXCAYAAADUUxW8AAAAYElEQVR4XmNgGAWjgFggJyenJC4uzo0ujgzk5eWz0MXgACj5V0lJiR9dHASAcl9xycEAIzYDQBplZGSEkMVwARQDSNEIA2ADyNEIAmRrJtvZ5AcYNo0wgDeqKE4kIwQAALb2HpmNGUynAAAAAElFTkSuQmCC>
[image16]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAXCAYAAADduLXGAAAAwUlEQVR4XmNgGAWUAgUFBQc5ObkQbBgozUK+YlIBI9CUBfLy8idkZGQ4QQJA9lQQH8hkhqtSUlLiByp8BGMDFfwH4q8gTUB6NhA/hysGScA5ED5IcauoqCgPlH0dLgnyHJJCTZACaWlpGbgCXACocBJIMbo4VgBU+BaIn6CLgwHUEyB3TQJyGWHuhckDnRgB5FuCOcBQ8AUpANFAHANVHA2SMzY2ZgWy38M0ggDItF9QRd1Am4Sg7P9AU08hKxyKAAAX4zkcMt6ZpQAAAABJRU5ErkJggg==>
[image17]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAWCAYAAAD5Jg1dAAAA8klEQVR4XuVROwrCQBRMQEGxEEGM5P+xsV7sxAtIbmCvtSew8BiClzJlGlsLK0vRmbC7bDaNvQND9s2bt2/IOs4/QAjRt7UOkiR5gB/P80Z2TwOGJ7gNw3BIc57nY9vTrINhYUhuHMelUf+INE3nmLxnWbZhXRRFhLU1eNYmrApgPIEDZmJG1cOwoIZjj8UVnERRtKLo+/5UGVEvqXFAac2AnNbAln3HCOEFVoaPWqVXS7gUeIPho5GZb1pQofFdKw1DB2qtp1T5wDcbqHes+TraRMh8tRwq8R9nLYNEkw882o0WmEuuCexeC/I2xYvdV/gC3FlC59JuS4oAAAAASUVORK5CYII=>
[image18]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAYCAYAAAARfGZ1AAABZklEQVR4Xu1TsUrEQBCNoIWoZYhFkk1SGK5SCFj4B4dgIRYnWFgINn6F32BlY+FPWFikt7W0UcRCK0EsDg59T2d1bi7KRe28B0N23pu8nd1MgmCCv0Ycx7POuQHiBXENasrW/BgwvPLrNE1PkD9o/Vdgx1mW9WTdYW5rPgDxEB3cyDEZd5Lfeg5m2/Y9IkmSjW/NPbyR5bHRDnk+rQZ+kOf5quWHEIbhvJjXVnNydPf+8TR/jhOtaK4RKOyKQbdBO5DOzxR3DONFrvHc/6xuALugQRRFcw3a29hVVTXDHGa7yPew2RbDnmgE0vXQfeMuI3B9xGOgZtnXqvjanN1KUZ9TIpPyRK4oiiVb3wp+nJy5b+RH5PlHar4VYFDTxN43P5Rsuqb5VhCDkfkGd0Eed79stbFQluWCmNdW85vyw1ptLMhY0WTTat6cDUj+bGsagcJT/7IOSNOqZl34S8Q9oqMsJvgveAWoLXtEEfJkvQAAAABJRU5ErkJggg==>
[image19]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACUAAAAYCAYAAAB9ejRwAAAB6ElEQVR4Xu1VPUsDQRC9oIKiYCHhilyySbBULA4LxdJCBLEVbOwEsTKo2Ig/wEoEQQRLEaytLPwHWiiCICQSAglosLAKfryXzCabVQvJmVjcg+Fm583Ovd2d23OcECFC/AMkk8n5RCLxqJT6MCynefjPFndtzv9TxOPxEXlp3oyn0+lBxN7gdpnxtkHvBoVIKILxO59mXluBo9wRYfscc4dc1+2389oKiOrVuwUrx2Ixz87pCCDmTkTN2lzHADEXIiprcx0BhOzietjQR+h5Xp+d0wIiaI8l1rWJHyETDsVvaviggAVPoeatHf8WSJzksemx2fBmXqtAvTPUXrbjX4AjGsYK7u24ajT8hM0RcqmW5LjPGcNzFS89RexA0njP1RcGvwK7gWX4F9HxJoDY5iR8+kM2h+IzIqpgcwTiT3xiUaPwi05NAEVx3iU51PdV47i6RWD1IoafS6VSY8JVAw/ywrrpZOHLNg87qReo5RQlXnKa5+b5y6IPUcf6uBAbV0Y/wa+An9bjIMD/YISXK4q/wI40QaGG/4rdcMXfg60JVd21QL9uFsQOLIi/qKTvotHoAHeAvu/7PVogdmSTgvCc4xhzt5R86YEBBVdgBbF1i+Plm4cdqVpTZ51GH7EtrmAZc06I3+AT7/CY0r/wI/8AAAAASUVORK5CYII=>
[image20]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAYCAYAAAACqyaBAAABdElEQVR4Xu2UvUrEQBSFN6gg+IdoCCQmk4RASotoZ6eNjYUICj6Aj2DjE1iJrZWFiA9gYSfa+AI+g6VYWCnGc+UOTK6z2SyONu4Hw84992ROspNJrzdixL9EKXWBUXcZ8lpntAWkabqL3qvUnRCG4SKH38oe4fv+NHr3UncCnuyAwuM43jJkj0JpkiTJPDznRs8dCH6kcB1GIGwPoWtcjqGe1D2nyP0uimKW63HD5h5jv+X4kF7n2PY7iqIlaNemj/UF6C9SN8E6q8YDnMl+A8X7XZbljKFtYr83hO8Y2rpqOXJ5ns+hf6Rr8uKaE9PTQN+l1G1goaotPMuyZVqLboJqzE/7+gedb8mgcAmti/Eg9S+w2CGHb8uejWHC+cNUV1U10WhgkRsOleOyYRQMEw7f27fgn9A1HJ4n/Hg8d/NZtoXTEYX2rGsc3Tv4dvRQHd+nVsT21PS+kI7wFdTvQRBM8c1Zfb8GQval9ld4+KuvpNiPT1p0hAlGoqcTAAAAAElFTkSuQmCC>
[image21]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAYCAYAAAA20uedAAAAkElEQVR4XmNgGJ5AXl7+LRCfQBcHAUagxH8gLkKXYJCTkzMGSSoqKoojCzoDcQhQYh9IEsQGYbCkgoJCAFTyDxA/BLFBYnDdDFD7gILpyIJgALNPSkpKBF0OJDkfJIkuDgZAia9AfBVdHATQ/cciDwsIUVFRHqgXjEF8IH3L2NiYFaYTbicIi4uLc8MlBhsAAJtgJhSnxjfGAAAAAElFTkSuQmCC>
[image22]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAYCAYAAAAlBadpAAAAxElEQVR4XmNgGAUDBOTl5ZcC8X9iMLpeOMCnQEFBIRwo9xVdHAykpKREoJoPoMuBgKioKA9Q7jC6OBgATU4HaZaVlfVDEmYEaQIx5OTkBIFqFiLJIQBQ41WQZphiEAAqjgBqsoFymYF8DpgcCkD3r4qKCh+Uz4KkDBMg+Rcd/0NXiwGw+VdaWloGKLYVWR1WIA/1r7q6Oi+SmCfQvy7I6rACmDPRxQkCQvGLFwCdVg7VHIQuhxMANe2EORcNL0dXOwooBACXVke8Lnk3PAAAAABJRU5ErkJggg==>
[image23]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAXCAYAAADUUxW8AAAAyklEQVR4XmNgGAUDCOTl5U8D8X8o/iInJ/cIhpHEDdH1gQFIUkFBoRGL+B6oxmB0OTAASmgCNR5CFweKNUANrUSXgwOggjVKSkr8aGLBUBv3IItjABkZGSFkvqKiohlII9C/t5DFCQKQQVAb36PL4QUgv0H92IAuhxfg8iPQC+JAsSnIYigAnx+BYvOBcoro4mAACmWojV/R5YCABSSHLggGxsbGrFCNIAWMaNLMQPFfQLwVTRwCYBqB+BmWpAjGsrKyOuj6RgGJAAC82ER8WRO91wAAAABJRU5ErkJggg==>
[image24]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAYCAYAAAAlBadpAAAAwUlEQVR4XmNgGAUDBOTl5Zvk5OQeAen/UPwMxIeK/YKKPUXXhwJgmtHFZWRkOKFyt9HlwEBUVJQHquAAuhwI4DIYDIASniBJWVlZP3Q5oPMFCWneA5IEuQCLXDNITkFBIQNdDgxwmQwU04TKLUaXAwNxcXFumGYs+Ju0tLQwuh44APkTpBDorHR0OYIAqPEASLOUlJQIuhxBAHMiujhBoK6uzgvVfBpdjiAA+jOBZP8CNcxBClE4VlJS4kdXOwooBACwSUl+C0KXaQAAAABJRU5ErkJggg==>
[image25]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAYCAYAAACWTY9zAAAB5klEQVR4Xu1VPUsDQRA9MYKiIIpHIOSyuesCgsiBRdDOQhv/gH9Ba1stLLUIloJ2gqSwCVhYWAoWNoKViDaCQQQxjWL0TZy9TJaNJuKRIHkw7O6bz+zMbRynhx566GIopbYzmcwd1g8S7I+lPpvNDoJ/0HrIC2xWpU2sQMKqTo5jn0VfQEGhyccKJBxD4hJuZ52LWzFtwN07loJjBZIuo7hZbBNcWNViUzG52IGk51gSvD+j4jzPm9R6FB1A9iKHNoAubFBMbPvpjH0eco14+85PHYDhm9j7fGtXgtv8zXzBZ833/Rn4FyFPkDLFJx0KPlSWzkTg+SpKDucKFRcEwSifab7aBvxeeT2V8QhUNHF1awOqPl8R8GsWyAlywDbPUt8q4JfnlWKdGLpasZJrgBLzZfAUjN61AOuuqW8VruuOcKxFyTNXklwDVJM+g99h5xvMyZSpbxX4iJYoTjKZHNYczjmOnZO2EVKp1ESzqtPp9BA7N79up5akIJOaUJaW0b+L5mhscJ6XenK64M/WCugvIY8mr6EHWH3zxrG+4ceTveaUeBEo4BE7ROJY3hT19XRsmbxGGIYD0L+zvxUcf1pyaO+czkudkbo/BRKUTa7j4FmsPStdBRR1Sy01+Y4DNzZucv8On3wvkQo23MoxAAAAAElFTkSuQmCC>
[image26]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAZCAYAAADjRwSLAAAAi0lEQVR4XmNgGAWDFcjLy/8G4htycnK3gLQ3EH8D4gNAvAesQFlZWQwoOV1WVtYUKPgfpEFFRYVPQUGhA8j+BzNlK1CAA0hHgxTJyMioSElJcUE1vIUp8obSh+E6cQGozq3o4nAgKirKA1IEdJsLuhwcAB3tB1IEchu6HBwAFewhxj2fQMGALj7kAQCeXiVXaN2b2wAAAABJRU5ErkJggg==>
[image27]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAYCAYAAACSuF9OAAACA0lEQVR4Xu2VvUsDQRDFIygoCiIYY0KSzQcKqdNaiFjYayfYKv4DVoIIFrapA2JnEWwkYGFhby1YaBEJFkIIFhZqNL53mTuWyV24SFII+cFwyXu7s3N7s3eRyIgRIwZDLpdbNsY8ICrFYnFC+w4wj9Pp9DOubYkX/het7uqxWGxaz+0H5HhCrPF3MpmcYs5oNDqjx3nIwl9adycjfrQXFtzcWTabNbaGfDXEia158O5l0Yr2iHhtrYchn88vYG5Z67xBFHqgdQcY61wQ1xXtWTv0ob0wIOc1c9gaemmWObXuAbMqOzDu4zXpMYn2woC5DV6x+BKK22LfSM5NPdaD24doIeJW7LMQxKUeHxYUMIf5pUwmsyq5GFyrqcd6YPCkDLyyionzjnD9RDxi2JieFwbM3UYUfPQm1j3SukOv/gHjUuy7NsKAeXdaI9BvETWtO5ge/UOkoD+dMCP9ozGd91tgQXymQe8Yd4f6Lkj6p+u4E8lZ0rr9/qlqj0C/Eb/rREA7TKVSea27oEd2Eec++p7cYPcT4UI02Ue2Lo3+Jt6O7RFoRSk0cOfg3ZvOznsHAv8LnIP8i9ZQxyi7CQPiG5NOIz1OF8a8IlpB3yN4DesJOOG3YwPFBDzqRCIxb/x6ZNhg0brWCPvH+Lx/hgr66EJ/wV1MwPtnqAQVQ7BDG1r7V/wC9ySnHtFM7nIAAAAASUVORK5CYII=>
[image28]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAYCAYAAADtaU2/AAABw0lEQVR4Xu2UP0vDUBTFXwdB8d8gJUJCmoRCR4dMBcHVj+Dgp1BQcHLxCxTBxVVEcHbq5Oji6mBBRdxEECtUqHpOemNfbpJOiksOPNqc3733vXfzXoypVOk/5Pv+ZqPRuOEIgmBd818XJrnCZB/4DcSqifeF32UrNBG8aSzygTmMwRjyWTw+c9yyjs5N5DjOrAQda0alk5uSAmB75JgwVqgmdQfKNyaO4ymBXc1SoWAkhXc1o8DuyhYG/5OsXq/PadAvS0rFJFncvWaUsFftm/GOs/XRwg0Bp+PYvFqt1rzE9TULw9ARlntN8LbJ0KkdDQYETM4AJSSuSvHcjuFtSY0Vy64h54g+Nrdm+aMTKcXYholCzLnEdgpYjyw9zdaJvtCxidIDg/GsmVa6wCiKFktY5v16njcj/qHtJ3Jd1xOYa58t8DbjeKU0wwSu1Ch6v/SftG/f3Yk7Bh8yzhScel4vMn1/rW72bP9HAC9lRSmwLjm7oxkF9liUD+9AJi7+NqDgUlkAvGsydkYzUXpH3zWA1xF2wmd/9O1vZ4Kk5Uk75US+8T/veCZQ1Gw2F6RoZiB+3wrjopKa8M8wLi1WqdLf6Bvl9K3mDKixXwAAAABJRU5ErkJggg==>
[image29]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAYCAYAAAB0kZQKAAABsElEQVR4Xu1VsUoDQRS0iJWKiIaThGSTFAZBSHEQ8A9MYyF2dtZWdnbiJ1jnCyxEW0kRSKet/oCFnU0QCwV1JsyFdy+XmMLC4gaW42Zm597uvmwWFnLkyDEHQggX1Wr1Gc9vjTe+QypEUbQk7SPR9X5sMxqNxlZWhuOu7ZxMJGbPE+C7KiD2mgU8A/qKxeKy5cvl8jr5Wq12b/kUEL6mIvpeI8C/TivQ4peFTNVGgHhEQ6VS2fcaoYCh5y14dPL1vFav1yNpX14bA+IDTX4bCQS0FND1mgX0jnwdr+EY7qghq+21MTSZZ37oBwJuFdDy8yzg6dHHHUm4OI4X2Qfk8dy0/hRMPzz6AjjAf1L38zzMQpJfxYs+fua9Ewh/0A88RvlS/YDMHRWyZ/kJBPVDs9lcydC2FT6zH7gA+VL9UCqVNlTEleUnoMmZ2w3+ktoc/dCnzzd20C7PXERSKcbAawT44bQCLaYtJGiXMU69NgbEE5kOvMbOlvbuNYvkNgwZCwH3ZPN5LLjiV0ciuvdGYmpAKpiLJTUQcG7yeX+0vUdjN/Hwg+L439P3GTly/Dv8APAywR01SopIAAAAAElFTkSuQmCC>
[image30]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAYCAYAAAAPtVbGAAABWklEQVR4Xu2TPUoEQRCFe0FBMVNhcGaYHzAxbhAEQ8ELGHgDT+AJvIGYG5l4BT2BGGpmoghG60YauIi+N1SvvW/HxYmdD5pl6qvuru7adq6nRymKYq8sy2v8PmEchDhiO0mSrMS5namq6gYLfWFcZVm2ZrF9fH/Wdb1Np3MQu8V4tnkcrywOamEqkdVZwhs+B1PSNSfz5u/VBcyPNd7gvV+0hJG6GObgVEcaJ3Bb9CjmXF0Dq2dCnufL6mKYk6bpusYJ3Klt4tXxvg+twkt1CvM0FoAbmp+5asqxnSJT1wWuwRvReNzsXyv8C+W8fuAvmtsmQ3VdKOf1A2LDNnlUp7RehcEi7TZm+wEGtsmLihj0axM5JxoP2BrvGp8AecckvhV1Bgtpf2Dupx8YF+omRA/xw8lxcYJV26D1Ggj8GeejH7vqFFb7YJvF41gTA3Cjlny+tyXN7fmHfANv5XKpcwl5NgAAAABJRU5ErkJggg==>

View File

@@ -0,0 +1,149 @@
# **RVW V2.0 表格提取疑难杂症专项解决方案 (v1.1 务实版)**
**问题焦点:** Word 表格“隐性多行”(单元格内多段落)导致的提取与验证错位 **核心策略:** **提取层保持原貌,验证层“懒分裂” (Lazy Split)** **技术栈:** Python (python-docx, pandas)
## **1\. 核心判断:技术选型定调**
| 维度 | 方案 A: 视觉模型 (VLM) | 方案 B: 结构重组 (预分裂) | 方案 C: 懒分裂 (推荐) |
| :---- | :---- | :---- | :---- |
| **原理** | 用 GPT-4V 截图识别 | 提取时把 Table 拆成 N 倍行 | **提取保持 \\n验证时 split** |
| **准确性** | 低 (幻觉/小数点风险) | 中 (容易破坏合并单元格结构) | **高 (数据无损,逻辑灵活)** |
| **复杂度** | 高 (GPU/Prompt) | 高 (重构 DataFrame 结构) | **低 (仅在 Validator 中处理)** |
| **前端适配** | 难 (无法定位) | 难 (需定制虚拟行渲染) | **易 (原生 HTML \<br\>)** |
**最终决策:**
1. **坚决不用视觉模型**:数值准确性是底线。
2. **放弃“预分裂”**不在提取阶段破坏表格的物理结构Row/Span避免引入元数据丢失风险。
3. **采用“懒分裂”**:在验证逻辑中,针对特定单元格内容进行 split('\\n'),实现细粒度验证。
## **2\. 提取层规范 (Extractor Layer)**
**目标**:忠实还原 Word 文档的物理结构,不自作聪明地拆行。
### **2.1 Python 实现逻辑**
在 DocxTableExtractor 中,对于单元格内的多段落,直接使用换行符 \\n 连接。
def extract\_cell\_text(cell):
"""
提取单元格文本,保留段落结构
"""
\# 过滤掉完全空白的段落,保留有内容的段落
paragraphs \= \[p.text.strip() for p in cell.paragraphs if p.text.strip()\]
return "\\n".join(paragraphs)
**输出数据结构示例 (JSON)**
{
"row\_index": 3,
"cells": \[
{ "text": "并发症\\n颅内出血\\n牙龈出血" }, // Col 0
{ "text": "277 (14.65)\\n85 (4.49)\\n94 (4.97)" }, // Col 1
{ "text": "χ²=5.687\\nχ²=0.003\\nχ²=13.745" }, // Col 3 (统计值)
{ "text": "0.017\\n0.01\\n\<0.001" } // Col 4 (P值)
\]
}
## **3\. 验证层规范 (Validator Layer)**
**核心逻辑:** 验证器在读取数据时,动态检测是否存在多行内容。如果存在,则在内存中“临时分裂”并逐一验证。
### **3.1 懒分裂验证算法 (Lazy Verification Logic)**
def verify\_row\_statistics(row\_data, col\_map):
"""
验证单行数据的统计逻辑(支持隐性多行)
"""
issues \= \[\]
\# 1\. 获取目标单元格的原始文本
\# 假设我们要验证 Col 1 (Group A) vs Col 2 (Group B) \-\> P Value
cell\_a\_text \= row\_data\[col\_map\['group\_a'\]\]
cell\_b\_text \= row\_data\[col\_map\['group\_b'\]\]
cell\_p\_text \= row\_data\[col\_map\['p\_value'\]\]
\# 2\. 懒分裂 (Lazy Split)
lines\_a \= cell\_a\_text.split('\\n')
lines\_b \= cell\_b\_text.split('\\n')
lines\_p \= cell\_p\_text.split('\\n')
\# 3\. 确定对齐基准(取最大行数)
max\_lines \= max(len(lines\_a), len(lines\_b), len(lines\_p))
\# 4\. 逐行验证 (Line-by-Line Validation)
for i in range(max\_lines):
\# 安全获取当前行的数据(处理长度不一致情况)
val\_a \= lines\_a\[i\] if i \< len(lines\_a) else ""
val\_b \= lines\_b\[i\] if i \< len(lines\_b) else ""
\# P 值匹配策略:
\# 如果 P 值列只有 1 行,但数据有 N 行 \-\> 广播机制 (Broadcast)
\# 如果 P 值列有 N 行 \-\> 一一对应 (One-to-One)
if len(lines\_p) \== 1 and max\_lines \> 1:
val\_p \= lines\_p\[0\] \# 策略 A: 共享 P 值
else:
val\_p \= lines\_p\[i\] if i \< len(lines\_p) else "" \# 策略 B: 独立 P 值
\# 跳过空行
if not val\_a or not val\_b or not val\_p:
continue
\# 执行具体的统计验证
\# 传入 line\_index=i 以便报错时定位
error \= validate\_single\_line(val\_a, val\_b, val\_p, line\_index=i)
if error:
issues.append(error)
return issues
### **3.2 优势分析**
1. **兼容性强**:完美支持您截图中的 颅内出血 | 85 | 90 | P=0.01 这种每行独立 P 值的场景。
2. **鲁棒性**:如果只有第一行有 P 值(合并单元格视觉效果),代码中的 Broadcast 逻辑也能兜底。
3. **定位精准**:报错信息可以包含 line\_index告诉前端是单元格里的第几行出错了。
## **4\. 前端渲染规范 (Frontend Layer)**
**目标**:使用最简单的 Web 技术还原 Word 样式,避免过度设计。
### **4.1 HTML 渲染策略**
后端返回的 html 字段中,直接将 \\n 替换为 \<br\>。
**Python 端处理:**
def generate\_html\_cell(text):
\# 转义 HTML 特殊字符,并将换行转为 \<br\>
safe\_text \= html.escape(text)
return safe\_text.replace("\\n", "\<br\>")
**前端展示效果:**
\<td\>
277 (14.65)\<br\>
85 (4.49)\<br\>
94 (4.97)
\</td\>
### **4.2 错误高亮策略**
由于我们不再拆分表格行DOM 结构),高亮的最小单位是 **Cell单元格**
* **交互设计**
* 当发现第 2 行子数据错误时,**高亮整个单元格**。
* **Tooltip 提示**:鼠标悬停时,显示具体错误信息:“第 2 行数据 P 值校验不通过”。
* **进阶优化V2.1 可选)**
* 如果确实需要高亮某一行Python 生成 HTML 时可以用 \<span\> 包裹每一行: \<span id="r3c2\_L0"\>277 (14.65)\</span\>\<br\>\<span id="r3c2\_L1"\>85 (4.49)\</span\>
* 但 MVP 阶段建议**只高亮单元格**,性价比最高。
## **5\. 总结**
| 模块 | 核心动作 | 复杂度 |
| :---- | :---- | :---- |
| **Python 提取** | 保持 \\n不拆行输出标准 JSON | ⭐ (低) |
| **Python 验证** | split('\\n'),循环对齐,独立计算 | ⭐⭐ (中) |
| **前端渲染** | 使用 \<br\> 换行CSS 控制对齐 | ⭐ (低) |
| **前端高亮** | 高亮整个单元格Tooltip 说明行号 | ⭐ (低) |
**这是目前最务实、风险最低的实施路径。** 请开发团队以此为准。