文档清洗系统初始化脚本

This commit is contained in:
cxs 2025-05-16 11:30:02 +08:00
parent a73040d739
commit 532eb2857c
29 changed files with 11568 additions and 225 deletions

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

12
.idea/doc-etl.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="GOOGLE" />
<option name="myDocStringFormat" value="Google" />
</component>
</module>

View File

@ -0,0 +1,21 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyInterpreterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="false" level="WARNING" enabled_by_default="false">
<option name="ignoredPackages">
<value>
<list size="7">
<item index="0" class="java.lang.String" itemvalue="httpx" />
<item index="1" class="java.lang.String" itemvalue="sanic_cors" />
<item index="2" class="java.lang.String" itemvalue="aiofiles" />
<item index="3" class="java.lang.String" itemvalue="sanic" />
<item index="4" class="java.lang.String" itemvalue="sanic-ext" />
<item index="5" class="java.lang.String" itemvalue="Jinja2" />
<item index="6" class="java.lang.String" itemvalue="PyExecJS2" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/doc-etl.iml" filepath="$PROJECT_DIR$/.idea/doc-etl.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

566
README.md
View File

@ -1,235 +1,361 @@
# 文档清理工具
# 文档处理系统
这是一个用于清理和标准化Word文档(doc/docx)的Python工具主要用于为构建RAG知识库做数据准备工作。
## 主要功能
- 移除页眉页脚(包括页码)
- 删除特殊符号(版权信息、水印等)
- 统一标点符号(全角转半角)
- 分离正文与附录/参考文献
- 删除重复段落(基于文本相似度)
- 自动跳过图片内容
- 支持doc格式自动转换为docx
- 保持原始文档格式统一输出docx格式
- 完整保留表格内容及格式
- 支持表格转换为格式化文本,便于大模型识别
- 同时输出docx和txt格式文件txt文件包含完整的文本内容和表格的文本表示
本系统用于处理和转换文档支持PDF和Word文档的处理。
## 系统要求
- Python 3.6+
- LibreOffice用于转换doc格式文件
### 必需组件
### 安装LibreOffice
1. Python 3.8 或更高版本
2. LibreOffice用于文档格式转换
- 下载地址https://www.libreoffice.org/download/download/
- 安装后需要将安装目录(通常是 `C:\Program Files\LibreOffice\program``C:\Program Files (x86)\LibreOffice\program`)添加到系统 PATH 环境变量
- 如果安装后仍然报错,请尝试重启系统
- macOS:
```bash
brew install libreoffice
```
3. Tesseract OCR用于图片文字识别
- 下载地址https://github.com/UB-Mannheim/tesseract/wiki
- 安装时选择"添加到系统路径"选项
- Ubuntu/Debian:
```bash
sudo apt-get install libreoffice
```
### Python 依赖
- Windows:
从[LibreOffice官网](https://www.libreoffice.org/download/download/)下载安装并确保将安装目录添加到系统PATH中。
## 安装依赖
所有必需的 Python 包都列在 `requirements.txt` 文件中。使用以下命令安装:
```bash
pip install -r requirements.txt
```
## 使用方法
```bash
python doc_cleaner.py 输入目录
```
### 示例
```bash
python doc_cleaner.py ./input_docs
```
## 输出说明
程序会为每个处理的文档生成两个文件:
- `文档名_cleaned.docx`: 包含清理后的正文内容和附录(如果存在)
- 附录内容会自动添加分页符并在新页面开始
- 所有文件包括原始doc格式都会统一转换并保存为docx格式
- 保持文档格式为docx支持段落对齐等基本格式
- 表格以文本格式显示使用ASCII字符绘制边框和分隔符
- 使用等宽字体Courier New确保表格对齐
- 自动调整列宽以适应内容
- 清晰标识表格序号和位置(正文/附录)
- `文档名_cleaned.txt`: 包含所有文本内容的纯文本文件
- 包含完整的正文内容
- 包含所有表格的文本表示使用ASCII字符绘制
- 保持文档的原始结构(正文、表格、附录的顺序)
- 使用空行分隔不同部分
- 清晰标注表格的序号和位置
## 注意事项
1. 确保输入目录中包含要处理的doc或docx文件
2. 程序会自动创建输出目录(如果不存在)
3. 处理过程中的错误会被记录但不会中断整体处理
4. 相似度阈值默认设置为0.85,可以通过修改代码中的`similarity_threshold`参数调整
5. 输出文件将统一保存为docx格式便于后续编辑和使用
6. 处理doc格式文件需要安装LibreOffice
7. 首次处理doc文件时可能需要较长时间因为需要进行格式转换
## 正则表达式说明
### 页眉页脚匹配模式
- `\d+-\d+`: 匹配类似"1-1"的页码格式
- `第\s*\d+\s*页`: 匹配中文页码
- `Page\s*\d+\s*of\s*\d+`: 匹配英文页码
### 附录标题匹配模式
- `^附录\s*[A-Za-z]?[\s:]`
- `^Appendix\s*[A-Za-z]?[\s:]`
- `^参考文献$`
- `^References$`
- `^Bibliography$`
## 版本历史
### v1.1.0 (2024-01-09)
- 新增完整的表格支持
- 保留表格原始格式和样式
- 优化文档处理流程
### v1.0.0
- 初始版本发布
- 基础文档清理功能
## 更新日志
### 2024-03-21
- 修复了表格位置错误的问题
- 改进了表格占位符的处理机制
- 实现了基于索引的精确表格定位
- 确保表格按原文档位置正确插入
- 重构了文档处理核心逻辑
- 改进了文档元素的解析和存储方式
- 优化了正文和附录的分离逻辑
- 加强了表格位置的追踪机制
- 简化了文档结构处理流程
### 2024-03-xx
- 修复了表格在清理过程中位置错位的问题
- 改进了文本清理逻辑,确保表格占位符不被清理
- 优化了去重算法,保持表格在文档中的原始位置
- 分离表格和文本内容的处理流程,避免交叉影响
### 2024-03-22
- 优化了文件类型检测方法
- 移除了对magic库的依赖
- 改用文件后缀名直接判断文件类型
- 简化了文件类型检测逻辑
### 2024-03-23
- 优化表格处理方式
- 将表格直接转换为文本格式显示在文档中
- 使用ASCII字符绘制表格边框和分隔符
- 采用等宽字体确保表格对齐
- 自动调整列宽以适应内容
- 优化了表格内容的文本对齐方式
- 保留表头样式,便于识别表格结构
- 清晰标识表格序号和位置(正文/附录)
- 移除了原始表格格式,提高可读性和兼容性
### 2024-03-24
- 新增文本文件输出功能
- 同时生成docx和txt格式的输出文件
- txt文件包含完整的文本内容和表格的文本表示
- 保持文档结构和内容的完整性
- 优化了表格在文本文件中的显示效果
### 2024-03-25
- 优化文本输出格式
- 移除文本输出中的换行符
- 使用空格分隔文本内容
- 提高文本的连续性和可读性
### 2024-03-26
- 优化表格文本格式
- 移除ASCII边框字符+和-
- 使用方括号[]标识单元格边界
- 避免JSON解析错误
- 简化表格文本表示方式
### 2024-03-27
- 进一步优化文本输出格式
- 移除所有换行符和多余空格
- 所有内容在单行内显示
- 优化表格文本的连续性
- 确保输出文本的JSON兼容性
### 2024-03-28
- 改进表格输出格式
- 采用表单形式输出表格内容
- 使用"键:值"格式表示每个单元格
- 按行组织表格数据
- 使用表头作为数据项的键名
- 提高表格数据的可读性和结构性
### 2024-03-29
- 完善表格输出格式
- 添加表头行作为第一行输出
- 确保每行数据包含所有字段
- 保持字段顺序与表头一致
- 空值字段显示为空字符串
- 提升数据的完整性和一致性
### 2024-03-30
- 增强复杂表格处理能力
- 支持多级表头的识别和处理
- 正确处理水平和垂直合并单元格
- 智能识别表头行数最多支持3行表头
- 优化表头和数据的关联方式
- 提供更清晰的数据结构表示
- 完善空值和特殊情况的处理
- 确保合并单元格数据的完整性
### 2024-03-31
- 优化表格处理错误处理机制
- 增加多层错误处理,确保程序稳定性
- 改进合并单元格处理逻辑
- 优化空值和异常值的处理方式
- 提供更详细的错误位置信息
- 确保部分错误不影响整体处理
- 完善日志输出,便于问题定位
### 2024-04-01
- 深度优化复杂表格处理
- 改进多级表头的识别和处理算法
- 优化表头层级结构的分析方法
- 增强跨行跨列单元格的处理能力
- 改进表头标识符的构建逻辑
- 提升数据与表头的关联准确性
- 支持更复杂的表格结构解析
- 完善异常情况的处理机制
### 2024-04-02
- 优化表头处理逻辑
- 消除表头中的重复前缀
- 优化多级表头的组合方式
- 改进跨列表头的处理机制
- 增加表头去重功能
- 保持表头层级的清晰性
- 提高表格数据的可读性
- 减少冗余信息
## 功能特性
- 支持doc和docx格式的文档处理
- 清理文档中的页眉页脚
- 保留文档中的表格并维持其原始位置
- 支持附录的单独处理
- 文本去重功能
- 批量处理目录下的所有文档
- 支持多种文档格式的处理:
- PDF 文件
- Word 文档 (.doc, .docx)
- HTML 文件 (.html, .htm)
- Excel 文件 (.xls, .xlsx)
- 自动提取文档中的表格和图片
- 智能清理和格式化文本内容
- 支持多种输出格式:
- Word 文档 (.docx)
- 纯文本文件 (.txt)
- Markdown 文件 (.md)
- 提供 RESTful API 接口
- 高级OCR图像识别功能
- 独立的OCR测试Web界面
- 多种图像预处理算法
- 支持中文优化的OCR处理
- 自动选择最佳OCR结果
- 直观显示不同处理方法的效果对比
- 可视化处理前后的图像变化
## 最近更新
### 2024年5月21日
- **增强复杂表格处理的安全性和稳定性**
- 全面优化索引安全处理,解决表格解析中的索引越界问题
- 增加表格行列索引检查机制,防止数组访问异常
- 引入垂直合并传播机制,自动填充复杂表格中的间隔空白单元格
- 增强多级分类表格处理,针对药品分类等特殊表格格式的优化
- 改进空白单元格智能填充算法,分析上下文识别合并单元格模式
- 优化错误日志记录,提供精确的错误位置信息便于问题定位
- 统一文本和Markdown输出处理逻辑确保不同格式输出的一致性
- 修复多行多列复杂表格中的内容缺失问题,提高数据完整性
### 2024年5月20日
- **增强复杂表格合并单元格识别能力**
- 改进垂直合并单元格的识别逻辑,即使未被正确标记的合并单元格也能被识别出来
- 特别优化对药品分类等表格中常见的第一列垂直合并单元格的处理
- 新增内容一致性检测机制,自动识别内容相同但分布在不同行的单元格
- 添加空白单元格智能填充机制,推断可能的合并单元格内容
- 统一文本与Markdown表格输出中对合并单元格的处理逻辑
- 对合并单元格内容进行正确复制,确保表格展示的结构完整性
- 提高对格式不规范表格的兼容性和处理能力
### 2024年5月19日
- **表格处理模块化重构**
- 将表格处理相关代码抽取到独立模块`cxs_table_processor.py`
- 实现`TableProcessor`类,封装所有表格相关的处理功能
- 保留`TableData`类作为表格数据的标准存储结构
- 优化代码结构,提高可维护性和扩展性
- 改进表格处理模块与主程序的交互接口
- 实现统一的表格处理方法调用方式
- 便于后续对表格处理功能的单独升级和优化
### 2024-05-16
- 增强了复杂表格处理能力
- 优化了对合并单元格表格的识别和处理
- 降低了表格有效性的判断门槛,能够识别更多种类的表格
- 改进了表格到Markdown的转换添加了HTML格式输出选项
- 完善了对垂直合并单元格的处理逻辑
- 增强了表格内容矩阵的构建和处理
- 优化了药品分类等复杂结构表格的识别
- 改进了表格文本输出格式,提高可读性
### 2024-05-15
- 修复了cxs_doc_cleaner.py中的语法错误
- 解决了处理文档元素时的try-except块和缩进结构问题
- 改进了表格和段落处理的代码结构
- 确保所有异常处理逻辑正确嵌套
- 优化了错误信息输出格式
### 2024-05-12
- 修复了`Table`类初始化错误问题
- 创建了自定义`TableData`类代替直接使用`docx.table.Table`
- 解决了`Table.__init__() missing 2 required positional arguments: 'tbl' and 'parent'`错误
- 重构了相关代码以适配新的类结构
- 优化了列属性处理方式
- 改进了单元格访问方法
- 添加了`test_big_file.py`工具
- 专门用于从大型复杂Word文档中提取表格
- 支持自动识别文档中的所有表格
- 实现了更强大的表格内容提取逻辑
- 可以处理合并单元格和复杂结构
- 生成格式友好的表格文本输出
### 2024-05-02
- 修复文档中图片OCR无法识别的问题
- 添加pytesseract显式导入确保OCR组件可用
- 优化Tesseract OCR路径自动检测和配置流程
- 增加pytesseract版本检测机制确保OCR组件正常工作
- 改进OCR处理流程添加直接调用pytesseract模式不再完全依赖pdf_processor
- 添加多重OCR尝试策略提高图片文字识别成功率
- 增强OCR错误处理和日志记录便于问题诊断
- 简化单图片OCR处理逻辑增加文本预览功能
- 修复PdfProcessor类中缺少_is_valid_image方法的问题解决图片验证失败的错误
### 2024-01-20
- 修复文档中图片OCR识别问题
- 解决了从Word文档中提取的图片无法被OCR识别的问题
- 增加了Tesseract OCR路径自动检测和配置功能
- 添加了独立的图片OCR测试工具方便排查问题
- 优化了图像提取和OCR处理流程提高识别成功率
- 增强了图像预处理算法,改进对不同格式图片的支持
- 新增fix_tesseract_path.py脚本提供一键修复和测试功能
### 2024-01-19
- 修复Word文档图像提取和OCR功能
- 修复了Word文档处理流程中图像提取功能未被正确调用的问题
- 增强了调试日志系统提供更详细的图像提取和OCR处理信息
- 优化了文档处理流程确保图像OCR结果被正确保存和展示
- 添加了更多统计信息包括图像提取数量和OCR识别成功率
- 改进了不同文件格式的处理逻辑,确保最大兼容性
- 增强了错误处理能力,提高了系统的鲁棒性和稳定性
### 2024-01-18
- 增强了DOCX文档中图像的提取与OCR识别能力
- 实现了三种不同的图像提取方法确保各种格式的Word文档中的图像都能被正确提取
- 为图像提取和OCR处理添加了全面的调试日志记录每个步骤的处理结果
- 增加了原始图像和处理后图像的保存功能便于分析OCR失败的原因
- 优化了图像过滤机制,自动识别并排除无效的小图像和非图像文件
- 引入中文优化的二次识别机制,提高中文图像的文字识别率
- 添加了详细的处理统计信息,包括成功识别率和处理时间
- 改进了错误处理和异常报告,提高系统稳定性
- 增加了中文OCR专用图像优化模块 `_optimize_for_chinese.py`
- 提供9种针对中文文本的图像处理方法适用于不同场景
- 支持图像倾斜校正,提高不规则拍摄图像的识别率
- 针对中文笔画特点优化的形态学处理算法
- 提供一体化的中文OCR预处理流程
- 自动判断最佳处理方法优化OCR结果
### 2024-01-17
- 新增OCR测试Web界面便于快速测试图像文字识别效果
- 开发了独立的OCR API服务支持多种图像处理模式
- 引入自动选择最佳OCR结果的机制通过对比不同处理方法的结果选择最优方案
- 可视化展示图像处理细节和实时预览效果
- 简化OCR测试流程支持拖放上传图像并一键处理
- 提供快速启动脚本,自动检查环境和依赖
- 优化调试文件存储和显示,方便问题分析
### 2024-01-16
- 大幅增强了OCR图像识别能力提高了复杂图像的文字识别率
- 引入了9种新的图像预处理方法并自动进行组合尝试
- 新增图像纠偏和倾斜校正,提高对歪斜文本的识别能力
- 增加针对中文处理的专项优化
- 增加超高DPI处理提高小字体和复杂字符的识别率
- 引入基于PSM模式的多方案OCR处理自动选择最佳结果
- 改进OCR结果评估机制综合考虑文本长度和置信度
- 增加图像处理调试功能,自动保存处理前后的图像用于分析
- 优化OCR结果报表提供各种处理方法的对比和详细统计
### 2024-01-15
- 增强了OCR功能
- 改进了PDF处理器中的OCR文本识别功能
- 增强了Tesseract OCR路径检测
- 添加了OCR识别重试机制
### 2024-01-14
- 优化文档图片OCR识别功能大幅提升Word文档内图片文本识别率
- 增强图片处理逻辑,添加文件类型验证,防止非图片文件误处理
- 增加OCR失败重试机制提高文本识别成功率
- 添加更多调试信息,帮助排查图片处理问题
- 改进Markdown输出中图片显示方式正确标识无法识别文本的图片
### 2024-01-13
- 添加openpyxl依赖库修复Excel文件处理的依赖问题
- 更新requirements.txt文件确保Excel文件可以正确读取
- 优化Excel文件处理逻辑解决处理大型Excel文件时卡住的问题
- 增加直接处理Excel文件选项不必转换为Word格式
- 限制处理的Excel行数提高大型文件处理效率
- 改进临时文件处理机制,增加文件删除重试功能
- 优化Excel文件句柄管理确保文件资源及时释放
### 2024-01-12
- 优化Markdown文档输出格式使其更接近原文排版
- 改进复杂表格的Markdown转换提高可读性
- 修复图片无法在Markdown中正确显示的问题
- 增强文档结构识别,自动识别标题层级
### 2024-01-11
- 修复缺少uuid库导入的问题
- 改进图片目录创建逻辑
- 优化文件路径处理机制
### 2024-01-10
- 新增支持 HTML 文件处理
- 新增支持 Excel 文件处理
- 优化文件格式处理逻辑
- 改进错误处理机制
### 2024-01-09
- 新增 Markdown 格式输出支持
- 优化文件处理逻辑
- 改进错误处理
## 安装说明
1. 克隆项目代码
2. 安装依赖:
```bash
pip install -r requirements.txt
```
3. 配置环境变量(可选):
- OLLAMA_HOSTOllama 服务器地址
- TESSERACT_CMDTesseract OCR 可执行文件路径
## 使用说明
### 文件上传
- 支持的文件格式:
- PDF (.pdf)
- Word (.doc, .docx)
- HTML (.html, .htm)
- Excel (.xls, .xlsx)
### API 接口
- 文件上传POST `/api/upload`
- 文件下载GET `/api/download/{filename}`
### 输出格式
- Word 文档:`response_文件名_output.docx`
- 纯文本文件:`response_文件名_output.txt`
- Markdown 文件:`response_文件名_output.md`
### OCR图像识别测试
使用OCR测试功能可以快速测试图像文字识别效果无需处理完整文档。
#### 快速启动方法
1. 运行项目根目录下的启动脚本:
```bash
python start_ocr_test.py
```
脚本会自动:
- 检查必要依赖是否安装
- 验证Tesseract OCR配置
- 启动OCR API服务
- 在浏览器中打开测试界面
2. 如果自动打开浏览器失败,请手动访问:
```
http://localhost:8001/static/ocr_test.html
```
#### 使用方法
1. 在测试界面上传图片文件(支持拖放上传)
2. 选择识别语言(默认为中文+英文)
3. 选择处理模式:
- **自动模式**:自动尝试最佳处理方法
- **标准模式**使用基本OCR处理速度最快
- **中文优化**:专为中文文本优化的处理方法
- **高级模式**:使用多种图像处理方法,并比较结果
4. 点击"执行OCR"按钮开始处理
5. 查看处理结果:
- **识别文本**:显示识别出的文本内容
- **处理详情**:显示不同处理方法的结果对比
- **处理图像**:显示处理前后的图像效果
#### 处理模式说明
- **自动模式**:适合大多数图像,自动选择最佳处理方法
- **标准模式**:适合清晰、对比度高的图像,处理速度最快
- **中文优化**:适合包含中文的图像,特别是小字体或模糊的中文文本
- **高级模式**:适合复杂图像,会尝试多种处理方法并选择最佳结果,处理时间较长
## 注意事项
1. Excel 文件处理时会将每个工作表转换为单独的章节
2. HTML 文件会保留基本的文本格式和表格结构
3. 所有临时文件会保存在 `temp` 目录下
## 开发说明
### 目录结构
```
doc-etl/
├── cxs/
│ ├── static/ # 前端文件
│ │ └── ocr_test.html # OCR测试界面
│ ├── main.py # 主程序
│ ├── cxs_doc_cleaner.py # 文档处理核心
│ ├── cxs_table_processor.py # 表格处理模块
│ ├── cxs_pdf_cleaner.py # PDF处理模块
│ ├── _optimize_for_chinese.py # 中文OCR优化模块
│ └── ocr_api.py # OCR API服务
├── temp/
│ ├── uploads/ # 上传文件
│ ├── outputs/ # 输出文件
│ ├── images/ # 临时图片
│ └── debug/ # OCR处理调试图像
├── start_ocr_test.py # OCR测试启动脚本
└── requirements.txt # 依赖清单
```
### 开发环境
- Python 3.8+
- 依赖详见 requirements.txt
## 图像OCR问题排查
### 问题: 文档中图片OCR无法识别
如果您遇到文档处理时图片OCR识别失败的问题很可能是因为Tesseract OCR工具的路径配置不正确。系统在初始化时会尝试自动查找Tesseract但如果系统环境变量中没有正确配置OCR功能可能无法正常工作。
### 解决方案
我们提供了一个修复脚本`fix_tesseract_path.py`,它可以:
1. 自动查找系统中已安装的Tesseract OCR
2. 正确设置Tesseract路径
3. 处理您的文档并启用图片OCR功能
使用方法:
```bash
# 直接处理指定文档
python fix_tesseract_path.py --file 您的文档.docx
# 指定Tesseract路径
python fix_tesseract_path.py --file 您的文档.docx --tesseract "C:\Program Files\Tesseract-OCR\tesseract.exe"
# 交互式模式
python fix_tesseract_path.py
```
### 注意事项
1. 确保已安装Tesseract OCR如未安装请从[官方GitHub](https://github.com/UB-Mannheim/tesseract/wiki)下载并安装
2. 安装时选择中文语言包以支持中文OCR识别
3. 建议将Tesseract添加到系统PATH环境变量中或在配置文件中明确指定路径
### 手动设置Tesseract路径
如果您希望永久解决这个问题,可以:
1. 将Tesseract安装目录(通常是`C:\Program Files\Tesseract-OCR`)添加到系统PATH环境变量
2. 设置环境变量`TESSERACT_CMD`为Tesseract可执行文件的完整路径

120
cxs/README.md Normal file
View File

@ -0,0 +1,120 @@
# 文档处理系统
这是一个基于 FastAPI 的文档处理系统,可以将 DOC、DOCX 和 PDF 文件转换为纯文本格式。
## 系统要求
### 必需组件
1. Python 3.8 或更高版本
2. LibreOffice用于文档格式转换
- 下载地址https://www.libreoffice.org/download/download/
- 安装后需要将安装目录(通常是 `C:\Program Files\LibreOffice\program`)添加到系统 PATH 环境变量
3. Tesseract OCR用于图片文字识别
- 下载地址https://github.com/UB-Mannheim/tesseract/wiki
- 安装时选择"添加到系统路径"选项
### Python 依赖
所有必需的 Python 包都列在 `requirements.txt` 文件中。使用以下命令安装:
```bash
pip install -r requirements.txt
```
## 功能特点
- 支持 DOC、DOCX 和 PDF 文件格式
- 提供简单的拖拽上传界面
- 自动清理文档内容,去除冗余信息
- 输出整洁的纯文本文件
- 自动提取文档正文内容
- 支持图片中的文字识别OCR
- 自动分离正文和附录内容
- 自动下载处理后的文本文件
## 安装说明
1. 克隆项目到本地:
```bash
git clone <repository_url>
cd doc-etl
```
2. 创建虚拟环境(推荐):
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
```
3. 安装依赖:
```bash
pip install -r requirements.txt
```
## 运行说明
1. 启动服务器:
```bash
uvicorn main:app --reload
```
2. 打开浏览器访问:
```
http://localhost:8000
```
3. 在网页界面上传文件,系统会自动处理并返回转换后的文本文件。
## 目录结构
```
doc-etl/
├── main.py # 主应用程序
├── requirements.txt # 项目依赖
├── README.md # 项目说明
├── static/ # 静态文件
│ └── index.html # 上传页面
├── temp/ # 临时文件目录
└── cxs/ # 文档处理模块
└── cxs_doc_cleaner.py
```
## 注意事项
1. 确保系统已安装 Python 3.8 或更高版本
2. 处理 PDF 文件时需要安装额外的依赖
3. 所有临时文件会在以下情况自动清理:
- 文件处理完成后
- 发生错误时
- 文件下载完成后
- 程序退出时
4. 临时文件存储在 `temp` 目录中,该目录会在程序启动时自动清理
## 更新日志
### 2024-03-21
- 初始版本发布
- 支持基本的文档处理功能
- 添加文件上传界面
### 2024-03-xx
- 优化了文件处理逻辑
- 添加了更详细的错误处理
- 改进了文件类型验证
- 添加了处理进度显示
- 增强了临时文件的自动清理机制
### 2024-03-22
- 优化了批量处理逻辑,改为顺序处理文件
- 添加了文件处理状态实时显示
- 改进了临时文件清理机制
- 增强了错误处理和提示信息
### 2024-03-23
- 优化了图片文件夹命名规则,使用"文件名_随机ID"格式
- 改进了文件清理机制,处理完立即清理
- 添加了更多的日志输出,方便调试问题
- 优化了临时目录的管理和清理时机

View File

@ -0,0 +1,285 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
针对中文OCR的图像预处理优化
"""
import cv2
import numpy as np
from typing import Optional, Tuple, List, Dict, Any
def optimize_for_chinese(image: np.ndarray) -> np.ndarray:
"""
针对中文文本的图像优化处理
Args:
image: 输入图像的NumPy数组
Returns:
优化后的图像NumPy数组
"""
# 确保图像不为空
if image is None or image.size == 0:
raise ValueError("输入图像为空")
# 转换为灰度图
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image.copy()
# 1. 自适应二值化 - 对于不同分辨率和对比度的图像很有效
binary = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 25, 15
)
# 2. 对二值化图像进行形态学操作,使文字更清晰
# 创建一个长方形核,水平方向较小,垂直方向较大
# 这有助于保持中文字符的笔画连接
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 3))
# 闭运算 - 用于连接断开的部分,尤其对于中文细笔画非常有效
morph = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=1)
# 3. 降噪 - 去除小的噪点
# 查找所有轮廓
contours, _ = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 创建一个空白图像
cleaned = np.zeros_like(morph)
# 筛选轮廓 - 保留较大的轮廓(文字),去除较小的轮廓(噪点)
min_contour_area = 20 # 最小轮廓面积,可以根据实际情况调整
for contour in contours:
if cv2.contourArea(contour) > min_contour_area:
cv2.drawContours(cleaned, [contour], -1, 255, -1)
# 4. 反转回来 - 因为OCR通常需要黑底白字
cleaned_inverted = cv2.bitwise_not(cleaned)
# 5. 对图像进行锐化,提高轮廓清晰度
# 创建一个锐化核
sharpen_kernel = np.array([[-1,-1,-1],
[-1, 9,-1],
[-1,-1,-1]])
sharpened = cv2.filter2D(cleaned_inverted, -1, sharpen_kernel)
# 6. 确保图像完全二值化
_, final = cv2.threshold(sharpened, 127, 255, cv2.THRESH_BINARY)
return final
def optimize_for_chinese_advanced(image: np.ndarray) -> List[np.ndarray]:
"""
针对中文文本的多种高级图像优化处理返回多种优化结果
Args:
image: 输入图像的NumPy数组
Returns:
优化后的图像NumPy数组列表
"""
# 确保图像不为空
if image is None or image.size == 0:
raise ValueError("输入图像为空")
# 转换为灰度图
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image.copy()
results = []
# 方法1: 自适应二值化基础版
binary1 = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 25, 15
)
results.append(binary1)
# 方法2: 自适应二值化增强版
binary2 = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY, 35, 15
)
results.append(binary2)
# 方法3: Otsu二值化
_, binary3 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
results.append(binary3)
# 方法4: 应用高斯模糊后再Otsu二值化
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
_, binary4 = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
results.append(binary4)
# 方法5: 增强对比度后的二值化
# 创建CLAHE对象
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
# 应用CLAHE增强对比度
contrast_enhanced = clahe.apply(gray)
_, binary5 = cv2.threshold(contrast_enhanced, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
results.append(binary5)
# 方法6: 使用基本优化函数
basic_optimized = optimize_for_chinese(image)
results.append(basic_optimized)
# 方法7: 形态学操作
# 先进行二值化
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 创建一个椭圆核
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
# 开运算去除噪点
opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=1)
# 闭运算连接断开的笔画
morph = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations=1)
results.append(morph)
# 方法8: 锐化处理
sharpen_kernel = np.array([[-1,-1,-1],
[-1, 9,-1],
[-1,-1,-1]])
sharpened = cv2.filter2D(gray, -1, sharpen_kernel)
_, binary8 = cv2.threshold(sharpened, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
results.append(binary8)
# 方法9: 边缘增强
# 先进行高斯模糊
blurred = cv2.GaussianBlur(gray, (0, 0), 3)
# 使用unsharp masking技术
edge_enhanced = cv2.addWeighted(gray, 1.5, blurred, -0.5, 0)
_, binary9 = cv2.threshold(edge_enhanced, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
results.append(binary9)
return results
def detect_and_correct_skew(image: np.ndarray, angle_range: Tuple[int, int] = (-15, 15), angle_step: float = 0.5) -> np.ndarray:
"""
检测并修正图像中文本的倾斜
Args:
image: 输入图像的NumPy数组
angle_range: 搜索倾斜角度的范围
angle_step: 角度搜索的步长
Returns:
修正倾斜后的图像
"""
# 确保图像不为空
if image is None or image.size == 0:
raise ValueError("输入图像为空")
# 转换为灰度图
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image.copy()
# 二值化
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# 计算每个旋转角度的像素和
scores = []
angles = np.arange(angle_range[0], angle_range[1] + angle_step, angle_step)
# 获取中心点
center = (binary.shape[1] // 2, binary.shape[0] // 2)
for angle in angles:
# 旋转图像
rotation_matrix = cv2.getRotationMatrix2D(center, angle, 1.0)
rotated = cv2.warpAffine(binary, rotation_matrix, (binary.shape[1], binary.shape[0]),
flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_CONSTANT, borderValue=0)
# 计算每行像素和
row_sums = np.sum(rotated, axis=1)
# 计算方差作为评分
score = np.var(row_sums)
scores.append(score)
# 找到最佳角度
best_angle_index = np.argmax(scores)
best_angle = angles[best_angle_index]
# 旋转原始图像
rotation_matrix = cv2.getRotationMatrix2D(center, best_angle, 1.0)
rotated_image = cv2.warpAffine(image, rotation_matrix, (image.shape[1], image.shape[0]),
flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_CONSTANT)
return rotated_image
def process_image_for_chinese_ocr(image: np.ndarray, correct_skew: bool = True) -> Dict[str, Any]:
"""
完整的中文OCR图像预处理流程
Args:
image: 输入图像的NumPy数组
correct_skew: 是否进行倾斜校正
Returns:
字典包含多种处理结果和原始图像
"""
result = {
'original': image.copy()
}
# 步骤1: 倾斜校正(如果需要)
if correct_skew:
corrected = detect_and_correct_skew(image)
result['deskewed'] = corrected
# 使用校正后的图像进行后续处理
working_image = corrected
else:
working_image = image
# 步骤2: 应用基本的中文优化
optimized = optimize_for_chinese(working_image)
result['optimized'] = optimized
# 步骤3: 应用高级优化,获取多种处理结果
advanced_results = optimize_for_chinese_advanced(working_image)
for i, img in enumerate(advanced_results):
result[f'method_{i+1}'] = img
return result
if __name__ == "__main__":
# 简单的测试代码
import sys
if len(sys.argv) > 1:
input_image_path = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else "."
# 读取图像
image = cv2.imread(input_image_path)
if image is None:
print(f"无法读取图像: {input_image_path}")
sys.exit(1)
# 处理图像
result = process_image_for_chinese_ocr(image)
# 保存结果
cv2.imwrite(f"{output_dir}/original.png", result['original'])
cv2.imwrite(f"{output_dir}/optimized.png", result['optimized'])
if 'deskewed' in result:
cv2.imwrite(f"{output_dir}/deskewed.png", result['deskewed'])
for i in range(1, 10):
key = f'method_{i}'
if key in result:
cv2.imwrite(f"{output_dir}/{key}.png", result[key])
print(f"处理完成,结果已保存到 {output_dir}")
else:
print("使用方法: python _optimize_for_chinese.py <输入图像路径> [输出目录]")

1897
cxs/cxs_doc_cleaner.py Normal file

File diff suppressed because it is too large Load Diff

1173
cxs/cxs_pdf_cleaner.py Normal file

File diff suppressed because it is too large Load Diff

961
cxs/cxs_table_processor.py Normal file
View File

@ -0,0 +1,961 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
from typing import List, Dict, Any, Optional
import os
from docx.oxml import parse_xml
from docx.oxml.ns import nsdecls
# 自定义TableData类用于存储表格数据
class TableData:
def __init__(self):
"""
初始化表格数据结构
"""
self.rows = []
self.style = None
self.columns = [] # 添加列属性
def cell(self, row_idx: int, col_idx: int) -> Dict[str, Any]:
"""
获取表格单元格
Args:
row_idx: 行索引
col_idx: 列索引
Returns:
Dict: 单元格数据
"""
try:
# 首先检查行索引是否有效
if row_idx < 0 or row_idx >= len(self.rows):
return {'text': '', 'gridspan': 1, 'vmerge': None}
# 然后检查列索引是否有效
if col_idx < 0 or col_idx >= len(self.rows[row_idx]):
return {'text': '', 'gridspan': 1, 'vmerge': None}
# 如果需要,进行额外的安全检查
cell = self.rows[row_idx][col_idx]
if not isinstance(cell, dict):
print(f"警告:单元格数据格式错误 [{row_idx},{col_idx}]")
return {'text': str(cell) if cell is not None else '', 'gridspan': 1, 'vmerge': None}
return cell
except Exception as e:
print(f"获取单元格时出错 [{row_idx},{col_idx}]: {str(e)}")
return {'text': '', 'gridspan': 1, 'vmerge': None}
class TableProcessor:
def __init__(self):
"""
初始化表格处理器
"""
print("初始化表格处理器")
def _extract_table_row(self, row_element, namespace):
"""
提取表格行数据增强的表格行处理
Args:
row_element: 行元素
namespace: XML命名空间
Returns:
List: 行数据列表
"""
row = []
try:
# 处理单元格
for cell_element in row_element.findall('.//w:tc', namespaces=namespace):
cell_text = ''
# 提取单元格中的所有文本
for paragraph in cell_element.findall('.//w:p', namespaces=namespace):
for run in paragraph.findall('.//w:t', namespaces=namespace):
if run.text:
cell_text += run.text
# 在段落后添加换行符
cell_text += '\n'
# 移除末尾换行
cell_text = cell_text.rstrip('\n')
# 检查单元格合并属性
gridspan = self._get_gridspan_value(cell_element)
vmerge = self._get_vmerge_value(cell_element)
# 创建单元格数据
cell = {
'text': cell_text,
'gridspan': gridspan,
'vmerge': vmerge
}
row.append(cell)
# 如果行为空,创建至少一个空单元格
if not row:
row.append({'text': '', 'gridspan': 1, 'vmerge': None})
return row
except Exception as e:
print(f"提取表格行数据时出错: {str(e)}")
# 返回至少有一个单元格的行
return [{'text': '', 'gridspan': 1, 'vmerge': None}]
def _preprocess_table(self, element, namespace):
"""
对表格进行预处理加强特殊表格的识别能力
Args:
element: 表格元素
namespace: XML命名空间
Returns:
TableData: 预处理后的表格数据
"""
table = TableData()
# 检查并处理表格行
rows_elements = element.findall('.//w:tr', namespaces=namespace)
# 表格为空的特殊处理
if not rows_elements:
# 尝试寻找更深层次的表格元素,可能是嵌套在其他元素中的表格
nested_rows = element.findall('.//*//w:tr', namespaces=namespace)
if nested_rows:
rows_elements = nested_rows
print(f"已找到嵌套表格行:{len(rows_elements)}")
else:
# 创建一个默认行,避免表格为空
print("未找到表格行,创建默认行")
table.rows.append([{'text': '', 'gridspan': 1, 'vmerge': None}])
return table
# 处理每一行
for row_element in rows_elements:
row = self._extract_table_row(row_element, namespace)
table.rows.append(row)
# 如果表格为空,创建默认行
if not table.rows:
table.rows.append([{'text': '', 'gridspan': 1, 'vmerge': None}])
# 分析表格,确定列数
max_cols = 0
for row in table.rows:
# 计算考虑gridspan的实际列数
effective_cols = sum(cell.get('gridspan', 1) for cell in row)
max_cols = max(max_cols, effective_cols)
# 确保每行都有足够的列
for i, row in enumerate(table.rows):
current_cols = sum(cell.get('gridspan', 1) for cell in row)
if current_cols < max_cols:
# 添加空单元格来填充行
padding_cells = max_cols - current_cols
for _ in range(padding_cells):
row.append({'text': '', 'gridspan': 1, 'vmerge': None})
# 设置列索引
table.columns = [i for i in range(max_cols)]
# 增强对垂直合并单元格的处理
self._enhance_vertical_merges(table)
# 额外执行一次垂直合并内容传播,修复复杂表格中的合并单元格
self._propagate_vertical_merges(table)
return table
def _propagate_vertical_merges(self, table: TableData):
"""
专门处理复杂表格中的垂直合并单元格向下传播内容
Args:
table: TableData对象
"""
rows = len(table.rows)
cols = len(table.columns) if table.columns else 0
if rows <= 1 or cols == 0:
return
# 创建一个矩阵记录每个单元格位置的内容
matrix = []
for i in range(rows):
row = []
for j in range(cols):
try:
if j < len(table.rows[i]):
cell = table.rows[i][j]
row.append(cell.get('text', '').strip())
else:
row.append('')
except (IndexError, KeyError):
row.append('') # 防止索引越界
matrix.append(row)
# 对每一列进行垂直合并检查
for j in range(cols):
# 从上到下传播非空内容
last_non_empty = None
last_non_empty_idx = -1
for i in range(rows):
try:
# 安全访问表格单元格
current_text = ''
if j < len(table.rows[i]):
cell = table.rows[i][j]
current_text = cell.get('text', '').strip()
# 如果当前单元格为空,但上方有非空单元格,考虑垂直合并
if not current_text and last_non_empty:
# 检查这是否可能是垂直合并
if i - last_non_empty_idx <= 3: # 限制垂直检查范围,避免过度填充
# 根据上下文判断是否真的是合并单元格
# 1. 检查该列其他单元格是否有相似模式
pattern_match = False
for k in range(rows):
if k != i and k != last_non_empty_idx:
# 查找相似模式:空单元格下方接非空单元格
if k > 0 and not matrix[k-1][j] and matrix[k][j]:
pattern_match = True
break
# 2. 检查第一列特殊情况 - 可能是分类表
is_first_columns = j < 2 # 前两列更可能是分类信息
if pattern_match or is_first_columns:
if j < len(table.rows[i]):
# 安全地更新当前单元格
table.rows[i][j]['text'] = last_non_empty
table.rows[i][j]['is_inferred_merge'] = True
matrix[i][j] = last_non_empty # 更新矩阵
print(f"传播合并内容到位置 [{i},{j}]: {last_non_empty[:20]}...")
# 更新最后一个非空单元格
if current_text:
last_non_empty = current_text
last_non_empty_idx = i
except Exception as e:
print(f"处理垂直合并传播时出错 [{i},{j}]: {str(e)}")
# 第二轮:处理常见的分类表格模式(第一列相同值表示同一类别)
for j in range(min(2, cols)): # 只处理前两列
# 查找具有相同值的行组
groups = {}
for i in range(rows):
try:
if j < len(table.rows[i]):
value = table.rows[i][j].get('text', '').strip()
if value:
if value not in groups:
groups[value] = []
groups[value].append(i)
except Exception as e:
print(f"分组时出错 [{i},{j}]: {str(e)}")
# 处理每个组
for value, indices in groups.items():
if len(indices) >= 2: # 至少有两行具有相同值
# 检查这些行之间是否有空行
indices.sort()
for idx in range(len(indices) - 1):
start_row = indices[idx]
end_row = indices[idx + 1]
# 如果两行不相邻,检查中间行
if end_row - start_row > 1:
for mid_row in range(start_row + 1, end_row):
try:
# 检查中间行的单元格是否为空
if j < len(table.rows[mid_row]):
mid_cell = table.rows[mid_row][j]
if not mid_cell.get('text', '').strip():
# 这可能是被合并的单元格,填充内容
mid_cell['text'] = value
mid_cell['is_inferred_merge'] = True
print(f"填充中间行合并单元格 [{mid_row},{j}]: {value[:20]}...")
except Exception as e:
print(f"填充中间行时出错 [{mid_row},{j}]: {str(e)}")
def _enhance_vertical_merges(self, table: TableData):
"""
增强对垂直合并单元格的处理
处理逻辑包括
1. 检查并处理第一列和第二列的特殊情况
2. 在表格中识别内容相似的单元格
Args:
table: TableData对象
"""
rows = len(table.rows)
cols = len(table.columns) if table.columns else 0
if rows <= 1 or cols == 0:
return
# 检查第一列和第二列的特殊情况
for j in range(min(2, cols)): # 检查前两列,因为合并单元格可能出现在这两列中
# 检查是否有垂直合并单元格
for i in range(1, rows):
try:
if j < len(table.rows[i]): # 确保索引有效
cell = table.rows[i][j]
# 如果单元格为空且没有标记为合并单元格,检查上面行的内容
if not cell.get('text', '').strip() and cell.get('vmerge') is None:
# 安全访问上一行
if j < len(table.rows[i-1]):
prev_cell = table.rows[i-1][j]
if prev_cell.get('text', '').strip():
# 如果上面行有内容,这可能是合并单元格
print(f"在位置 [{i},{j}] 检测到可能的垂直合并单元格")
# 将内容复制到当前单元格
cell['text'] = prev_cell['text']
cell['is_inferred_merge'] = True # 标记为推导出的合并单元格
except IndexError as e:
print(f"增强垂直合并处理索引错误 [{i},{j}]: {str(e)}")
except Exception as e:
print(f"增强垂直合并处理一般错误 [{i},{j}]: {str(e)}")
# 特殊情况:检查分类表格中的模式
try:
# 在分类表格中,同一列的内容如果重复出现,可能是合并单元格
content_groups = self._identify_content_groups(table, j)
# 处理内容相似的单元格
for group_indices in content_groups:
if len(group_indices) > 1: # 如果有多个相同的单元格
if group_indices[0] < len(table.rows) and j < len(table.rows[group_indices[0]]):
group_text = table.rows[group_indices[0]][j].get('text', '')
if group_text.strip(): # 如果单元格有内容
print(f"在列 {j} 中发现可能的内容合并组: {group_indices}")
# 将这些单元格标记为具有相同的内容
for idx in group_indices:
if idx < len(table.rows) and j < len(table.rows[idx]):
table.rows[idx][j]['content_group'] = group_indices
except Exception as e:
print(f"处理内容组时出错 [列 {j}]: {str(e)}")
def _identify_content_groups(self, table: TableData, col_idx: int) -> List[List[int]]:
"""
根据内容相似性识别合并单元格
Args:
table: TableData对象
col_idx: 要分析的列索引
Returns:
List[List[int]]: 可能合并单元格的行索引组
"""
rows = len(table.rows)
# 存储每个唯一内容的所有行索引
content_groups = {}
for i in range(rows):
try:
if col_idx < len(table.rows[i]):
cell_text = table.rows[i][col_idx].get('text', '').strip()
if cell_text:
if cell_text not in content_groups:
content_groups[cell_text] = []
content_groups[cell_text].append(i)
except IndexError:
# 安全跳过索引越界情况
continue
except Exception as e:
print(f"识别内容组时出错 [{i},{col_idx}]: {str(e)}")
# 返回包含多个行索引的组
return [indices for text, indices in content_groups.items() if len(indices) > 1]
def _is_valid_table(self, table: TableData) -> bool:
"""
检查表格是否有效至少有一行一列且含有有意义的内容
Args:
table: TableData对象
Returns:
bool: 表格是否有效
"""
try:
# 检查表格尺寸
rows = len(table.rows)
cols = len(table.columns) if table.columns else 0
# 如果没有行或列,表格无效
if rows < 1 or cols < 1:
print(f"表格无效: 没有行或列 (行数={rows}, 列数={cols})")
return False
# 检查表格XML结构是否包含表格标记
# 此步骤可以简单检测表格是否有表格相关的XML标记
try:
# 以下逻辑是为了特殊处理可能被误判的表格
# 判断是否是特殊表格(如药品分类表)
first_cell_text = ""
if rows > 0 and len(table.rows[0]) > 0:
first_cell_text = table.cell(0, 0).get('text', '').strip()
# 检查首行首列是否包含特定文本模式(如编号、分类名称等)
# 这些模式暗示这可能是一个重要表格
special_patterns = [
r'^\d{2}-\d{2}', # 类似 01-01 的编码
r'^[一二三四五六七八九十]+级', # 中文级别(一级、二级等)
r'^\d+\.\d+', # 类似 1.1 的编号格式
r'类[别型]|分类|编码', # 包含分类相关词汇
r'\s*\d+', # 表格编号(如"表1"
r'产品|器械|设备|材料' # 常见医疗或药品分类术语
]
for pattern in special_patterns:
if re.search(pattern, first_cell_text):
print(f"检测到特殊表格模式: '{first_cell_text}',强制视为有效表格")
return True
except Exception as e:
# 特殊检测失败,继续常规检测
print(f"特殊表格检测时出错: {str(e)}")
# 计算表格中的有效内容
total_cells = 0
non_empty_cells = 0
total_text_length = 0
for i in range(rows):
for j in range(min(cols, len(table.rows[i]))): # 防止越界
total_cells += 1
cell_text = table.cell(i, j)['text'].strip()
if cell_text:
non_empty_cells += 1
total_text_length += len(cell_text)
# 计算非空单元格比例
non_empty_ratio = non_empty_cells / total_cells if total_cells > 0 else 0
# 表格行列数检查 - 如果行数或列数足够多,更可能是有效表格
has_multiple_rows = rows >= 3
has_multiple_cols = cols >= 3
# 实际单元格内容检查
# 进一步放宽标准,只要有内容就视为可能有效
is_meaningful = (
# 1. 标准条件至少有2个单元格有内容
non_empty_cells >= 2 or
# 2. 极低门槛至少有1个单元格有内容且文本长度>=1个字符
(non_empty_cells > 0 and total_text_length >= 1) or
# 3. 表格足够大至少有3行3列可能是重要表格
(has_multiple_rows and has_multiple_cols) or
# 4. 非空率较高:即使单元格少,但如果填充率高,也可能是有意义的
(non_empty_ratio >= 0.5 and total_text_length > 0)
)
if not is_meaningful:
print(f"表格无效: 内容不足 (非空单元格={non_empty_cells}/{total_cells}, 文本长度={total_text_length})")
return is_meaningful
except Exception as e:
print(f"警告:检查表格有效性时出错: {str(e)}")
import traceback
traceback.print_exc()
# 出错时默认认为有效,避免丢失潜在有用的表格
return True
def _extract_plain_text_from_table(self, table: TableData) -> str:
"""
从表格中提取纯文本用于将无效表格作为普通文本处理
Args:
table: docx表格对象
Returns:
str: 表格内容的纯文本表示
"""
try:
text_parts = []
for row in table.rows:
for cell in row:
cell_text = cell['text'].strip()
if cell_text:
text_parts.append(cell_text)
return " ".join(text_parts)
except Exception as e:
print(f"警告:从表格提取文本时出错: {str(e)}")
return "【表格文本提取失败】"
def _convert_table_to_text(self, table: TableData) -> str:
"""
将表格转换为文本格式使用简化易读的表格表示
Args:
table: TableData对象
Returns:
str: 表格的文本表示
"""
try:
# 获取表格的行数和列数
rows = len(table.rows)
cols = len(table.columns) if table.columns else 0
if rows == 0 or cols == 0:
return "【空表格】"
# 构建一个完整的表格矩阵,处理合并单元格
matrix = []
for i in range(rows):
row = [""] * cols
matrix.append(row)
# 首先安全地处理所有已知的单元格内容
for i in range(rows):
for j in range(cols):
try:
if j < len(table.rows[i]):
cell = table.rows[i][j]
text = cell.get('text', '').strip()
matrix[i][j] = text
except IndexError:
continue # 跳过索引越界
# 填充矩阵,处理合并单元格
for i in range(rows):
j = 0
while j < cols:
try:
if j >= len(table.rows[i]):
j += 1
continue
cell = table.rows[i][j]
text = cell.get('text', '').strip()
# 特殊处理:检查是否有内容组标记
content_group = cell.get('content_group', [])
if content_group:
# 如果这是内容组的一部分
if i in content_group and content_group[0] < len(table.rows) and j < len(table.rows[content_group[0]]):
group_text = table.rows[content_group[0]][j].get('text', '').strip()
if group_text:
text = group_text
# 处理水平合并(gridspan)
gridspan = cell.get('gridspan', 1)
# 处理垂直合并(vmerge)和推断的合并
if cell.get('vmerge') == 'continue' or cell.get('is_inferred_merge'):
# 如果是继续合并的单元格或推断的合并,使用当前已有的文本
if not text:
# 如果当前单元格文本为空,尝试从上面行查找
for prev_i in range(i-1, -1, -1):
if prev_i < len(table.rows) and j < len(table.rows[prev_i]):
prev_cell = table.rows[prev_i][j]
prev_text = prev_cell.get('text', '').strip()
if prev_text:
text = prev_text
break
# 填充当前单元格
matrix[i][j] = text
# 处理水平合并,将内容复制到被合并的单元格
for k in range(1, gridspan):
if j + k < cols:
matrix[i][j+k] = text
# 如果这是垂直合并的起始单元格,复制内容到下面被合并的单元格
if text and (cell.get('vmerge') == 'restart' or not cell.get('vmerge')):
for next_i in range(i+1, rows):
if next_i < len(table.rows) and j < len(table.rows[next_i]):
next_cell = table.rows[next_i][j]
if next_cell.get('vmerge') == 'continue' or not next_cell.get('text', '').strip():
# 复制到下面被合并的单元格
matrix[next_i][j] = text
# 处理水平合并
next_gridspan = next_cell.get('gridspan', 1)
for k in range(1, next_gridspan):
if j + k < cols:
matrix[next_i][j+k] = text
else:
break
j += max(1, gridspan)
except IndexError as e:
print(f"表格转文本处理索引错误 [{i},{j}]: {str(e)}")
j += 1 # 确保进度
except Exception as e:
print(f"表格转文本一般错误 [{i},{j}]: {str(e)}")
j += 1
# 再次处理第一列和第二列中的空白单元格 - 增强垂直合并处理
for j in range(min(3, cols)): # 扩展到前三列
# 自上而下扫描
last_content = ""
for i in range(rows):
if matrix[i][j]:
last_content = matrix[i][j]
elif last_content and i > 0 and matrix[i-1][j]:
# 如果当前为空且上一行不为空,填充内容
matrix[i][j] = last_content
# 自下而上扫描,填充孤立的空单元格
for i in range(rows-2, 0, -1): # 从倒数第二行开始向上
if not matrix[i][j] and matrix[i-1][j] and matrix[i+1][j] and matrix[i-1][j] == matrix[i+1][j]:
# 如果当前为空且上下行内容相同,填充内容
matrix[i][j] = matrix[i-1][j]
# 如果有表头,提取它们
headers = matrix[0] if rows > 0 else ["" + str(j+1) for j in range(cols)]
# 确保表头不为空
for j in range(cols):
if not headers[j]:
headers[j] = "" + str(j+1)
# 构建结构化输出 - 使用统一简化格式
result = []
result.append("表格内容(简化格式):")
# 添加表头行
header_line = []
# 计算每列最大宽度
col_widths = [0] * cols
for j in range(cols):
col_widths[j] = max(len(headers[j]), col_widths[j])
# 计算数据行的宽度
for i in range(1, rows):
for j in range(cols):
if matrix[i][j]:
col_widths[j] = max(col_widths[j], len(matrix[i][j]))
# 加入表头与分隔线
for j in range(cols):
header_line.append(headers[j].ljust(col_widths[j]))
result.append(" | ".join(header_line))
# 添加分隔线
separator = []
for j in range(cols):
separator.append("-" * col_widths[j])
result.append(" | ".join(separator))
# 添加数据行
for i in range(1, rows):
row_line = []
has_content = False
for j in range(cols):
cell_text = matrix[i][j]
if cell_text:
has_content = True
# 始终添加单元格内容,即使为空
row_line.append(cell_text.ljust(col_widths[j]))
if has_content:
result.append(" | ".join(row_line))
return "\n".join(result)
except Exception as e:
print(f"警告:处理表格时出错: {str(e)}")
import traceback
traceback.print_exc()
return "【表格处理失败】"
def _convert_table_to_markdown(self, table: TableData) -> str:
"""
将表格转换为Markdown格式使用简化易读的表格表示
Args:
table: TableData对象
Returns:
str: 表格的Markdown表示
"""
try:
# 获取表格的行数和列数
rows = len(table.rows)
cols = len(table.columns) if table.columns else 0
if rows == 0 or cols == 0:
return "| 空表格 |"
# 构建一个完整的表格矩阵,处理合并单元格
matrix = []
for i in range(rows):
row = [""] * cols
matrix.append(row)
# 首先安全地处理所有已知的单元格内容
for i in range(rows):
for j in range(cols):
try:
if j < len(table.rows[i]):
cell = table.rows[i][j]
text = cell.get('text', '').strip()
matrix[i][j] = text
except IndexError:
continue # 跳过索引越界
# 填充矩阵,处理合并单元格
for i in range(rows):
j = 0
while j < cols:
try:
if j >= len(table.rows[i]):
j += 1
continue
cell = table.rows[i][j]
text = cell.get('text', '').strip()
# 特殊处理:检查是否有内容组标记
content_group = cell.get('content_group', [])
if content_group and i in content_group:
# 如果这是内容组的一部分,保证内容的一致性
if content_group[0] < len(table.rows) and j < len(table.rows[content_group[0]]):
group_text = table.rows[content_group[0]][j].get('text', '').strip()
if group_text:
text = group_text
# 处理水平合并(gridspan)
gridspan = cell.get('gridspan', 1)
# 处理垂直合并(vmerge)和推断的合并
if cell.get('vmerge') == 'continue' or cell.get('is_inferred_merge'):
# 如果是继续合并的单元格或推断的合并,使用当前已有的文本
if not text:
# 如果当前单元格文本为空,尝试从上面行查找
for prev_i in range(i-1, -1, -1):
if prev_i < len(table.rows) and j < len(table.rows[prev_i]):
prev_cell = table.rows[prev_i][j]
prev_text = prev_cell.get('text', '').strip()
if prev_text:
text = prev_text
break
# 填充当前单元格
matrix[i][j] = text
# 处理水平合并,将内容复制到被合并的单元格
for k in range(1, gridspan):
if j + k < cols:
matrix[i][j+k] = text
# 如果这是垂直合并的起始单元格,复制内容到下面被合并的单元格
if text and (cell.get('vmerge') == 'restart' or not cell.get('vmerge')):
for next_i in range(i+1, rows):
if next_i < len(table.rows) and j < len(table.rows[next_i]):
next_cell = table.rows[next_i][j]
if next_cell.get('vmerge') == 'continue' or not next_cell.get('text', '').strip():
# 复制到下面被合并的单元格
matrix[next_i][j] = text
# 处理水平合并
next_gridspan = next_cell.get('gridspan', 1)
for k in range(1, next_gridspan):
if j + k < cols:
matrix[next_i][j+k] = text
else:
break
j += max(1, gridspan)
except Exception as e:
print(f"Markdown表格处理错误 [{i},{j}]: {str(e)}")
j += 1
# 再次处理第一列中的空白单元格 - 增强垂直合并处理
for j in range(min(3, cols)): # 扩展到前三列
# 自上而下扫描
last_content = ""
for i in range(rows):
if matrix[i][j]:
last_content = matrix[i][j]
elif last_content and i > 0 and matrix[i-1][j]:
# 如果当前为空且上一行不为空,填充内容
matrix[i][j] = last_content
# 确保表头不为空
headers = matrix[0] if rows > 0 else []
for j in range(cols):
if j >= len(headers) or not headers[j]:
headers.append("" + str(j+1))
# 构建Markdown表格
markdown_rows = []
# 添加表头行
header_row = "| " + " | ".join(headers) + " |"
markdown_rows.append(header_row)
# 添加分隔行
separator = "| " + " | ".join(["---"] * cols) + " |"
markdown_rows.append(separator)
# 添加数据行
for i in range(1, rows):
row_data = []
has_content = False
for j in range(cols):
cell_text = matrix[i][j]
if cell_text:
has_content = True
row_data.append(cell_text)
if has_content:
markdown_rows.append("| " + " | ".join(row_data) + " |")
return "\n".join(markdown_rows)
except Exception as e:
print(f"警告处理Markdown表格时出错: {str(e)}")
import traceback
traceback.print_exc()
return "| 表格处理失败 |"
def _extract_table_text(self, table: TableData) -> str:
"""
提取表格中的文本内容返回格式化的文本表示
Args:
table: docx表格对象
Returns:
str: 表格内容的文本表示
"""
# 调用优化后的表格处理函数,确保合并单元格被正确处理
return self._convert_table_to_text(table)
def _convert_small_table_to_text(self, table: TableData) -> str:
"""
将小型表格转换为更简洁的文本格式
Args:
table: TableData对象
Returns:
str: 表格的文本表示
"""
rows = len(table.rows)
cols = len(table.columns) if table.columns else 0
if rows == 0 or cols == 0:
return "【空表格】"
# 提取所有单元格文本
cell_texts = []
for i in range(rows):
row_texts = []
for j in range(min(cols, len(table.rows[i]))):
cell_text = table.cell(i, j)['text'].strip().replace('\n', ' ')
row_texts.append(cell_text)
cell_texts.append(row_texts)
# 计算每列的最大宽度
col_widths = [0] * cols
for i in range(rows):
for j in range(len(cell_texts[i])):
col_widths[j] = max(col_widths[j], len(cell_texts[i][j]))
# 生成表格文本
result = []
# 添加表头
header_row = cell_texts[0]
header_line = []
for j, text in enumerate(header_row):
width = min(col_widths[j], 30) # 限制最大宽度
header_line.append(text.ljust(width))
result.append(" | ".join(header_line))
# 添加分隔线
separator = []
for j in range(cols):
width = min(col_widths[j], 30)
separator.append("-" * width)
result.append(" | ".join(separator))
# 添加数据行
for i in range(1, rows):
row_line = []
for j, text in enumerate(cell_texts[i]):
width = min(col_widths[j], 30) # 限制最大宽度
row_line.append(text.ljust(width))
result.append(" | ".join(row_line))
return "\n".join(result)
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(self._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(self._qn('w:val')):
return int(gridspan[0].get(self._qn('w:val')))
except (ValueError, TypeError, AttributeError) as e:
print(f"警告获取gridspan值时出错: {str(e)}")
return 1 # 默认返回1表示没有合并
def _get_vertical_span(self, table: TableData, 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 cell.get('vmerge') == 'continue':
span += 1
else:
break
return span
def _qn(self, tag: str) -> str:
"""
将标签转换为带命名空间的格式
Args:
tag: 原始标签
Returns:
str: 带命名空间的标签
"""
prefix = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
return prefix + tag

View File

@ -0,0 +1,183 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import re
import json
import argparse
def split_text_into_paragraphs(text):
"""
将连续文本智能分段
策略:
1. 识别表格标记将表格内容作为独立段落处理
2. 对普通文本按照语义和长度适当分段约500字/
3. 确保分段不破坏语义完整性
"""
# 清理文本中可能存在的多余空格
text = re.sub(r'\s+', ' ', text).strip()
# 识别表格范围,表格以"表格 N 开始"和"表格 N 结束"标记
table_pattern = re.compile(r'表格\s*\d+\s*开始(.*?)表格\s*\d+\s*结束', re.DOTALL)
# 使用表格标记分割文本
parts = []
last_end = 0
for match in table_pattern.finditer(text):
# 添加表格前的文本
if match.start() > last_end:
parts.append(("text", text[last_end:match.start()]))
# 获取表格内容(去掉表格标记)
table_content = match.group(1).strip()
parts.append(("table", table_content))
last_end = match.end()
# 添加最后一个表格之后的文本
if last_end < len(text):
parts.append(("text", text[last_end:]))
# 如果没有找到表格,则整个文本作为一个文本片段
if not parts:
parts = [("text", text)]
# 对文本段落进行处理
final_paragraphs = []
# 可能表示段落边界或重要语义分割点的标记
paragraph_markers = [
r'^第.{1,3}章',
r'^第.{1,3}节',
r'^[一二三四五六七八九十][、.\s]',
r'^\d+[、.\s]',
r'^[IVX]+[、.\s]',
r'^附录',
r'^前言',
r'^目录',
r'^摘要',
r'^引言',
r'^结论',
r'^参考文献'
]
marker_pattern = re.compile('|'.join(paragraph_markers))
# 按句子分割的分隔符
sentence_separators = r'([。!?\!\?])'
# 目标段落长度(字符数)
target_length = 500
# 最小段落长度阈值
min_length = 100
# 最大段落长度阈值
max_length = 800
for part_type, content in parts:
# 如果是表格内容,直接添加为独立段落
if part_type == "table":
final_paragraphs.append(content)
continue
# 处理普通文本
# 按句子分割文本
sentences = re.split(sentence_separators, content)
# 将分割后的句子和标点符号重新组合
sentence_list = []
for i in range(0, len(sentences)-1, 2):
if i+1 < len(sentences):
sentence_list.append(sentences[i] + sentences[i+1])
else:
sentence_list.append(sentences[i])
# 如果最后一个元素不是句子结束符,添加它
if len(sentences) % 2 == 1:
if sentences[-1]:
sentence_list.append(sentences[-1])
# 构建段落
current_para = ""
for sentence in sentence_list:
# 检查是否是段落标记的开始
is_marker = marker_pattern.search(sentence)
# 如果当前段落已经足够长,或者遇到段落标记,则开始新段落
if ((len(current_para) >= target_length and len(current_para) + len(sentence) > max_length) or
(is_marker and current_para)):
if current_para.strip():
final_paragraphs.append(current_para.strip())
current_para = sentence
else:
current_para += sentence
# 添加最后一个段落
if current_para.strip():
final_paragraphs.append(current_para.strip())
# 对段落进行后处理,合并过短的段落
processed_paragraphs = []
temp_para = ""
for para in final_paragraphs:
if len(para) < min_length:
# 如果段落太短,尝试与临时段落合并
if temp_para:
temp_para += " " + para
else:
temp_para = para
else:
# 如果有临时段落,先处理它
if temp_para:
# 如果临时段落也很短,合并到当前段落
if len(temp_para) < min_length:
para = temp_para + " " + para
else:
processed_paragraphs.append(temp_para)
temp_para = ""
processed_paragraphs.append(para)
# 处理最后可能剩余的临时段落
if temp_para:
if processed_paragraphs and len(temp_para) < min_length:
processed_paragraphs[-1] += " " + temp_para
else:
processed_paragraphs.append(temp_para)
return processed_paragraphs
def save_to_json(paragraphs, output_file):
"""将段落保存为JSON格式"""
data = {
"total_paragraphs": len(paragraphs),
"paragraphs": paragraphs
}
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"成功将文本分成 {len(paragraphs)} 个段落并保存到 {output_file}")
def main():
parser = argparse.ArgumentParser(description="将连续文本智能分段并保存为JSON")
parser.add_argument("input_file", help="输入文本文件路径")
parser.add_argument("--output", "-o", default="paragraphs.json", help="输出JSON文件路径")
args = parser.parse_args()
# 读取输入文件
try:
with open(args.input_file, 'r', encoding='utf-8') as f:
text = f.read()
except Exception as e:
print(f"读取文件出错: {e}")
return
# 分段
paragraphs = split_text_into_paragraphs(text)
# 保存为JSON
save_to_json(paragraphs, args.output)
if __name__ == "__main__":
main()

500
cxs/main.py Normal file
View File

@ -0,0 +1,500 @@
from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
import os
import tempfile
from pathlib import Path
import uuid
import sys
import shutil
import glob
import asyncio
from typing import List
import json
import atexit
import re
import time # 添加time模块导入
# 获取当前文件所在目录的绝对路径
CURRENT_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
if str(CURRENT_DIR) not in sys.path:
sys.path.append(str(CURRENT_DIR))
# 定义目录
TEMP_DIR = CURRENT_DIR / "temp"
STATIC_DIR = CURRENT_DIR / "static"
UPLOAD_DIR = TEMP_DIR / "uploads"
OUTPUT_DIR = TEMP_DIR / "outputs"
IMAGES_DIR = TEMP_DIR / "images" # 添加图片目录
# 确保所有必要的目录都存在
def ensure_directories():
"""确保所有必要的目录都存在且具有正确的权限"""
directories = [TEMP_DIR, STATIC_DIR, UPLOAD_DIR, OUTPUT_DIR, IMAGES_DIR]
for directory in directories:
try:
# 只在目录不存在时创建
if not directory.exists():
directory.mkdir(parents=True, exist_ok=True)
print(f"创建目录: {directory}")
# 在 Windows 上设置目录权限
if os.name == 'nt':
os.system(f'icacls "{directory}" /grant Everyone:(OI)(CI)F /T')
print(f"设置目录权限: {directory}")
except Exception as e:
print(f"创建目录失败 {directory}: {e}")
raise
def clean_temp_directories():
"""清理临时目录中的内容,但保留目录结构"""
try:
# 只清理临时目录中的内容
for directory in [UPLOAD_DIR, OUTPUT_DIR, IMAGES_DIR]:
if directory.exists():
print(f"清理目录: {directory}")
# 删除目录中的所有文件和子目录
for item in directory.glob("*"):
try:
if item.is_file():
item.unlink()
print(f"删除文件: {item}")
elif item.is_dir():
shutil.rmtree(str(item))
print(f"删除目录: {item}")
except Exception as e:
print(f"清理项目失败 {item}: {e}")
except Exception as e:
print(f"清理临时目录失败: {e}")
# 初始化目录
ensure_directories()
try:
from cxs_doc_cleaner import DocCleaner
except ImportError as e:
print(f"导入错误: {e}")
print(f"当前目录: {CURRENT_DIR}")
print(f"Python路径: {sys.path}")
raise
app = FastAPI(debug=True)
# 配置CORS
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"],
expose_headers=["*"]
)
# API 路由
@app.options("/api/upload/")
async def upload_options():
return {}
@app.post("/api/upload/")
async def upload_files(request: Request, files: List[UploadFile] = File(...)):
"""处理文件上传"""
print(f"收到上传请求: {request.method} {request.url}")
print(f"请求头: {request.headers}")
print(f"收到的文件数量: {len(files)}")
# 确保目录存在
ensure_directories()
# 检查是否有文件上传
if not files:
return {
"results": [],
"error": "没有上传文件"
}
results = []
cleaner = None
try:
# 创建文档处理器
cleaner = DocCleaner()
print("成功创建DocCleaner实例")
# 一次只处理一个文件
for index, file in enumerate(files):
print(f"\n开始处理第 {index + 1}/{len(files)} 个文件: {file.filename}")
temp_file = None
output_file = None
try:
# 保存上传的文件
temp_file, save_error = await save_uploaded_file(file)
if save_error or not temp_file:
print(f"保存文件失败: {save_error}")
results.append({
"filename": file.filename,
"status": "error",
"error": save_error or "保存文件失败",
"output_file": None,
"markdown_file": None,
"content": None
})
continue
print(f"文件已保存到临时位置: {temp_file}")
# 检查文件类型
file_ext = Path(file.filename).suffix.lower()
supported_formats = {
'.doc': 'word',
'.docx': 'word',
'.pdf': 'pdf',
'.html': 'html',
'.htm': 'html',
'.xls': 'excel',
'.xlsx': 'excel'
}
if file_ext not in supported_formats:
print(f"不支持的文件类型: {file_ext}")
results.append({
"filename": file.filename,
"status": "error",
"error": f"不支持的文件类型: {file_ext}",
"output_file": None,
"markdown_file": None,
"content": None
})
if temp_file.exists():
temp_file.unlink()
continue
# 确保文件存在
if not temp_file.exists():
print(f"错误:临时文件不存在: {temp_file}")
results.append({
"filename": file.filename,
"status": "error",
"error": "临时文件不存在",
"output_file": None,
"markdown_file": None,
"content": None
})
continue
print(f"开始处理文件内容: {temp_file}")
# 处理文件
output_file, text_content, markdown_file, error = await process_single_file(str(temp_file), cleaner)
# 处理完成后删除临时文件
if temp_file and temp_file.exists():
# 修改为使用安全删除函数
if safe_delete_file(temp_file):
print(f"删除临时文件: {temp_file}")
else:
print(f"警告:无法完全删除临时文件,但处理已成功完成: {temp_file}")
if error:
print(f"处理文件时出错: {error}")
results.append({
"filename": file.filename,
"status": "error",
"error": str(error),
"output_file": None,
"markdown_file": None,
"content": None
})
continue
# 创建响应文件
response_file = OUTPUT_DIR / f"response_{Path(file.filename).stem}_output.txt"
response_markdown = OUTPUT_DIR / f"response_{Path(file.filename).stem}_output.md"
print(f"创建响应文件: {response_file}")
print(f"创建Markdown响应文件: {response_markdown}")
if output_file and Path(output_file).exists():
shutil.copy2(output_file, str(response_file))
print(f"复制输出文件到响应文件: {output_file} -> {response_file}")
# 复制Markdown文件
if markdown_file and Path(markdown_file).exists():
shutil.copy2(markdown_file, str(response_markdown))
print(f"复制Markdown文件到响应文件: {markdown_file} -> {response_markdown}")
# 删除原始输出文件
Path(output_file).unlink()
print(f"删除原始输出文件: {output_file}")
# 删除原始Markdown文件
if markdown_file and Path(markdown_file).exists():
Path(markdown_file).unlink()
print(f"删除原始Markdown文件: {markdown_file}")
else:
print(f"警告:输出文件不存在: {output_file}")
results.append({
"filename": file.filename,
"status": "error",
"error": "处理后的文件不存在",
"output_file": None,
"markdown_file": None,
"content": None
})
continue
# 添加成功结果
results.append({
"filename": file.filename,
"status": "success",
"error": None,
"output_file": response_file.name,
"markdown_file": response_markdown.name,
"content": text_content or ""
})
print(f"文件处理完成: {file.filename}")
except Exception as e:
print(f"处理文件时出错: {file.filename}, 错误: {str(e)}")
results.append({
"filename": file.filename,
"status": "error",
"error": f"处理文件时发生错误: {str(e)}",
"output_file": None,
"markdown_file": None,
"content": None
})
# 确保清理临时文件
if temp_file and temp_file.exists():
try:
# 修改为使用安全删除函数
safe_delete_file(temp_file)
except Exception as cleanup_error:
print(f"清理临时文件失败: {cleanup_error}")
except Exception as e:
print(f"处理过程发生错误: {str(e)}")
return {
"results": results,
"error": f"处理过程发生错误: {str(e)}"
}
# 返回处理结果
return {
"results": results,
"error": None if results else "没有成功处理任何文件"
}
@app.get("/api/download/{filename:path}")
async def download_file(filename: str):
"""下载处理后的文件"""
# 确保输出目录存在
ensure_directories()
file_path = OUTPUT_DIR / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail="文件不存在")
# 根据文件扩展名设置正确的MIME类型
file_extension = Path(filename).suffix.lower()
if file_extension == '.md':
media_type = 'text/markdown'
elif file_extension == '.txt':
media_type = 'text/plain'
elif file_extension == '.docx':
media_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
else:
media_type = 'application/octet-stream'
return FileResponse(
path=str(file_path),
filename=filename,
media_type=media_type
)
# 在应用启动时清理所有临时目录的内容
@app.on_event("startup")
async def startup_event():
"""应用启动时的初始化操作"""
ensure_directories()
clean_temp_directories()
# 在应用关闭时清理所有临时目录的内容
@app.on_event("shutdown")
async def shutdown_event():
"""应用关闭时的清理操作"""
clean_temp_directories()
# 挂载静态文件目录 - 放在所有API路由之后
app.mount("/", StaticFiles(directory=str(STATIC_DIR), html=True), name="static")
async def save_uploaded_file(file: UploadFile) -> tuple[Path, str]:
"""保存上传的文件并返回临时文件路径"""
try:
if not file or not file.filename:
return None, "无效的文件"
# 确保上传目录存在
if not UPLOAD_DIR.exists():
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
print(f"创建上传目录: {UPLOAD_DIR}")
# 生成唯一的文件名
unique_id = str(uuid.uuid4())
# 处理文件名,移除.tmp部分
original_name = Path(file.filename).name
if '.tmp.' in original_name:
# 如果文件名中包含.tmp.,则移除它
name_parts = original_name.split('.tmp.')
safe_filename = name_parts[-1] # 取.tmp.后面的部分
else:
safe_filename = original_name
# 确保文件名只包含安全字符
safe_filename = re.sub(r'[^\w\-_\.]', '_', safe_filename)
temp_file = UPLOAD_DIR / f"temp_{unique_id}_{safe_filename}"
print(f"准备保存文件到: {temp_file}")
# 读取文件内容
content = await file.read()
if not content:
return None, "文件内容为空"
# 保存文件
with open(temp_file, "wb") as buffer:
buffer.write(content)
# 验证文件是否成功保存
if not temp_file.exists():
return None, "文件保存失败"
print(f"文件成功保存到: {temp_file}")
return temp_file, None
except Exception as e:
print(f"保存文件时出错: {str(e)}")
return None, f"保存文件时发生错误: {str(e)}"
def safe_delete_file(file_path, max_retries=3, retry_delay=1.0):
"""
安全删除文件带有重试机制
Args:
file_path: 要删除的文件路径
max_retries: 最大重试次数
retry_delay: 重试之间的延迟
Returns:
bool: 是否成功删除文件
"""
path = Path(file_path)
if not path.exists():
return True
for attempt in range(max_retries):
try:
path.unlink()
print(f"删除临时文件: {file_path}")
return True
except Exception as e:
print(f"尝试 {attempt+1}/{max_retries} 删除文件失败: {str(e)}")
if "WinError 32" in str(e):
# 如果是"另一个程序正在使用此文件"的错误,等待一会再重试
print(f"文件被锁定,等待 {retry_delay} 秒后重试...")
time.sleep(retry_delay)
else:
# 其他错误不继续尝试
print(f"删除文件时发生错误: {str(e)}")
return False
print(f"无法删除文件 {file_path},已尝试 {max_retries}")
return False
async def process_single_file(file_path: str, cleaner: DocCleaner) -> tuple[str, str, str, str]:
"""处理单个文件并返回结果文件路径、文件内容和Markdown文件路径"""
image_dir = None
output_file = None
temp_docx = None
try:
# 确保输入文件存在
file_path = Path(file_path)
if not file_path.exists():
print(f"错误:输入文件不存在: {file_path}")
raise FileNotFoundError(f"找不到输入文件: {file_path}")
# 规范化文件路径
file_path = str(file_path.resolve())
print(f"规范化后的文件路径: {file_path}")
# 处理文件名,移除.tmp部分
file_stem = Path(file_path).stem
if '.tmp.' in file_stem:
# 如果文件名中包含.tmp.,则移除它
name_parts = file_stem.split('.tmp.')
file_stem = name_parts[-1] # 取.tmp.后面的部分
# 生成唯一的图片目录名
unique_id = str(uuid.uuid4())[:8]
# 确保文件名只包含安全字符
safe_file_stem = re.sub(r'[^\w\-_\.]', '_', file_stem)
image_dir = IMAGES_DIR / f"{safe_file_stem}_{unique_id}"
# 确保图片目录存在
image_dir.mkdir(parents=True, exist_ok=True)
print(f"创建图片目录: {image_dir}")
# 生成输出文件路径
output_file = OUTPUT_DIR / f"{safe_file_stem}_output.txt"
markdown_file = OUTPUT_DIR / f"{safe_file_stem}_output.md"
docx_file = OUTPUT_DIR / f"{safe_file_stem}_output.docx"
print(f"输出文件路径: {output_file}")
print(f"Markdown文件路径: {markdown_file}")
print(f"Word文件路径: {docx_file}")
# 处理文档
print(f"开始处理文件: {file_path}")
print(f"图片将保存到: {image_dir}")
# 处理文档并保存所有格式
main_content, appendix, tables = cleaner.clean_doc(file_path)
print(f"文档处理完成: {file_path}")
# 保存为docx格式这个函数会同时生成txt和md文件
cleaner.save_as_docx(main_content, appendix, tables, str(docx_file))
# 合并正文和附录内容用于返回
all_content = main_content + ["附录"] + appendix if appendix else main_content
text_content = " ".join([t.replace("\n", " ").strip() for t in all_content if t.strip()])
# 验证所有文件是否成功创建
if not output_file.exists():
raise FileNotFoundError(f"TXT文件未能成功创建: {output_file}")
if not markdown_file.exists():
raise FileNotFoundError(f"Markdown文件未能成功创建: {markdown_file}")
return str(output_file), text_content, str(markdown_file), None
except Exception as e:
print(f"处理文件时出错: {str(e)}")
return None, None, None, str(e)
finally:
# 清理临时文件和目录
try:
if image_dir and image_dir.exists():
print(f"清理图片目录: {image_dir}")
shutil.rmtree(str(image_dir))
except Exception as cleanup_error:
print(f"清理图片目录时出错: {str(cleanup_error)}")
try:
if temp_docx and os.path.exists(temp_docx):
print(f"清理临时DOCX文件: {temp_docx}")
safe_delete_file(temp_docx) # 使用安全删除函数
temp_dir = os.path.dirname(temp_docx)
if os.path.exists(temp_dir):
try:
os.rmdir(temp_dir)
except Exception as dir_error:
print(f"清理临时目录时出错: {str(dir_error)}")
except Exception as cleanup_error:
print(f"清理临时DOCX文件时出错: {str(cleanup_error)}")

285
cxs/ocr_api.py Normal file
View File

@ -0,0 +1,285 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import FastAPI, File, UploadFile, Form, HTTPException
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
import os
import tempfile
from pathlib import Path
import uuid
import time
import base64
import io
# 导入PDF处理器
try:
from cxs_pdf_cleaner import PdfProcessor
except ImportError:
try:
from cxs.cxs_pdf_cleaner import PdfProcessor
except ImportError:
# 如果导入失败添加当前目录到Python路径
import sys
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(current_dir)
from cxs_pdf_cleaner import PdfProcessor
# 获取当前文件所在目录
CURRENT_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
# 定义目录
TEMP_DIR = CURRENT_DIR / "temp"
STATIC_DIR = CURRENT_DIR / "static"
DEBUG_DIR = TEMP_DIR / "debug"
# 确保所有必要的目录都存在
def ensure_directories():
"""确保所有必要的目录都存在"""
directories = [TEMP_DIR, STATIC_DIR, DEBUG_DIR]
for directory in directories:
directory.mkdir(parents=True, exist_ok=True)
print(f"确保目录存在: {directory}")
# 初始化目录
ensure_directories()
# 创建FastAPI应用
app = FastAPI(debug=True, title="OCR图像识别API",
description="提供高级图像OCR识别服务")
# 配置CORS
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"],
expose_headers=["*"]
)
# 初始化PDF处理器
pdf_processor = PdfProcessor()
# 设置静态文件
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
app.mount("/debug", StaticFiles(directory=str(DEBUG_DIR)), name="debug")
@app.get("/")
async def root():
"""重定向到OCR测试页面"""
return {"message": "欢迎使用OCR图像识别API", "test_page": "/static/ocr_test.html"}
@app.post("/api/ocr")
async def ocr_image(
image: UploadFile = File(...),
lang: str = Form("chi_sim+eng"),
mode: str = Form("auto")
):
"""
对上传的图片进行OCR识别
- **image**: 要进行OCR识别的图片文件
- **lang**: OCR语言默认为中文简体+英文 (chi_sim+eng)
- **mode**: 处理模式auto=自动standard=标准advanced=高级chinese=中文优化
"""
print(f"接收到OCR请求: 文件名={image.filename}, 语言={lang}, 模式={mode}")
# 检查文件类型
valid_types = ["image/jpeg", "image/png", "image/bmp", "image/tiff", "image/gif"]
if image.content_type not in valid_types:
raise HTTPException(status_code=400, detail="不支持的文件类型,请上传图片文件")
# 创建一个唯一的ID用于此次处理
process_id = str(uuid.uuid4())[:8]
# 保存上传的图片
temp_dir = tempfile.mkdtemp(dir=TEMP_DIR)
temp_path = Path(temp_dir) / f"image_{process_id}{Path(image.filename).suffix}"
try:
# 保存上传的图片
content = await image.read()
with open(temp_path, "wb") as f:
f.write(content)
print(f"图片已保存到临时路径: {temp_path}")
# 记录开始时间
start_time = time.time()
# 执行OCR处理
ocr_results = []
best_result = ""
# 根据不同模式选择不同的处理参数
if mode == "standard":
# 标准模式 - 使用基本的OCR处理
ocr_text = pdf_processor.perform_ocr(str(temp_path), lang, retry_count=0)
best_result = ocr_text
ocr_results.append({
"name": "标准处理",
"text": ocr_text,
"length": len(ocr_text),
"confidence": 90.0,
"blocks": 1
})
elif mode == "chinese":
# 中文优化模式 - 使用中文专项处理
image = pdf_processor._read_image(str(temp_path))
if image is not None:
# 应用中文优化
processed = pdf_processor._optimize_for_chinese(image)
# 保存处理后的图像以供显示
debug_path = DEBUG_DIR / f"chinese_{process_id}.png"
pdf_processor._save_debug_image(processed, str(debug_path))
# 执行OCR
ocr_text = pdf_processor.perform_ocr(str(debug_path), lang, retry_count=1)
best_result = ocr_text
ocr_results.append({
"name": "中文优化",
"text": ocr_text,
"length": len(ocr_text),
"confidence": 90.0,
"blocks": 1
})
elif mode == "advanced":
# 高级模式 - 使用多种处理方法并比较结果
# 读取原始图像
image = pdf_processor._read_image(str(temp_path))
if image is not None:
# 使用多种图像处理方法
preprocessed_images = pdf_processor._apply_multiple_preprocessing(image)
# 对每个预处理后的图像执行OCR并比较结果
best_length = 0
best_confidence = 0
for method_name, processed_image in preprocessed_images:
# 保存处理后的图像以供显示
debug_path = DEBUG_DIR / f"{method_name.replace(' ', '_').lower()}_{process_id}.png"
pdf_processor._save_debug_image(processed_image, str(debug_path))
# 执行OCR
try:
import pytesseract
ocr_result = pytesseract.image_to_data(processed_image, lang=lang, output_type=pytesseract.Output.DICT)
# 提取文本
extracted_text = []
total_confidence = 0
valid_blocks = 0
for i in range(len(ocr_result['text'])):
confidence = ocr_result['conf'][i]
text = ocr_result['text'][i].strip()
if confidence > pdf_processor.min_text_confidence and text:
extracted_text.append(text)
total_confidence += confidence
valid_blocks += 1
# 合并结果
result_text = " ".join(extracted_text)
result_length = len(result_text)
avg_confidence = total_confidence / valid_blocks if valid_blocks > 0 else 0
ocr_results.append({
"name": method_name,
"text": result_text,
"length": result_length,
"confidence": avg_confidence,
"blocks": valid_blocks
})
# 更新最佳结果
if result_length > 0:
if (result_length > best_length * 1.5) or \
(result_length >= best_length * 0.8 and avg_confidence > best_confidence):
best_result = result_text
best_length = result_length
best_confidence = avg_confidence
except Exception as e:
print(f"处理方法 {method_name} 失败: {str(e)}")
else:
# 自动模式 - 使用完整的OCR处理流程
best_result = pdf_processor.perform_ocr(str(temp_path), lang, retry_count=3)
# 添加处理结果
ocr_results.append({
"name": "自动处理",
"text": best_result,
"length": len(best_result),
"confidence": 90.0,
"blocks": 1
})
# 计算处理时间
processing_time = time.time() - start_time
print(f"OCR处理完成耗时: {processing_time:.2f}")
# 收集处理后的图像列表
processed_images = []
try:
# 查找调试目录中的图像
debug_files = list(DEBUG_DIR.glob(f"*_{process_id}.png"))
for debug_file in debug_files:
# 提取处理方法名称
method_name = debug_file.stem.split('_')[0].replace('_', ' ').title()
# 创建图像URL
image_url = f"/debug/{debug_file.name}"
processed_images.append({
"name": method_name,
"url": image_url
})
except Exception as e:
print(f"收集处理图像时出错: {str(e)}")
# 根据OCR结果长度排序
ocr_results.sort(key=lambda x: x['length'], reverse=True)
# 返回OCR结果
response = {
"text": best_result,
"processing_time": processing_time,
"lang": lang,
"mode": mode,
"methods": ocr_results,
"processed_images": processed_images
}
return JSONResponse(content=response)
except Exception as e:
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"OCR处理失败: {str(e)}")
finally:
# 清理临时文件
try:
if temp_path.exists():
temp_path.unlink()
if Path(temp_dir).exists():
os.rmdir(temp_dir)
print(f"临时文件已清理")
except Exception as e:
print(f"清理临时文件时出错: {str(e)}")
if __name__ == "__main__":
import uvicorn
print("启动OCR API服务...")
print(f"当前工作目录: {os.getcwd()}")
print(f"静态文件目录: {STATIC_DIR}")
print(f"调试文件目录: {DEBUG_DIR}")
# 启动服务器
uvicorn.run(app, host="0.0.0.0", port=8001)

20
cxs/requirements.txt Normal file
View File

@ -0,0 +1,20 @@
fastapi==0.104.1
python-multipart==0.0.6
uvicorn==0.24.0
python-docx==1.0.1
numpy>=1.26.2
scikit-learn>=1.3.2
requests>=2.32.2
reportlab==4.0.4
python-Levenshtein>=0.22.0
regex>=2023.0.0
pdf2docx>=0.5.6
pytesseract>=0.3.10
opencv-python>=4.8.0
Pillow>=10.0.0
beautifulsoup4>=4.12.0
html2text>=2020.1.16
pandas>=2.0.0
aiofiles>=23.1.0
openpyxl>=3.1.2
uuid>=1.30

468
cxs/static/index.html Normal file
View File

@ -0,0 +1,468 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文档处理系统</title>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.upload-area {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
margin-bottom: 20px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.upload-area:hover {
border-color: #666;
}
.upload-area.dragover {
border-color: #4CAF50;
background-color: #E8F5E9;
}
#file-input {
display: none;
}
.btn {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
margin: 0 5px;
}
.btn:hover {
background-color: #45a049;
}
.btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#status {
margin-top: 20px;
padding: 10px;
border-radius: 4px;
display: none;
}
.success {
background-color: #E8F5E9;
color: #2E7D32;
}
.error {
background-color: #FFEBEE;
color: #C62828;
}
.file-list {
margin: 20px 0;
max-height: 300px;
overflow-y: auto;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
border: 1px solid #ddd;
margin-bottom: 5px;
border-radius: 4px;
}
.file-item .progress-container {
flex: 1;
margin: 0 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
}
.file-item .progress-bar {
height: 20px;
background-color: #4CAF50;
width: 0%;
transition: width 0.3s ease;
border-radius: 10px;
position: relative;
}
.progress-text {
position: absolute;
width: 100%;
text-align: center;
color: white;
font-size: 12px;
line-height: 20px;
}
.file-item .remove-btn {
background-color: #f44336;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
}
.result-container {
margin-top: 20px;
border-top: 1px solid #ddd;
padding-top: 20px;
}
.result-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border: 1px solid #ddd;
margin-bottom: 5px;
border-radius: 4px;
background-color: #fff;
}
.result-item.error {
background-color: #FFEBEE;
}
.result-item.success {
background-color: #E8F5E9;
}
.result-info {
flex: 1;
margin-right: 10px;
}
.button-group {
text-align: center;
margin: 20px 0;
}
.result-text {
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 10px;
margin-top: 10px;
background-color: #fff;
border-radius: 4px;
white-space: pre-wrap;
display: none;
}
.result-buttons {
display: flex;
gap: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>文档处理系统</h1>
<div class="upload-area" id="drop-area">
<p>点击或拖拽文件到此处上传</p>
<p>支持的格式:.doc, .docx, .pdf, .html, .htm, .xls, .xlsx</p>
<p>可以同时选择多个文件</p>
<input type="file" id="file-input" accept=".doc,.docx,.pdf,.html,.htm,.xls,.xlsx" multiple>
</div>
<div class="file-list" id="file-list"></div>
<div class="button-group">
<button id="upload-btn" class="btn" disabled>开始处理</button>
<button id="clear-btn" class="btn" style="background-color: #f44336;">清空列表</button>
</div>
<div id="status"></div>
<div class="result-container">
<h2>处理结果</h2>
<div id="result-list"></div>
</div>
</div>
<script>
const dropArea = document.getElementById('drop-area');
const fileInput = document.getElementById('file-input');
const uploadBtn = document.getElementById('upload-btn');
const clearBtn = document.getElementById('clear-btn');
const status = document.getElementById('status');
const fileList = document.getElementById('file-list');
const resultList = document.getElementById('result-list');
let files = new Map(); // 存储待处理的文件
let processing = false; // 是否正在处理文件
// 处理拖拽事件
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});
function highlight(e) {
dropArea.classList.add('dragover');
}
function unhighlight(e) {
dropArea.classList.remove('dragover');
}
// 处理文件拖放
dropArea.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
handleFiles(Array.from(dt.files));
}
// 点击上传区域触发文件选择
dropArea.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', function() {
handleFiles(Array.from(this.files));
this.value = ''; // 清空input允许重复选择相同文件
});
// 清空按钮事件
clearBtn.addEventListener('click', () => {
if (!processing) {
files.clear();
updateFileList();
uploadBtn.disabled = true;
}
});
function handleFiles(newFiles) {
const validTypes = ['.doc', '.docx', '.pdf', '.html', '.htm', '.xls', '.xlsx'];
newFiles.forEach(file => {
const fileExtension = file.name.toLowerCase().slice(file.name.lastIndexOf('.'));
if (validTypes.includes(fileExtension)) {
files.set(file.name, {
file: file,
progress: 0,
status: 'pending' // pending, processing, completed, error
});
}
});
updateFileList();
uploadBtn.disabled = files.size === 0;
}
function updateFileList() {
fileList.innerHTML = '';
files.forEach((fileData, fileName) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
const nameSpan = document.createElement('span');
nameSpan.textContent = fileName;
const progressContainer = document.createElement('div');
progressContainer.className = 'progress-container';
const progressBar = document.createElement('div');
progressBar.className = 'progress-bar';
progressBar.style.width = fileData.progress + '%';
const progressText = document.createElement('div');
progressText.className = 'progress-text';
progressText.textContent = fileData.progress + '%';
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.textContent = '删除';
removeBtn.onclick = () => {
if (!processing) {
files.delete(fileName);
updateFileList();
uploadBtn.disabled = files.size === 0;
}
};
progressBar.appendChild(progressText);
progressContainer.appendChild(progressBar);
fileItem.appendChild(nameSpan);
fileItem.appendChild(progressContainer);
fileItem.appendChild(removeBtn);
fileList.appendChild(fileItem);
});
}
// 处理文件上传
uploadBtn.addEventListener('click', async () => {
if (processing || files.size === 0) return;
processing = true;
uploadBtn.disabled = true;
status.style.display = 'none';
resultList.innerHTML = '';
try {
const results = [];
// 一个一个处理文件
for (const [fileName, fileData] of files.entries()) {
const formData = new FormData();
formData.append('files', fileData.file);
// 更新进度显示
fileData.status = 'processing';
updateFileList();
try {
const response = await fetch('/api/upload/', {
method: 'POST',
body: formData,
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log(`文件 ${fileName} 处理结果:`, result); // 调试日志
if (result.error) {
fileData.status = 'error';
showMessage(`文件 ${fileName} 处理失败: ${result.error}`);
} else if (result.results && result.results.length > 0) {
fileData.status = 'completed';
results.push(...result.results);
}
} catch (error) {
console.error(`文件 ${fileName} 处理错误:`, error);
fileData.status = 'error';
showMessage(`文件 ${fileName} 处理失败: ${error.message}`);
}
// 更新进度显示
fileData.progress = 100;
updateFileList();
// 等待一小段时间,确保文件处理完成
await new Promise(resolve => setTimeout(resolve, 500));
}
// 显示所有处理结果
displayResults(results);
} catch (error) {
console.error('处理错误:', error);
showMessage(`处理失败: ${error.message}`);
} finally {
processing = false;
uploadBtn.disabled = false;
files.clear();
updateFileList();
}
});
async function displayResults(results) {
if (results.length === 0) {
showMessage('没有文件被处理');
return;
}
results.forEach(result => {
const resultItem = document.createElement('div');
resultItem.className = `result-item ${result.status}`;
const resultInfo = document.createElement('div');
resultInfo.className = 'result-info';
if (result.status === 'success') {
resultInfo.innerHTML = `<strong>${result.filename}</strong> 处理成功`;
const buttonsDiv = document.createElement('div');
buttonsDiv.className = 'result-buttons';
// 下载TXT按钮
if (result.output_file) {
const downloadBtn = document.createElement('button');
downloadBtn.className = 'btn';
downloadBtn.textContent = '下载TXT';
downloadBtn.onclick = () => {
window.location.href = `/api/download/${result.output_file}`;
};
buttonsDiv.appendChild(downloadBtn);
}
// 下载Markdown按钮
if (result.markdown_file) {
const downloadMarkdownBtn = document.createElement('button');
downloadMarkdownBtn.className = 'btn';
downloadMarkdownBtn.style.backgroundColor = '#2196F3'; // 使用不同的颜色区分
downloadMarkdownBtn.textContent = '下载MD';
downloadMarkdownBtn.onclick = () => {
window.location.href = `/api/download/${result.markdown_file}`;
};
buttonsDiv.appendChild(downloadMarkdownBtn);
}
// 查看内容按钮
if (result.content) {
const showTextBtn = document.createElement('button');
showTextBtn.className = 'btn';
showTextBtn.textContent = '查看内容';
const textDiv = document.createElement('div');
textDiv.className = 'result-text';
textDiv.textContent = result.content;
textDiv.style.display = 'none';
showTextBtn.onclick = () => {
const isVisible = textDiv.style.display === 'block';
textDiv.style.display = isVisible ? 'none' : 'block';
showTextBtn.textContent = isVisible ? '查看内容' : '隐藏内容';
};
buttonsDiv.appendChild(showTextBtn);
resultItem.appendChild(textDiv);
}
resultItem.appendChild(resultInfo);
resultItem.appendChild(buttonsDiv);
} else {
resultInfo.innerHTML = `<strong>${result.filename}</strong> 处理失败: ${result.error || '未知错误'}`;
resultItem.appendChild(resultInfo);
}
resultList.appendChild(resultItem);
});
}
function showMessage(message) {
const statusDiv = document.getElementById('status');
statusDiv.textContent = message;
statusDiv.className = 'error';
statusDiv.style.display = 'block';
setTimeout(() => {
statusDiv.style.display = 'none';
statusDiv.textContent = '';
statusDiv.className = '';
}, 3000);
}
</script>
</body>
</html>

526
cxs/static/ocr_test.html Normal file
View File

@ -0,0 +1,526 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OCR图像识别测试</title>
<style>
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background-color: #f5f7fa;
margin: 0;
padding: 20px;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 25px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 30px;
border-bottom: 2px solid #eee;
padding-bottom: 15px;
}
.subtitle {
color: #7f8c8d;
text-align: center;
margin-top: -20px;
margin-bottom: 30px;
}
.upload-container {
border: 2px dashed #3498db;
border-radius: 8px;
padding: 40px;
text-align: center;
margin-bottom: 20px;
background-color: #f8fafc;
transition: background-color 0.3s;
}
.upload-container.dragover {
background-color: #e1f0fa;
}
.upload-container p {
margin: 0;
color: #7f8c8d;
}
.upload-icon {
font-size: 50px;
color: #3498db;
margin-bottom: 15px;
}
.file-input {
display: none;
}
.upload-btn, .ocr-btn {
background-color: #3498db;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
margin: 10px 5px;
}
.upload-btn:hover, .ocr-btn:hover {
background-color: #2980b9;
}
.ocr-btn {
background-color: #2ecc71;
display: none;
}
.ocr-btn:hover {
background-color: #27ae60;
}
.ocr-btn:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.preview-container {
margin-top: 20px;
text-align: center;
}
.image-preview {
max-width: 100%;
max-height: 400px;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
display: none;
}
.settings {
background-color: #f8fafc;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
display: none;
}
.settings h3 {
margin-top: 0;
color: #2c3e50;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #34495e;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
font-family: inherit;
font-size: 16px;
}
.results {
margin-top: 30px;
display: none;
}
.tabs {
display: flex;
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
cursor: pointer;
border: 1px solid transparent;
border-radius: 4px 4px 0 0;
margin-right: 5px;
background-color: #f8f9fa;
}
.tab.active {
border: 1px solid #ddd;
border-bottom-color: white;
background-color: white;
font-weight: bold;
}
.tab-content {
display: none;
padding: 20px;
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 4px 4px;
}
.tab-content.active {
display: block;
}
.ocr-text {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
line-height: 1.5;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
}
.processing-info {
margin-top: 20px;
padding: 15px;
background-color: #f0f7fb;
border-radius: 4px;
border-left: 5px solid #3498db;
}
.method-result {
margin: 10px 0;
padding: 15px;
background-color: #f8fafc;
border-radius: 4px;
border: 1px solid #ddd;
}
.method-result h4 {
margin-top: 0;
color: #2c3e50;
}
.confidence-bar {
height: 10px;
background-color: #ecf0f1;
border-radius: 5px;
margin: 5px 0;
position: relative;
}
.confidence-value {
height: 100%;
background-color: #2ecc71;
border-radius: 5px;
position: absolute;
left: 0;
top: 0;
}
.processed-images {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 20px;
}
.processed-image {
max-width: calc(50% - 15px);
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
background-color: white;
}
.processed-image h4 {
margin-top: 0;
text-align: center;
color: #2c3e50;
font-size: 14px;
}
.processed-image img {
max-width: 100%;
border-radius: 4px;
}
.loader {
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 2s linear infinite;
margin: 20px auto;
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
color: #e74c3c;
padding: 10px;
background-color: #fadbd8;
border-radius: 4px;
margin: 20px 0;
display: none;
}
</style>
</head>
<body>
<div class="container">
<h1>OCR图像识别测试</h1>
<p class="subtitle">上传图片并测试文字识别效果</p>
<div class="upload-container" id="uploadContainer">
<div class="upload-icon">📁</div>
<p>拖放图片到这里,或点击上传</p>
<input type="file" id="fileInput" class="file-input" accept="image/*">
<button class="upload-btn" id="uploadBtn">选择图片</button>
</div>
<div class="preview-container">
<img id="imagePreview" class="image-preview">
</div>
<div class="settings" id="settings">
<h3>OCR设置</h3>
<div class="form-group">
<label for="langSelect">识别语言</label>
<select id="langSelect" class="form-control">
<option value="chi_sim+eng" selected>中文简体+英文</option>
<option value="chi_sim">中文简体</option>
<option value="eng">英文</option>
<option value="chi_tra">中文繁体</option>
<option value="jpn">日语</option>
<option value="kor">韩语</option>
<option value="rus">俄语</option>
</select>
</div>
<div class="form-group">
<label for="modeSelect">处理模式</label>
<select id="modeSelect" class="form-control">
<option value="auto" selected>自动模式</option>
<option value="standard">标准模式</option>
<option value="chinese">中文优化</option>
<option value="advanced">高级模式</option>
</select>
</div>
<button class="ocr-btn" id="ocrBtn">执行OCR</button>
</div>
<div class="loader" id="loader"></div>
<div class="error-message" id="errorMessage"></div>
<div class="results" id="results">
<div class="tabs">
<div class="tab active" data-tab="text">识别文本</div>
<div class="tab" data-tab="details">处理详情</div>
<div class="tab" data-tab="images">处理图像</div>
</div>
<div class="tab-content active" id="textContent">
<h3>OCR识别结果</h3>
<div class="ocr-text" id="ocrText"></div>
<div class="processing-info" id="processingInfo"></div>
</div>
<div class="tab-content" id="detailsContent">
<h3>处理方法详情</h3>
<div id="methodsList"></div>
</div>
<div class="tab-content" id="imagesContent">
<h3>处理后的图像</h3>
<div class="processed-images" id="processedImages"></div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const uploadContainer = document.getElementById('uploadContainer');
const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn');
const imagePreview = document.getElementById('imagePreview');
const settings = document.getElementById('settings');
const ocrBtn = document.getElementById('ocrBtn');
const results = document.getElementById('results');
const ocrText = document.getElementById('ocrText');
const processingInfo = document.getElementById('processingInfo');
const methodsList = document.getElementById('methodsList');
const processedImages = document.getElementById('processedImages');
const loader = document.getElementById('loader');
const errorMessage = document.getElementById('errorMessage');
const tabs = document.querySelectorAll('.tab');
const tabContents = document.querySelectorAll('.tab-content');
// 处理文件选择
fileInput.addEventListener('change', handleFileSelect);
uploadBtn.addEventListener('click', () => fileInput.click());
// 拖放功能
uploadContainer.addEventListener('dragover', (e) => {
e.preventDefault();
uploadContainer.classList.add('dragover');
});
uploadContainer.addEventListener('dragleave', () => {
uploadContainer.classList.remove('dragover');
});
uploadContainer.addEventListener('drop', (e) => {
e.preventDefault();
uploadContainer.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
fileInput.files = e.dataTransfer.files;
handleFileSelect(e);
}
});
// 处理OCR按钮点击
ocrBtn.addEventListener('click', performOCR);
// 处理标签页切换
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
tab.classList.add('active');
const tabId = tab.getAttribute('data-tab');
document.getElementById(`${tabId}Content`).classList.add('active');
});
});
function handleFileSelect(e) {
const file = fileInput.files[0];
if (!file) return;
// 检查文件类型
if (!file.type.match('image.*')) {
showError('请选择图片文件');
return;
}
// 隐藏之前的错误消息和结果
errorMessage.style.display = 'none';
results.style.display = 'none';
// 更新预览
const reader = new FileReader();
reader.onload = function(e) {
imagePreview.src = e.target.result;
imagePreview.style.display = 'block';
settings.style.display = 'block';
ocrBtn.style.display = 'block';
ocrBtn.disabled = false;
};
reader.readAsDataURL(file);
}
function performOCR() {
const file = fileInput.files[0];
if (!file) {
showError('请先选择图片文件');
return;
}
const lang = document.getElementById('langSelect').value;
const mode = document.getElementById('modeSelect').value;
// 显示加载状态
loader.style.display = 'block';
ocrBtn.disabled = true;
errorMessage.style.display = 'none';
results.style.display = 'none';
// 创建FormData对象
const formData = new FormData();
formData.append('image', file);
formData.append('lang', lang);
formData.append('mode', mode);
// 发送OCR请求
fetch('/api/ocr', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error(err.detail || '处理图片时出错');
});
}
return response.json();
})
.then(data => {
// 隐藏加载状态
loader.style.display = 'none';
// 显示OCR结果
ocrText.textContent = data.text || '未识别到文本';
// 显示处理信息
processingInfo.innerHTML = `
<p><strong>处理时间:</strong> ${data.processing_time.toFixed(2)}秒</p>
<p><strong>识别语言:</strong> ${data.lang}</p>
<p><strong>处理模式:</strong> ${getModeLabel(data.mode)}</p>
<p><strong>识别文本长度:</strong> ${data.text ? data.text.length : 0}个字符</p>
`;
// 显示处理方法详情
methodsList.innerHTML = '';
if (data.methods && data.methods.length > 0) {
data.methods.forEach(method => {
const methodDiv = document.createElement('div');
methodDiv.className = 'method-result';
const confidencePercent = method.confidence || 0;
methodDiv.innerHTML = `
<h4>${method.name}</h4>
<p><strong>文本长度:</strong> ${method.length} 字符</p>
<p><strong>置信度:</strong> ${confidencePercent.toFixed(2)}%</p>
<div class="confidence-bar">
<div class="confidence-value" style="width: ${Math.min(100, confidencePercent)}%"></div>
</div>
<p><strong>文本块数:</strong> ${method.blocks}</p>
<div class="ocr-text">${method.text || '未识别到文本'}</div>
`;
methodsList.appendChild(methodDiv);
});
} else {
methodsList.innerHTML = '<p>没有可用的处理方法详情</p>';
}
// 显示处理后的图像
processedImages.innerHTML = '';
if (data.processed_images && data.processed_images.length > 0) {
data.processed_images.forEach(img => {
const imgDiv = document.createElement('div');
imgDiv.className = 'processed-image';
imgDiv.innerHTML = `
<h4>${img.name}</h4>
<img src="${img.url}" alt="${img.name}">
`;
processedImages.appendChild(imgDiv);
});
} else {
processedImages.innerHTML = '<p>没有处理后的图像可供显示</p>';
}
// 显示结果区域
results.style.display = 'block';
// 恢复OCR按钮
ocrBtn.disabled = false;
})
.catch(error => {
console.error('OCR处理失败:', error);
loader.style.display = 'none';
ocrBtn.disabled = false;
showError(error.message || '处理图片时出错,请重试');
});
}
function showError(message) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
}
function getModeLabel(mode) {
const modes = {
'auto': '自动模式',
'standard': '标准模式',
'chinese': '中文优化',
'advanced': '高级模式'
};
return modes[mode] || mode;
}
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,69 @@
# 文档内容
【文档信息】
作者: Lenovo
创建时间: 2025-05-15 08:30:10
修改时间: 2025-05-15 08:30:25
1111
【图片识别文本】
“ 完 善 了 异 常 处 理 , 防 止 惑 时 目 录 券 除 失 败 导 致 程 序 崖
4. 更 新 README.md
* 在 暨 近 更 新 部 分 记 录 了 临 时 文 件 处 理 机 制 的 改 进
。 添 加 了 Excel 文 件 句 柄 管 理
使 用 说 明
这 东 改 进 不 需 要 您 做 任 何 额 外 操 作 , 系 统 会 自 动 -
1. 在 处 理 Excel 文 件 时 正 球 关 闭 文 件 句 柄
2 当 尝 试 删 除 文 件 通 刨 “ 文 件 被 占 用 “ 错 误 时 , 自 动 等 待
并 重 试
3 即 使 无 法 券 除 临 时 文 件 , 也 不 影 响 处 #
如 果 仍 然 通 到 惧 时 文 件 问 题 , 系 统 会 在 下 次 启 动 时 自 动
清 理 所 有 临 时 文 件 , 不 会 影 响 系 统 功 能 。
以 上 优 化 星 觞 失 了 临 时 文 伟 删 除 问 题 , 又 保 持 了 系 统 的
稳 定 性 , 让 您 能 雪 顺 畅 地 处 理 Bxcel 文 件 。
## 图片内容
### 图片 1
![图片 1](images/image_1.png)
**OCR文本内容:**
“ 完 善 了 异 常 处 理 , 防 止 惑 时 目 录 券 除 失 败 导 致 程 序 崖
4. 更 新 README.md
* 在 暨 近 更 新 部 分 记 录 了 临 时 文 件 处 理 机 制 的 改 进
。 添 加 了 Excel 文 件 句 柄 管 理
使 用 说 明
这 东 改 进 不 需 要 您 做 任 何 额 外 操 作 , 系 统 会 自 动 -
1. 在 处 理 Excel 文 件 时 正 球 关 闭 文 件 句 柄
2 当 尝 试 删 除 文 件 通 刨 “ 文 件 被 占 用 “ 错 误 时 , 自 动 等 待
并 重 试
3 即 使 无 法 券 除 临 时 文 件 , 也 不 影 响 处 #
如 果 仍 然 通 到 惧 时 文 件 问 题 , 系 统 会 在 下 次 启 动 时 自 动
清 理 所 有 临 时 文 件 , 不 会 影 响 系 统 功 能 。
以 上 优 化 星 觞 失 了 临 时 文 伟 删 除 问 题 , 又 保 持 了 系 统 的
稳 定 性 , 让 您 能 雪 顺 畅 地 处 理 Bxcel 文 件 。

View File

@ -0,0 +1 @@
【文档信息】 作者: Lenovo 创建时间: 2025-05-15 08:30:10 修改时间: 2025-05-15 08:30:25 1111 【图片识别文本】 “ 完 善 了 异 常 处 理 , 防 止 惑 时 目 录 券 除 失 败 导 致 程 序 崖 澎 澎 4. 更 新 README.md * 在 暨 近 更 新 部 分 记 录 了 临 时 文 件 处 理 机 制 的 改 进 。 添 加 了 Excel 文 件 句 柄 管 理 使 用 说 明 这 东 改 进 不 需 要 您 做 任 何 额 外 操 作 , 系 统 会 自 动 - 1. 在 处 理 Excel 文 件 时 正 球 关 闭 文 件 句 柄 2 当 尝 试 删 除 文 件 通 刨 “ 文 件 被 占 用 “ 错 误 时 , 自 动 等 待 并 重 试 3 即 使 无 法 券 除 临 时 文 件 , 也 不 影 响 处 # 如 果 仍 然 通 到 惧 时 文 件 问 题 , 系 统 会 在 下 次 启 动 时 自 动 清 理 所 有 临 时 文 件 , 不 会 影 响 系 统 功 能 。 以 上 优 化 星 觞 失 了 临 时 文 伟 删 除 问 题 , 又 保 持 了 系 统 的 稳 定 性 , 让 您 能 雪 顺 畅 地 处 理 Bxcel 文 件 。

View File

@ -0,0 +1,359 @@
表格 1
表格 2
表格 3
表格 4
表格 5
表格 6
表格 7
表格 8
表格 9
表格 10
表格 11
表格 12
表格 13
表格 14
表格 15
表格 16
表格 17
表格 18
表格 19
表格 20
表格 21
表格 22
表格 23
表格 24
表格 25
表格 26
表格 27
表格 28
表格 29
表格 30
表格 31
表格 32
表格 33
表格 34
表格 35
表格 36
表格 37
表格 38
表格 39
表格 40
表格 41
表格 42
表格 43
表格 44
表格 45
表格 46
表格 47
表格 48
表格 49
表格 50
表格 51
表格 52
表格 53
表格 54
表格 55
表格 56
表格 57
表格 58
表格 59
表格 60
表格 61
表格 62
表格 63
表格 64
表格 65
表格 66
表格 67
表格 68
表格 69
表格 70
表格 71
表格 72
表格 73
表格 74
表格 75
表格 76
表格 77
表格 78
表格 79
表格 80
表格 81
表格 82
表格 83
表格 84
表格 85
表格 86
表格 87
表格 88
表格 89
表格 90
表格 91
表格 92
表格 93
表格 94
表格 95
表格 96
表格 97
表格 98
表格 99
表格 100
表格 101
表格 102
表格 103
表格 104
表格 105
表格 106
表格 107
表格 108
表格 109
表格 110
表格 111
【文档信息】
作者: 王兴龙
创建时间: 2017-09-04 06:43:00
修改时间: 2022-10-25 03:31:00
附件
医疗器械分类目录
2017年8月目 录
01有源手术器械1
02无源手术器械6
03神经和心血管手术器械16
04骨科手术器械23
05放射治疗器械33
06医用成像器械37
07医用诊察和监护器械48
08呼吸、麻醉和急救器械56
09物理治疗器械62
10输血、透析和体外循环器械69
11医疗器械消毒灭菌器械75
12有源植入器械78
13无源植入器械81
14注输、护理和防护器械88
15患者承载器械102
16眼科器械105
17口腔科器械116
18妇产科、辅助生殖和避孕器械128
19医用康复器械136
20中医器械139
21医用软件143
22临床检验器械147
编制说明155
01 有源手术器械说明
一、范围
本子目录包括以手术治疗为目的与有源相关的医疗器械,包括超声、激光、高频/射频、微波、冷冻、冲击波、手术导航及控制系统、手术照明设备、内窥镜手术用有源设备等医疗器械。
二、框架结构
本子目录按照产品预期用途和专业技术及功能特点进行层级排序共划分为10个一级产品类别在一级产品类别的基础上根据先设备后附件的形式设立二级产品类别共25个列举120个品名举例。
本子目录包括2002版医疗器械分类目录中《6821医用电子仪器设备》《6822医用光学器具仪器及内窥镜设备》《6824医用激光仪器设备》《6825医用高频仪器设备》《6854手术室急救室诊疗室设备及器具》《6858医用冷疗低温冷藏设备及器具》和《〈6816烧伤整形科手术器械〉部分还包括了2012版医疗器械分类目录中《〈6823医用超声仪器及有关设备〉部分》。
该子目录中一级产品类别与2002/2012版分类目录产品类别的对应关系如下
与2002/2012版分类目录对应关系
三、其他说明
医用激光光纤与激光治疗仪配套应用传输激光器产生的能量用于激光手术治疗。依据《关于一次性前列腺治疗套件等产品分类界定的通知》国食药监械2008587号和《国家食品药品监督管理局关于超声肿瘤治疗系统等17个产品分类界定的通知》国食药监械201236号分类界定文件规定管理类别为二类分类编码6824。因此将医用激光光纤纳入《01有源手术器械》目录中。
(二)射频消融设备用灌注泵,管理类别由第三类降为第二类。
发光二极管LED手术照明灯管理类别由第二类降为第一类。
本子目录包括通用刀、剪、钳等各类无源手术医疗器械,不包括神经和心血管手术器械、骨科手术器械、眼科器械、口腔科器械、妇产科、辅助生殖和避孕器械。
02无源手术器械说明
本子目录按照无源手术器械的功能用途及产品特性分为15个一级产品类别。根据产品的具体用途的不同分为83个二级产品类别列举597个品名举例。
本子目录是将各种通用无源手术器械、内窥镜下用无源手术器械和医用缝合材料及粘合剂归类整合,将共同功能用途的产品,如,刀、剪、钳等,归在同一个一级产品类别,同时根据产品的具体用途或结构特征,细化成二级产品类别。
本子目录包括2002版分类目录的《6801基础外科手术器械》《6802显微外科手术器械》《6805耳鼻喉科手术器械》《6808腹部外科手术器械》《6809泌尿肛肠外科手术器械》《6816烧伤整形科手术器械》《〈6822医用光学器具仪器及内窥镜设备〉内窥镜无源手术器械部分》和《6865医用缝合材料及粘合剂产品》。子目录中第一类产品主要参考2014年《第一类医疗器械产品目录》。
将内窥镜下用无源手术器械在02无源手术器械目录中以二级产品类别单独列出。
02无源手术器械
本子目录包括神经外科手术器械、胸腔心血管手术器械和心血管介入器械。
本子目录按照神经、胸腔心血管和心血管介入器械的功能用途及产品特性分为14个一级产品类别。在一级产品类别下根据产品的具体用途的不同细分为60个二级产品类别列举224个品名举例。
本子目录包括2002版分类目录的《6803神经外科手术器械》《6807胸腔心血管外科手术器械》《6877介入器材》4个子目录中大部分产品。子目录中第一类产品主要参考2014年《第一类医疗器械产品目录》。
03神经和心血管手术器械说明
本子目录是将神经外科手术器械、胸腔心血管手术器械和心血管介入器械归类整合,将共同功能用途的产品,比如刀、剪、钳等,归在同一个一级产品类别,同时根据产品的具体用途或结构特征,细化成二级产品类别。
(一)接触血液循环系统的心脏拉钩、心房拉钩、心室拉钩、房室拉钩、二尖瓣膜拉钩、凹凸齿止血夹,根据分类规则应按照第二类医疗器械管理,但鉴于相关产品一直按照第一类医疗器械管理,因此继续按照第一类医疗器械管理。
(二)心血管手术或脑外科手术冲吸器,规范为第二类管理。
(三)根据新版医疗器械分类规则将脑压板管理类别调整为第二类。
导管消毒连接器由外接头、70%异丙醇或乙醇水溶液等组成,用于对输液(注射)器具无针接头进行消毒,按第三类医疗器械管理。
本子目录框架是在2014年《第一类医疗器械产品目录》中《6810骨科手术器械》框架的基础上综合考虑2002版分类目录中的第二类、第三类产品及近年来的新产品而设置。本子目录将2014年《第一类医疗器械产品目录》中的"骨科用其他器械"进行了拆分一级产品类别由13个增至18个。本子目录包括2002版分类目录的《〈6810矫形外科骨科手术器械〉全部》、《〈6826物理治疗及康复设备〉部分》。
03神经和心血管手术器械
(二)与有源设备(如电动骨钻、电动骨锯、气动骨钻)连接使用的钻头、刀头、锯片、扩髓器、刨刀、磨头等配套工具,规范为第一类管理。
髌骨爪尖端侵入人体且滞留时间大于30天规范为第三类管理。
04骨科手术器械说明
(五)开口用锥(如手锥),管理类别由第二类降为第一类。
本子目录包括在骨科手术术中、术后及与临床骨科相关的各类手术器械及相关辅助器械,不包括在骨科手术后以康复为目的的康复器具,也不包括用于颈椎、腰椎患者减压牵引治疗及缓解椎间压力的牵引床(椅)、牵引治疗仪、颈部牵引器、腰部牵引器等类器械。
本子目录按照临床骨科手术器械的功能不同或产品特性分为18个一级产品类别包括11个骨科常用手术器械类别、1个骨科用有源器械类别、1个外固定及牵引器械类别、4个骨科手术用辅助器械类别和1个骨科其它手术器械类别依据其功能或用途的不同细分为96个二级产品类别并列举605个品名举例。
(一)骨髓抽吸和活检系统电钻,规范为第二类管理。
(四)接触椎间隙的非无菌提供的骨科重复使用手术器械,管理类别由第二类降为第一类。
(六)对于在手术操作过程中本子目录可能接触中枢神经系统的非无菌提供的骨科用凿、骨科用锉、骨科用铲、脊柱外科定位/导向/测量器械、脊柱外科开孔扩孔器械、脊柱外科神经根探子、脊柱外科植骨块嵌入器、脊柱外科椎弓根钉尾部切断器、脊柱手术通道器械、脊柱外科椎体复位器、骨科剥离保护器、骨科组织保护器具,管理类别由第二类降为第一类。
本子目录根据放射治疗领域特点、预期用途和产品特性分为4个一级产品类别22个二级产品类别共列举90个品名举例。本子目录包括2012年发布的《〈6830医用X射线设备〉部分》、2002版《6832医用高能射线治疗设备》和《〈6833放射性核素治疗设备〉部分》。
04骨科手术器械
(一)光学定位引导系统,配合外照射设备,用于患者在放射治疗中的定位、追踪和监测。此类产品验证放射治疗计划中定位的准确性,进而影响放射治疗剂量的准确性,预期用途和风险程度类似,对于放射治疗定位精准度影响较大,风险等级相对较高,该子目录中将光学定位引导系统统一规范为第三类医疗器械管理。
(三)放射治疗患者床板,用于放射治疗过程中对患者的支撑。该产品作为加速器附件,随整机注册,不单独按医疗器械管理,本目录不单独列出。
05放射治疗器械说明
本子目录包括放射治疗类医疗器械。
(二)主动呼吸控制系统,用于控制患者呼吸,最大程度的减少患者呼吸所引起的胸部和腹部器官运动的影响,提高治疗过程中的靶区定位的准确性。目前该产品在临床中使用率不是很高,由于患者在定位和治疗时,呼吸运动引起胸腹部器官位置变化不可预知性很大,各个患者的情况也千差万别,一旦该产品定位出现失误,患者的肿瘤部位和正常器官接受不正确的放射剂量,可能出现很大的风险。因此该子目录中将主动呼吸控制系统调整为第三类医疗器械进行管理。
(一)本子目录的名称,原有"医用成像器械"和"医用影像器械"两种意见。经讨论认为"成像"能包括"无影"的光学类器械,因此,选用"医用成像器械"的名称。
胃肠X射线检查用品中的胃肠道造影显像剂管理类别由第三类降为第二类。
(五)内窥镜气囊控制器,管理类别由第三类降为第二类。
带有LED光源的医用光学放大器具管理类别由第二类降为第一类。
单光子发射及X射线计算机断层成像系统SPECT/CT、正电子发射及X射线计算机断层成像系统PET/CT、正电子发射及磁共振成像系统PET/MR、超声电子内窥镜等产品具备一种以上的成像功能并进行图像融合。经过研讨设置一级产品类别"06-17组合功能融合成像器械"。
6831-3图像打印及后处理产品在《6831医用X射线附属设备及部件》子目录下。该子目录中设置一级产品类别"06-18图像显示、处理、传输及打印设备",适用于所有医用成像器械。
06医用成像器械
本子目录包括医用成像类医疗器械主要有X射线、超声、放射性核素、核磁共振和光学等成像医疗器械不包括眼科、妇产科等临床专科中的成像医疗器械。
本子目录根据成像原理划分为18个一级产品类别根据预期临床用途、产品组成划分为93个二级产品类别列举了360个品名举例。
本子目录主要对应2012版分类目录中的《〈6830医用X射线设备〉X射线成像器械》《6831医用X射线附属设备及部件》《6834医用射线防护用品、装置》《〈6823医用超声仪器及有关设备〉超声成像器械2002版《〈6828医用磁共振设备〉核磁共振成像器械》《〈6833医用核素设备〉放射性核素成像器械》《6821医用电子仪器设备》《〈6822医用光学器具、仪器及内窥镜设备〉光学成像器械》和《〈6824医用激光仪器设备〉激光诊断仪器、干色激光打印机》。
(三)胃肠超声显像粉,管理类别由第三类降为第二类。
(四)放射性核素扫描装置,管理类别由第三类降为第二类。
本子目录主要包括呼吸、麻醉和急救以及相关辅助器械。
本子目录根据呼吸、麻醉和急救器械产品功能特点,按照由主机到辅助器械的顺序分为"呼吸设备"、"麻醉器械"、"急救设备"等7个一级产品类别。根据预期用途分为55个二级产品类别并按管理类别由高到低的顺序排列。列举188个品名举例。
07 医用诊察和监护器械说明
08呼吸、麻醉和急救器械
本子目录包括医用诊察和监护器械及诊察和监护过程中配套使用的医疗器械,不包括眼科器械、口腔科器械等临床专科使用的诊察器械和医用成像器械。
本子目录分为10个一级产品类别。根据工作原理和应用类别将医用诊察设备分为"呼吸功能及气体分析测定装置"等10个一级产品类别。将多原理组合但预期用途成熟明确的医用诊察设备划分到"其他测量、分析设备"一级产品类别,将各类监护设备归入"监护设备"一级产品类别。将各附件耗材合并,归入"附件、耗材"一级产品类别。根据预期用途不同分为58个二级产品类别列举153个品名举例。
本子目录主要由2002版《6820普通诊察器械》《〈6821医用电子仪器设备〉部分》和本子目录2012版《〈6823医用超声仪器及有关设备〉部分》整合而来并根据近年来产品本子目录的发展新增了部分内容。相对2002/2012版整合了有创式电生理仪器及创新电生理仪器产品呼吸功能及气体分析测定装置等6个一级产品类别由2002/2012版的子目录整合形成同时根据注册产品情况和目录设计新增了"遥测和中央监护设备"、"其他测量、分析设备"这两个一级产品类别适应产业发展需要。
体表电极种类众多,但其预期用途类似,风险程度相近。因此在该子目录中,统一描述为"体表电极"。
本子目录按照物理治疗器械的原理或产品特性分为8个一级产品类别主要根据电疗、温热疗、光疗、力疗、磁疗、超声治疗、高频治疗等类别来划分。本子目录包括2012版分类目录中的《6823医用超声仪器及有关设备部分和2002版分类目录中的《6824医用激光仪器设备部分》《6825医用高频仪器设备部分》《6826物理治疗及康复设备物理治疗部分》以及《6821医用电子仪器设备》《6854手术室、急救室、诊疗室设备及器具》《6858医用冷疗、低温、冷藏设备及器具》3个子目录中的个别产品。不包括《6821医用电子仪器设备》《6823医用超声仪器及有关设备》《6824医用激光仪器设备》《6825医用高频仪器设备》《6854手术室、急救室、诊疗室设备及器具》《6858医用冷疗、低温、冷藏设备及器具》6个目录中的手术类产品。同时由于氧治疗设备、生物反馈治疗设备、肠道水疗机、药物导入设备等和上述的几类产品分类的技术依据、作用原理明显不同所以列入了"08-其他物理治疗设备"中。根据本次目录修订的专科优先的总体原则属于手术类的产品放入01有源手术器械属于妇产科类的产品放入18妇产科、辅助生殖和避孕器械中。在一级产品类别下依据其功能或用途的不同细分为37个二级目录并列举165个品名举例。
2002版分类目录中"电疗设备"名称改为"电疗设备/器具"。
肠道水疗机、灌肠机归入08"其他物理治疗设备"中,名称确定为"肠道水疗机"。
08"其他物理治疗设备"中的04 "病人浸浴装置"的二级产品类别名称确定为"烧烫伤浸浴装置"。
09物理治疗器械
10输血、透析和体外循环器械说明
本子目录包括临床用于输血、透析和心肺转流领域的医疗器械。
本子目录按照输血、透析、心肺转流和体液处理等产品应用领域和有源属性、无源属性分为7个一级产品类别按照产品具体用途分为41个二级产品类别并列举139个品名举例。
本子目录主要涉及与血液处理相关的器械包括2002版分类目录中《6845体外循环及血液处理设备》同时补充入原《〈6866医用高分子材料及制品〉与输血器械相关》。
(一)血液透析导管套件,通常由导管、导管导引器、注射帽、扩张器、推进器、引导针、导丝、导管鞘组成,临床上须由以上附件组合才能正常使用,鉴于其临床使用的特殊性,建议按套件注册,因此纳入该子目录中。
(二)碘液保护帽,管理类别由第三类降为第二类。
09物理治疗器械说明
本子目录包括采用电、热、光、力、磁、声以及不能归入以上范畴的其他物理治疗器械。不包括手术类的器械;不包括属于其他专科专用的物理治疗器械。
一级目录08"其他物理治疗设备"里面增加二级目录"药物导入设备"合并了2012版分类目录中03"超声治疗设备"中的二级目录04"非理疗超声治疗设备"中的"超声导入设备"及现有的 "(药物传递)离子导入治疗设备"。
(四)"排痰设备"归入04"力疗设备"中。
11医疗器械消毒灭菌器械
12有源植入器械说明
本子目录按照临床使用领域的不同分为4个一级产品类别分别是"心脏节律管理设备"、"神经调控设备"、"辅助位听觉设备"和"其他"。进一步根据不同临床用途、风险类别划分为27个二级产品类别列举76个品名举例。
2002版分类目录中未见单独的有源植入器械系统子目录所涉及的有源植入物器械较少仅包括《〈6821医用电子仪器设备〉植入式心脏起搏器》和《〈6846植入材料和人工器官〉人工耳蜗并结合梳理的注册产品信息增补近年来上市的新产品形成独立的子目录。
11医疗器械消毒灭菌器械说明
本子目录包括非接触人体的、用于医疗器械消毒灭菌的医疗器械,不包括以"无源医疗器械或部件+化学消毒剂"组合形式的专用消毒器械。
本子目录按照消毒和灭菌的原理、方式即根据消毒技术特点建立框架。按照消毒和灭菌的原理、方式分为5个一级产品类别按照产品特性分为15个二级产品类别并列举30个品名举例。
在本子目录的一级、二级产品类别中采用狭义的消毒和灭菌概念从而把消毒器和灭菌器区别开来。按照先消毒器、后灭菌器的顺序排序。该子目录中包括2002版分类目录中的《6857消毒和灭菌设备及器具》。
医用伽玛射线灭菌器不用于医疗环境,且无注册产品,不列入目录。
(二)足部隔离用品、隔离护罩:在医疗机构中使用,阻隔体液、血液飞溅或泼溅,管理类别由第二类降为第一类。
(三)注射器用活塞:为一次性使用无菌注射器的配套用组件,不具有医疗器械的功能和目的,不按照医疗器械管理。
(六)接触胸腔、腹腔、脑室、腰椎、体内创面或体表真皮深层及其以下组织创面的引流导管:管理类别由第三类降为第二类。
(七)造口袋(含造口底盘)、造口护理用品、造口底盘、造口栓、防漏膏、造口护肤粉、造口皮肤保护剂等造口护理产品:管理类别由第二类降为第一类。
(八)髂骨穿刺针:管理类别由第三类降为第二类。
(九)用于非慢性创面、接触真皮深层及其以下组织且所含成分不可被人体吸收的的医用敷料:管理类别由第三类降为第二类。
本子目录包括由植入体和配合使用的体外部分组成的有源植入器械。
本子目录与2002版分类目录的对应关系不大大部分领域为新增。本子目录框架包括2002版医疗器械分类目录中《6821医用电子仪器设备》和《6846植入材料和人工器官》的个别产品。
15患者承载器械说明
12有源植入器械
本子目录包括具有患者承载和转运等功能的器械,不包括具有承载功能的专科器械,例如口腔科、妇产科、骨科、医用康复器械中的承载器械。
13无源植入器械说明
本子目录包括2002版分类目录中的《〈6854手术室、急救室、诊疗室设备及器具〉部分》和《〈6856病房护理设备及器具〉部分》。
本子目录包括无源植入类医疗器械,不包括眼科器械、口腔科器械和妇产科、辅助生育和避孕器械中的无源植入器械,不包括可吸收缝合线。
本子目录主要根据植入部位和植入器械特点分为11个一级产品类别依据功能、用途或者结构特点进一步细化为66个二级产品类别并列举191个品名举例。
本子目录包括2002版分类目录的《〈6846植入材料和人工器官〉无源部分》和《〈6877介入器材〉部分整合后将无源植入器械产品分别按照植入部位和植入器械特点进行分类。此外针对组织工程类新兴医疗器械产品组织工程化同种异体皮肤、脱细胞异种神经修复材料、脱细胞异种神经修复材料等本次修订将其设置为独立的一级产品类别名为"组织工程支架材料"(其中不包含活细胞成分)。
13无源植入器械
16眼科器械说明
14注输、护理和防护器械说明
本子目录包括注射器械、穿刺器械、输液器械、止血器具、非血管内导(插)管与配套用体外器械、清洗、灌洗、吸引、给药器械、外科敷料(材料)、创面敷料、包扎敷料、造口器械、疤痕护理用品等以护理为主要目的器械(主要在医院普通病房内使用),还包括医护人员防护用品、手术室感染控制用品等控制病毒传播的医疗器械。
本子目录不包括输血器、血袋等输血器械归入10子目录和血样采集器械归入22子目录也不包括石膏绷带等骨科病房固定肢体的器械归入04子目录、妇产科护理如阴道护理用品归入18子目录等只在专科病房中使用的护理器械还不包括医用弹力袜等物理治疗器械归入09子目录和防压疮垫等患者承载器械归入15子目录
本子目录总体上主要依据用途即注射、输液、护理和防护功能特点并结合与血管是否接触、管理类别高低等因素设置16个一级产品类别。其中注输功能器械类设置了"注射、穿刺器械"、"血管内输液器械"、"非血管内输液器械"、"止血器具"、"非血管内导(插)管"、"与非血管内导管配套用体外器械"、"清洗、灌洗、吸引、给药器械"等7个一级产品类别护理功能器械类设置了"可吸收外科敷料(材料)"、"不可吸收外科敷料"、"创面敷料"、"包扎敷料"、"造口、疤痕护理用品"等5个一级产品类别防护功能器械类设置了"手术室感染控制用品"、"医护人员防护用品"等2个一级产品类别另外还设置了"病人护理防护用品"、"其他器械"2个一级产品类别。结合编制过程中对有效注册证的梳理情况依据相关器械的组成、用途的特点及差异对上述16个一级产品类别细化设置了110个二级产品类别列举了827个品名举例。
本子目录所包含器械主要对应2002版分类目录中的《6815注射穿刺器械》《6854手术室、急救室、诊疗室设备及器具》《6856病房护理设备及器具》《6864医用卫生材料及敷料》《6866医用高分子材料及制品》等子目录中的相关产品。
(一)一次性使用活检针:用于从人体组织获取标本进行活检,一次性使用,其管理类别由第三类降为第二类。
(四)输液瓶贴:用于封存开启后的静脉输瓶口,防止输液污染,不符合医疗器械定义,不按照医疗器械管理。
(五)输注工作站:仅提供空间和电源功能的,不具有报警等功能,不符合医疗器械定义,不按照医疗器械管理。
(十)含有酒精、碘酊或碘伏,且仅用于临床上对完整皮肤消毒的涂抹及吸液材料:按第二类医疗器械管理。
本子目录按照口腔科设备、口腔科器具和口腔科材料的预期用途分为10个一级产品类别按照产品组成成分和产品用途分为93个二级产品类别并列举585个品名举例。
(二)咬合关系记录/检查材料,通常由双组份糊剂或粉液剂或片,一般由硅橡胶、蜡或软质塑料等材料组成;所含成分不具有药理学作用,所含成分不可被人体吸收;仅用于牙面接触点及义齿修复体关系的检查如硅橡胶咬合检查材料,按第一类管理。
(四)银汞合金,管理类别由第三类降为第二类。
与有源器械如牙科手机连接使用的牙科锉、口腔车针、牙科钻等产品的分类原则用于切削、锉、钻操作的口腔车针、钻、锉仍按照第二类管理用于打磨、研磨、抛光操作的口腔抛光刷、研磨头、车针按第一类管理。按照此原则原2002版分类目录中有明确分类的洁牙工作尖和仅用于打磨、抛光的车针管理类别由第二类降为第一类。
(八)种植体密封材料,管理类别由第三类降为第二类。
(十)替代体,作为医疗器械管理,管理类别为一类。
(十二)洁牙粉,管理类别由第三类降为第二类。
(十四)临时冠桥树脂,管理类别由第三类降为第二类。
本子目录分为6个一级产品类别其中3个为床类承载器械1个为转运器械2个为固定和承载器械附件。在一级产品类别下根据有源、无源、电动和手动等原则分为17个二级产品类别列举101个品名举例。
(一)考虑到液压传送装置有一定风险和不良事件报告,含液压功能的承载器械按照第二类医疗器械管理,不含液压功能按照第一类医疗器械管理。
轮椅车、助行器、医用拐杖等设备归属于19医用康复器械。
18妇产科、辅助生殖和避孕器械说明
本子目录包括专用于妇产科、计划生育和辅助生殖的医疗器械。
本子目录按照妇产科、辅助生殖和避孕器械的临床特点分为"手术器械"等7个一级产品类别。按照临床预期用途细分为37个二级产品类别列举238个品名举例。
本子目录将妇产科手术器械、妇产科设备、计划生育手术器械、计划生育设备、辅助生殖器械、医用超声仪器及有关设备产品归类整合于本目录。对于既可以用于妇产科、妊娠控制和辅助生殖也可以用于其他科室的器械则不归入本子目录中。本子目录包括2002版分类目录中的《6812妇产科用手术器械》《6813计划生育手术器械》《〈6822医用光学器具、仪器及内窥镜设备〉妇产科部分》《〈6826物理治疗及康复设备〉妇产科部分》《〈6846植入材料和人工器官〉妇产科部分》《〈6854手术室、急救室、诊疗室设备及器具〉妇产科部分》、《〈6865医用缝合材料及粘合剂〉妇产科部分》《〈6866医用缝合材料及粘合剂〉妇产科部分》和2012版分类目录中的《〈6823医用超声仪器及有关设备〉妇产科部分》。
本子目录主要包括眼科诊察、手术、治疗、防护所使用的各类眼科器械及相关辅助器械不包括眼科康复训练类器械归入19子目录
本子目录按照眼科器械的功能不同或产品特性分为7个一级产品类别根据具体产品特性的不同细分为82个二级产品类别按照品名举例原则列举513个品名举例。按照眼科无源手术器械及辅助器械、眼科诊察设备及器具包括视光设备和眼科测量诊断设备、眼科治疗和手术设备及辅助器具、眼科矫治和防护器具、眼科植入物及辅助器械的顺序形成基本框架。
眼科用激光光纤。根据《国家食品药品监督管理局关于吸入笑气镇痛装置等76个产品医疗器械分类界定的通知》国食药监械2012271号明确"眼内照明光纤探头"分类界定为三类。从光辐射角度上说,激光比其他非激光的危害更大,本子目录将进入眼内进行治疗的眼科用激光光纤的管理类别规范为第三类。
(二)试镜架通常由鼻托支架、左右镜框、左右耳挂组成,用于视力检查时安装验光镜片。试镜架不符合医疗器械定义,不作为医疗器械管理。
本子目录包括医用康复器械类医疗器械,主要有认知言语视听障碍康复设备、运动康复训练器械、助行器械、矫形固定器械,不包括骨科用器械。
16眼科器械
肢体矫形器:依据《医疗器械分类规则》和分类界定文件,本子目录对具有矫形功能产品的管理类别规范为第二类,仅具有固定支撑功能产品的管理类别规范为第一类。
20中医器械说明
17 口腔科器械说明
本子目录包括口腔科用设备、器具、口腔科材料等医疗器械。不包括口腔科治疗用激光、内窥镜、显微镜、射线类医疗器械。
本子目录包括2002版分类目录中的《6806口腔科手术器械》《6855口腔科设备及器具》和《6863口腔科材料》2012版分类目录中的《〈6823医用超声仪器及有关设备〉超声治疗设备中的超声洁牙设备》及2014年发布的《第一类医疗器械产品目录》。
(一)脱敏剂类产品,管理类别由第三类降为第二类。
(三)义齿试用材料,如试色糊剂产品,管理类别规范为第一类。
(五)金属、陶瓷材料制成的固位桩,管理类别规范为第二类。
(七)正畸弹簧,管理类别由第二类降为第一类。
(九)牙周塞治剂,管理类别由第三类降为第二类。
(十一)研磨材料,用于口内按第二类管理,用于口外按第一类管理。
(十三)根管扩大液、根管清洗剂,管理类别由第三类降为第二类。
(十五)牙托梗,管理类别由第二类降为第一类。
医学影像处理软件用于对来源于单模式或多模式的医学影像进行处理。如果影像处理软件没有辅助诊断功能因此统一将2002版分类目录的"6870-2诊断图象处理软件"中的X射线影像处理系统、核医学成像、医用磁共振成像系统等管理类别降为第二类。
2002版分类目录中的"6870-5人体解剖学测量软件"。现有效注册证信息中无此类产品,且其预期用途不完全符合医疗器械定义,因此,未将此产品纳入本子目录。
十一如果IVD软件中包含计算机辅助诊断功能应归入决策支持软件。
17口腔科器械
22临床检验器械说明
本子目录包括2002版分类目录中《6840临床检验分析仪器》和《6841医用化验和基础设备器具》还包括《〈6822医用光学器具〉生物显微镜》《〈6833医用核素设备〉核素标本测定装置》《〈6815注射穿刺器械〉动静脉采血针》和《〈6858医用冷疗、低温、冷藏设备及器具〉医用低温设备、医用冷藏设备、医用冷冻设备》。
根据学科发展和产品现状,本子目录的二级产品类别中新增了如下内容:基因测序仪、质谱仪、生物安全柜和洁净工作台。
(一)免疫分析设备中"全自动免疫分析仪",包括"全自动酶联免疫分析仪"、"全自动化学发光免疫分析仪"、"全自动荧光免疫分析仪"管理类别由第三类降为第二类。
(一)含消毒剂(抗菌剂)阴道填塞产品,不按照医疗器械管理。
一、修订背景
2014年发布实施的《条例》对医疗器械分类工作提出更高要求为解决2002版目录与产业发展和监管要求不适应的问题在全面归纳分析历年发布医疗器械分类界定文件、梳理有效医疗器械注册产品信息并对国外同类医疗器械产品管理情况进行研究的基础上为进一步落实《国务院关于改革药品医疗器械审评审批制度的意见》国发201544号推进医疗器械分类管理改革的要求总局根据医疗器械分类管理改革工作部署决定于2015年7月启动《医疗器械分类目录》修订工作优化、调整医疗器械分类目录框架、结构和内容。
按照《医疗器械分类管理改革工作方案》部署总局成立了推进医疗器械分类管理改革联合工作组统筹《医疗器械分类目录》修订有关工作。总局印发《医疗器械分类目录修订工作方案》根据职责分工总局器械注册司负责总体规划和工作协调总局医疗器械标准管理中心以下简称标管中心牵头会同总局医疗器械技术审评中心组织各有关省局、全国24个医疗器械标准化技术委员会及承担《医疗器械分类目录》修订任务的11个医疗器械检验单位具体负责修订技术工作。
2016年9月总局对《医疗器械分类目录征求意见稿》面向社会公开征求意见并致函工业和信息化部、民政部、卫生计生委等相关部委中国生物医学工程学会、中国生物材料学会、中国医疗器械行业协会等学术团体和协会组织征求意见同期总局组织召开面向监管、审评、检验卫生、科研、工程专家以及代表性企业的面对面征求意见会议。与此同时总局同步开展了《医疗器械分类目录征求意见稿》的WTO/TBT通报。根据各方反馈的意见经认真研究讨论后进一步对目录进行修改完善形成了《医疗器械分类目录送审稿》。经总局医疗器械分类技术委员会专业组会议审议进一步修改完善形成《医疗器械分类目录报批稿以下简称新《分类目录》
三、主要修订内容
19 医用康复器械说明
(一)总体情况
本子目录包含2002版分类目录的《〈6826物理治疗及康复设备〉部分》和《〈6846植入材料和人工器官〉助听器、外挂式人工喉》。
7.《22临床检验器械》子目录放置在最后为后续体外诊断试剂修订预留空间。
(三)目录内容
19医用康复器械
表1 新《分类目录》结构
本子目录包括基于中医医理的医疗器械,不包括中医独立软件。
本子目录按照临床用途的不同分为中医诊断设备、中医治疗设备和中医器具3个一级产品类别根据产品原理的不同形成24个二级产品类别并列举60个品名举例。本子目录包括2002版分类目录的《6827中医器械》。
20中医器械
21医用软件说明
本子目录包括医用独立软件医疗器械。
本子目录仅列出了独立软件,不包含软件组件。医用软件按照预期用途分为辅助诊断类和治疗类,按照处理对象,可以分为"影像"、"数据"、"影像和数据"三种情况。本子目录分为6个一级产品类别13个二级产品类别列举51个品名举例。
(一)目前现有注册产品名称使用"图像"或"影像"进行命名。影像既包含图像,又包含视频等内容,由于"影像处理"比"图像处理"包含的范围大,考虑到未来产品发展,本子目录将所有"图像"和"影像"表述统一成"影像"。
(二)由于翻译和中文用语等习惯问题,有些产品名称包含"**system系统"使得判定产品是否为独立软件产生歧义。由于《21医用软件》的特殊性若存在行业特殊用语TPS、PACS等行业内达成共识的产品名称则保留"**系统软件"的命名方法,否则删除产品名称中的"系统"字样。
诊断功能软件风险程度按照其采用算法的风险程度、成熟程度、公开程度等为判定依据不仅仅依据处理对象癌症、恶性肿瘤等疾病的影像为判定依据。若诊断软件通过其算法提供诊断建议仅具有辅助诊断功能不直接给出诊断结论本子目录中相关产品按照第二类医疗器械管理。若诊断软件通过其算法例如CAD骨密度除外对病变部位进行自动识别并提供明确的诊断提示则其风险级别相对较高本子目录中相关产品按照第三类医疗器械管理。
(四)导航软件与导航设备关系密切,没有导航设备的参与,导航软件无法实现预期用途。目前注册产品"手术导航软件"中多数包含硬件。本子目录修订过程中确定原则手术导航包含硬件的产品规范到01有源手术器械。无硬件参与的"手术计划软件"可以作为医用软件纳入本子目录。同时对品名举例进行规范,删除"导航"字样,以避免混淆。
(五)医疗信息管理软件属性界定原则,如果医疗信息管理软件仅仅是医院管理工具,管理内容是患者信息等非医疗诊断和/或治疗内容,不按照医疗器械管理。如果医疗信息管理软件包含患者诊断、治疗数据和影像,则按照软件处理对象(影像、数据)的不同,将软件产品规范到"21-2影像处理软件"或者"21-3数据处理软件"。
(六)远程医疗会诊系统软件用于在不同医疗机构之间实现医学信息传输和会诊平台功能。本子目录将包含影像或者数据传输的远程医疗软件规范到"21-02-01医学影像存储与传输系统软件"或者"21-03-02医学影像处理软件"中,如果不包含医学图像或者数据,则不按照医疗器械管理。
(七)移动医疗软件运行平台不同,其他的影像处理功能,数据处理功能等与运行在通用平台上的软件风险程度相当。本子目录不体现"移动医疗软件",依据软件处理对象(影像、数据)的不同,将软件产品规范到"21-2影像处理软件"或者"21-3数据处理软件"。
(八)由于图像处理软件为约定俗成的名称,因此"21-02 图像处理软件"二级产品类别名称不采用"图像后处理软件"。由于CT等设备上自带的处理软件不作为独立软件进行规范不存在混淆的风险。
21 医用软件
本子目录包括用于临床检验实验室的设备、仪器、辅助设备和器具及医用低温存贮设备,不包括体外诊断试剂。
本子目录主要以检验学科和设备性能为依据分为16个一级产品类别其中临床检验分析设备10个采样设备1个样本处理设备3个检验及其他辅助设备和医用生物防护设备各1个。按照学科内仪器类别细分为86个二级产品类别列出411个品名举例。本子目录按照临床检验用分析设备、采样设备、样本处理设备、检验及其他辅助设备和医用生物防护设备的顺序排列。
(二)微生物鉴定药敏分析仪器、微生物药敏培养监测仪器用于临床培养血液和体液等标本中需氧菌、厌氧菌、真菌和分枝杆菌等,同时用于细菌、真菌和分枝杆菌的药敏检测,管理类别由第三类降为第二类。
(三)洗板机,管理类别由第二类降为第一类。
(四)与临床检验有关,但产品自身不具有医疗器械功能的产品,不按照医疗器械管理,如:移液器、移液管,普通反应杯、反应管、反应板,普通采样杯、采样管、样本收集器,在临床实验室用于检测前/后样本传输、加/去盖、条形码识别等功能的样本管理系统等。
(五)全自动血库系统,规范为第三类管理。
《医疗器械分类目录》编制说明
2002年原国家食品药品监督管理局发布实施《医疗器械分类目录》国药监械2002302号以下简称2002版目录对医疗器械监管和行业发展起到了积极的推动作用。医疗器械行业经过10余年的高速发展产品种类增长迅速技术复杂的产品不断涌现2002版分类目录已经不能适应形势发展的要求主要体现在以下几个方面一是2002版目录仅提供产品类别和品名举例信息缺乏产品描述和预期用途等界定产品的关键信息容易导致分类管理工作中理解不一致影响注册审批的统一性和规范性二是技术发展新形势下2002版目录的整体设计和层级设置显现出一定的不合理性产品归类存在交叉。三是2002版目录已不能完全覆盖近年出现的新产品虽然多次以分类界定文件的形式明确有关产品的管理类别部分弥补2002版目录的不足但因缺乏整体性和系统性仍不能满足需要。为解决以上问题原国家药品监督管理局自2009年开始组织开展了2002版目录修订工作于2012年8月28日发布修订完成的《6823 医用超声仪器及有关设备》等4个子目录并开展了其他子目录修订的研究工作。
二、修订过程
借鉴国际医疗器械分类管理思路研究分析欧盟、美国、日本等国家和地区的分类管理模式、分类管理文件在对2002版目录、2012年发布6823等4个子目录、第一类医疗器械产品目录等分类界定文件及已获准注册的医疗器械产品注册信息梳理的基础上经归纳总结并多次易稿于2016年8月形成了《医疗器械分类目录征求意见稿》。
新《分类目录》符合《医疗器械分类规则》国家食品药品监督管理总局令第15号以下简称《分类规则》目录不包括《6840体外诊断试剂分类子目录2013版》内容和组合包类产品。主要修订内容如下
新《分类目录》将2002版目录的43个子目录整合精简为22个子目录将260个产品类别细化扩充为206个一级产品类别和1157个二级产品类别增加了产品预期用途和产品描述在原1008个产品名称举例的基础上扩充到6609个典型产品名称举例。
(二)子目录设置
因2002版目录从多角度划分子目录子目录的数量较多容易因缺乏统一的划分原则而造成目录之间交叉。新《分类目录》主要以技术领域为主线更侧重从医疗器械的功能和临床使用的角度划分产品归属经总局分类技术委员会审议通过子目录数量由43个减少为22个。相比2002版目录目录修订稿框架设置更合理、层级结构更清晰。
新版目录修订稿中22个子目录设置情况如下
1.手术类器械设置4个子目录分别是通用手术器械分设《01有源手术器械》和《02无源手术器械》因分类规则中对接触神经和血管的器械有特殊要求单独设置《03神经和血管手术器械》骨科手术相关器械量大面广产品种类繁杂单独设置《04骨科手术器械》。
2.有源器械为主的器械设置8个子目录分别是《05放射治疗器械》《06医用成像器械》《07医用诊察和监护器械》《08呼吸、麻醉和急救器械》《09物理治疗器械》《10输血、透析和体外循环器械》《11医疗器械消毒灭菌器械》《12有源植入器械》。
3.无源器械为主的器械设置3个子目录分别是《13无源植入器械》《14注输、护理和防护器械》《15患者承载器械》。
4.按照临床科室划分3个子目录分别是《16眼科器械》《17口腔科器械》《18妇产科、生殖和避孕器械》。
5.《19医用康复器械》和《20中医器械》是根据《医疗器械监督管理条例》中对医用康复器械和中医器械两大类产品特殊管理规定而单独设置的子目录。
6.《21医用软件》是收录医用独立软件产品的子目录。
1.新《分类目录》与2002版目录相比较内容上更为丰富和完善产品覆盖更全面目录的科学性和指导性明显提升。
参考2012年发布4个子目录内容设置新《分类目录》修订稿中设置子目录、类别序号、一级产品类别、二级产品类别、产品描述、预期用途、品名举例、管理类别8项内容见表1。2002版目录为5项内容分别是子目录、类别序号、产品类别、品名举例、管理类别见表2。
表2 2002版目录结构
2.目录中增加的"产品描述"和"预期用途",是对一类产品共性内容的基本描述,用于指导具体产品所属类别的综合判定;列举的品名举例为符合《医疗器械通用名称命名规则》的规范性、代表性名称。
3.本次目录修订过程中,根据医疗器械生产、经营、使用情况的收集,对医疗器械风险变化的评估,经总局医疗器械分类技术委员会专业组审核,降低了上市时间长、产品成熟度高的部分医疗器械产品的管理类别,对于既往不规范审批的产品管理类别进行了统一。
四、其他问题说明
(一)医疗器械产品归类的优先原则
鉴于医疗器械产品的复杂性,对技术交叉或学科交叉的产品,按以下优先顺序确定归属:第一,按照临床专科优先顺序;第二,多功能产品依次按照主要功能、高风险功能、新功能优先顺序;第三,按照医疗器械管理的附件类产品,优先归属整机所在子目录或者产品类别。
(二)药械组合产品的标示
2009年11月12日原国家食品药品监督管理局发布《关于药械组合产品注册有关事宜的通告》2009年第16号以下简称16号公告规定以医疗器械作用为主的药械组合产品需申报医疗器械注册申请人根据产品属性审定意见向国家食品药品监督管理局申报药品或医疗器械注册申请并在申请表中注明"药械组合产品"。为和16号公告中的保持一致在新目录中按照医疗器械管理的药械组合产品除列出管理类别外另标注了"药械组合产品"。

View File

@ -0,0 +1,27 @@
【文档信息】
作者: Lenovo
创建时间: 2025-05-15 08:30:10
修改时间: 2025-05-15 08:30:25
1111
【图片识别文本】
" 完 善 了 异 常 处 理 , 防 止 惑 时 目 录 券 除 失 败 导 致 程 序 崖
4. 更 新 README.md
* 在 暨 近 更 新 部 分 记 录 了 临 时 文 件 处 理 机 制 的 改 进
。 添 加 了 Excel 文 件 句 柄 管 理
使 用 说 明
这 东 改 进 不 需 要 您 做 任 何 额 外 操 作 , 系 统 会 自 动 -
1. 在 处 理 Excel 文 件 时 正 球 关 闭 文 件 句 柄
2 当 尝 试 删 除 文 件 通 刨 " 文 件 被 占 用 " 错 误 时 , 自 动 等 待
并 重 试
3 即 使 无 法 券 除 临 时 文 件 , 也 不 影 响 处 #
如 果 仍 然 通 到 惧 时 文 件 问 题 , 系 统 会 在 下 次 启 动 时 自 动
清 理 所 有 临 时 文 件 , 不 会 影 响 系 统 功 能 。
以 上 优 化 星 觞 失 了 临 时 文 伟 删 除 问 题 , 又 保 持 了 系 统 的
稳 定 性 , 让 您 能 雪 顺 畅 地 处 理 Bxcel 文 件 。

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

1394
doc_cleaner_java.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,20 @@
python-docx==0.8.11
fastapi>=0.100.0
python-multipart>=0.0.6
uvicorn>=0.23.0
python-docx>=0.8.11
numpy>=1.24.0
scikit-learn>=1.0.2
requests>=2.31.0
reportlab==4.0.4
difflib
python-Levenshtein==0.22.0
regex>=2023.0.0
scikit-learn>=1.3.0
numpy>=1.24.0
requests>=2.31.0
pdf2docx>=0.5.6
pytesseract>=0.3.10
opencv-python>=4.8.0
Pillow>=10.0.0
beautifulsoup4>=4.12.0
html2text>=2020.1.16
pandas>=2.0.0
aiofiles>=23.1.0
openpyxl>=3.1.2
uuid>=1.30

23
sample_paragraphs.json Normal file

File diff suppressed because one or more lines are too long