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:
@@ -1,21 +1,30 @@
|
||||
# RVW稿件审查模块 - 当前状态与开发指南
|
||||
|
||||
> **文档版本:** v5.0
|
||||
> **文档版本:** v5.1
|
||||
> **创建日期:** 2026-01-07
|
||||
> **最后更新:** 2026-02-18
|
||||
> **维护者:** 开发团队
|
||||
> **当前状态:** 🚀 **V2.0 "数据侦探" 开发中(Week 2 Day 10 完成)**
|
||||
> **当前状态:** 🚀 **V2.0 "数据侦探" Week 3 完成(统计验证扩展+用户体验优化)**
|
||||
> **文档目的:** 快速了解RVW模块状态,为新AI助手提供上下文
|
||||
>
|
||||
> **🎉 V2.0 进展(2026-02-18):**
|
||||
> **🎉 V2.0 进展(2026-02-18 Week 3):**
|
||||
> - ✅ **负号归一化**:防止 float() 崩溃,覆盖 6 种负号变体
|
||||
> - ✅ **T 检验验证增强**:智能样本量提取 + subrow 精确高亮
|
||||
> - ✅ **SE 三角验证增强**:多行单元格 subrow 支持
|
||||
> - ✅ **CI vs P 值验证增强**:subrow 支持 + 灵活 P 值解析
|
||||
> - ✅ **前端翻译映射**:新增 6 种 IssueType 中文翻译
|
||||
> - ✅ **文件格式提示**:PDF/.doc 上传时提示无法数据验证
|
||||
>
|
||||
> **🎉 V2.0 进展(Week 1-2):**
|
||||
> - ✅ **L1 算术验证器**:行列加总、百分比验证(Day 3)
|
||||
> - ✅ **L2 统计验证器**:CI↔P 值一致性、T检验逆向验证(Day 6)
|
||||
> - ✅ **L2 统计验证器**:CI↔P 值一致性、卡方检验逆向验证(Day 6)
|
||||
> - ✅ **L2.5 一致性取证**:SE三角验证、SD>Mean检查(Day 6 终审提权)
|
||||
> - ✅ **Word 文档解析**:python-docx 表格提取(Day 2)
|
||||
> - ✅ **Word 文档解析**:python-docx 表格提取 + 特殊符号提取(Day 2)
|
||||
> - ✅ **Skills 核心框架**:types、registry、executor、profile、context(Day 7)
|
||||
> - ✅ **DataForensicsSkill**:依赖注入、路径安全、优雅降级(Day 8)
|
||||
> - ✅ **DataForensicsSkill**:OSS 集成、依赖注入、优雅降级(Day 8)
|
||||
> - ✅ **EditorialSkill + MethodologySkill**:封装现有服务(Day 9)
|
||||
> - ✅ **ReviewWorker 改造**:集成 SkillExecutor,支持 V1/V2 切换(Day 10)
|
||||
> - ✅ **前端数据验证 Tab**:ForensicsReport 组件、精确单元格高亮(Week 3)
|
||||
|
||||
---
|
||||
|
||||
@@ -377,37 +386,50 @@ Content-Type: multipart/form-data
|
||||
| 阶段 | 任务 | 状态 | 完成日期 |
|
||||
|------|------|------|---------|
|
||||
| Week 1 Day 1 | Python 服务搭建 | ✅ 已完成 | 2026-02-12 |
|
||||
| Week 1 Day 2 | Word 表格提取 | ✅ 已完成 | 2026-02-13 |
|
||||
| Week 1 Day 2 | Word 表格提取 + 特殊符号 | ✅ 已完成 | 2026-02-13 |
|
||||
| Week 1 Day 3 | L1 算术验证器 | ✅ 已完成 | 2026-02-14 |
|
||||
| Week 1 Day 4 | 数据结构设计 | ✅ 已完成 | 2026-02-15 |
|
||||
| Week 1 Day 5 | API 集成 | ✅ 已完成 | 2026-02-16 |
|
||||
| **Week 2 Day 6** | **L2 统计验证器 + L2.5 一致性取证** | **✅ 已完成** | **2026-02-17** |
|
||||
| Week 2 Day 7 | Skills 核心框架 | 📋 待开发 | - |
|
||||
| Week 2 Day 8 | DataForensicsSkill | 📋 待开发 | - |
|
||||
| Week 2 Day 9 | EditorialSkill 封装 | 📋 待开发 | - |
|
||||
| Week 2 Day 10 | ReviewService 改造 | 📋 待开发 | - |
|
||||
| Week 2 Day 6 | L2 统计验证器 + L2.5 一致性取证 | ✅ 已完成 | 2026-02-17 |
|
||||
| Week 2 Day 7 | Skills 核心框架 | ✅ 已完成 | 2026-02-18 |
|
||||
| Week 2 Day 8 | DataForensicsSkill | ✅ 已完成 | 2026-02-18 |
|
||||
| Week 2 Day 9 | EditorialSkill 封装 | ✅ 已完成 | 2026-02-18 |
|
||||
| Week 2 Day 10 | ReviewWorker 改造 | ✅ 已完成 | 2026-02-18 |
|
||||
| **Week 3** | **统计验证扩展 + 用户体验优化** | **✅ 已完成** | **2026-02-18** |
|
||||
| Week 4 | 功能测试 + Bug 修复 | 📋 待开始 | - |
|
||||
|
||||
**V2.0 核心功能**:
|
||||
**V2.0 核心功能(已完成)**:
|
||||
- **L1 算术验证**:行列加总、百分比验证
|
||||
- **L2 统计验证**:CI↔P 一致性、T检验逆向、卡方检验
|
||||
- **L2 统计验证**:CI↔P 一致性、T检验逆向、卡方检验(含 subrow 精确高亮)
|
||||
- **L2.5 一致性取证**(终审提权):SE三角验证、SD>Mean检查
|
||||
- **Skills 架构**:Skill Registry、Skill Executor、Journal Profiles
|
||||
- **负号归一化**:6 种 Unicode 负号变体支持
|
||||
- **文件格式提示**:PDF/.doc 无法数据验证的用户提示
|
||||
|
||||
**Week 3 完成内容(2026-02-18)**:
|
||||
- ✅ 负号归一化(防止 float() 崩溃)
|
||||
- ✅ T 检验验证增强(智能样本量提取)
|
||||
- ✅ SE 三角验证增强(subrow 支持)
|
||||
- ✅ CI vs P 值验证增强(subrow 支持)
|
||||
- ✅ 前端翻译映射更新(6 种新 IssueType)
|
||||
- ✅ 文件格式提示(Header、ReportDetail、TaskDetail)
|
||||
|
||||
### 后续版本(V2.1+)
|
||||
|
||||
- [ ] Week 4 功能测试和 Bug 修复
|
||||
- [ ] ANOVA 验证(多组比较)
|
||||
- [ ] 配对 T 检验验证
|
||||
- [ ] 非参数检验(Mann-Whitney、Wilcoxon)
|
||||
- [ ] .doc 格式支持(Pandoc 方案评估)
|
||||
- [ ] Profile 管理 UI(期刊配置界面)
|
||||
- [ ] PDF报告导出优化
|
||||
- [ ] PICO卡片UI实现
|
||||
- [ ] 历史归档UI实现
|
||||
- [ ] L3 高级逻辑推理验证
|
||||
- [ ] 登录页面(独立产品时)
|
||||
- [ ] 审稿人管理系统
|
||||
- [ ] 多轮审稿流程
|
||||
- [ ] 期刊库管理
|
||||
- [ ] 独立产品打包
|
||||
|
||||
---
|
||||
|
||||
**文档版本:** v3.2
|
||||
**最后更新:** 2026-01-10
|
||||
**当前状态:** ✅ Phase 1-6 完成,模块95%可用,Schema已隔离
|
||||
**下一步:** 生产环境部署测试
|
||||
**文档版本:** v5.1
|
||||
**最后更新:** 2026-02-18
|
||||
**当前状态:** 🚀 V2.0 "数据侦探" Week 3 完成,Skills 架构 + 统计验证 + 用户体验优化
|
||||
**下一步:** Week 4 功能测试和 Bug 修复
|
||||
|
||||
Binary file not shown.
184
docs/03-业务模块/RVW-稿件审查系统/06-开发记录/2026-02-18 统计验证扩展与用户体验优化.md
Normal file
184
docs/03-业务模块/RVW-稿件审查系统/06-开发记录/2026-02-18 统计验证扩展与用户体验优化.md
Normal 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 测试完成后
|
||||
137
docs/03-业务模块/RVW-稿件审查系统/06-开发记录/RVW V2.0 表格提取疑难杂症专项解决方案.md
Normal file
137
docs/03-业务模块/RVW-稿件审查系统/06-开发记录/RVW V2.0 表格提取疑难杂症专项解决方案.md
Normal 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 只有一行”的问题。
|
||||
201
docs/03-业务模块/RVW-稿件审查系统/06-开发记录/临床统计特殊符号提取白皮书.md
Normal file
201
docs/03-业务模块/RVW-稿件审查系统/06-开发记录/临床统计特殊符号提取白皮书.md
Normal 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==>
|
||||
149
docs/03-业务模块/RVW-稿件审查系统/06-开发记录/务实版.md
Normal file
149
docs/03-业务模块/RVW-稿件审查系统/06-开发记录/务实版.md
Normal 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 说明行号 | ⭐ (低) |
|
||||
|
||||
**这是目前最务实、风险最低的实施路径。** 请开发团队以此为准。
|
||||
Reference in New Issue
Block a user