- 新增 scripts/readers/_utils.py 作为 Reader 内部共享工具模块 - 将 parse_with_markitdown 等函数从 core/markdown.py 迁移到 _utils.py - 函数重命名:parse_with_xxx → parse_via_xxx,_unstructured_elements_to_markdown → convert_unstructured_to_markdown - 更新 17 个 Reader 实现文件的 import 路径 - 从 core/__init__.py 移除已迁移函数的导出 - 新增测试文件 tests/test_readers/test_utils.py - 新增 spec 文档 openspec/specs/reader-internal-utils/spec.md 这次重构明确了模块边界:core/ 提供公共 API,readers/_utils.py 提供 Reader 内部工具
96 lines
3.7 KiB
Markdown
96 lines
3.7 KiB
Markdown
## Purpose
|
||
|
||
提供 Reader 内部共享工具模块,包含解析器包装函数、格式化工具、ZIP 安全处理和 unstructured 库集成。此模块仅供 readers 包内部使用,不作为公共 API。
|
||
|
||
## Requirements
|
||
|
||
### Requirement: 解析器包装函数
|
||
系统 SHALL 提供统一的解析器包装函数,封装第三方库的调用细节。
|
||
|
||
#### Scenario: 使用 MarkItDown 解析
|
||
- **WHEN** 调用 `parse_via_markitdown(file_path)`
|
||
- **THEN** 系统使用 MarkItDown 库解析文件
|
||
- **AND** 成功时返回 `(markdown_content, None)`
|
||
- **AND** 失败时返回 `(None, error_message)`
|
||
|
||
#### Scenario: 使用 docling 解析
|
||
- **WHEN** 调用 `parse_via_docling(file_path)`
|
||
- **THEN** 系统使用 docling 库解析文件
|
||
- **AND** 成功时返回 `(markdown_content, None)`
|
||
- **AND** 失败时返回 `(None, error_message)`
|
||
|
||
#### Scenario: 库未安装时返回友好错误
|
||
- **WHEN** 调用解析器包装函数但对应库未安装
|
||
- **THEN** 系统返回 `(None, "<库名> 库未安装")`
|
||
|
||
### Requirement: Markdown 表格格式化
|
||
系统 SHALL 提供将二维列表格式化为 Markdown 表格的工具函数。
|
||
|
||
#### Scenario: 格式化标准表格
|
||
- **WHEN** 调用 `build_markdown_table(rows_data)` 且 rows_data 包含表头和数据行
|
||
- **THEN** 系统生成标准 Markdown 表格格式
|
||
- **AND** 第一行前生成分隔行(`| --- | --- |`)
|
||
|
||
#### Scenario: 空数据返回空字符串
|
||
- **WHEN** 调用 `build_markdown_table([])` 或 `build_markdown_table([[]])`
|
||
- **THEN** 系统返回空字符串
|
||
|
||
### Requirement: 列表堆栈处理
|
||
系统 SHALL 提供列表堆栈处理工具函数,用于处理嵌套列表的格式化输出。
|
||
|
||
#### Scenario: 刷新列表堆栈
|
||
- **WHEN** 调用 `flush_list_stack(list_stack, target)`
|
||
- **THEN** 系统将 list_stack 中所有非空项添加到 target 列表
|
||
- **AND** 每个项末尾添加换行符
|
||
- **AND** 清空 list_stack
|
||
|
||
#### Scenario: 跳过空项
|
||
- **WHEN** list_stack 中包含空字符串
|
||
- **THEN** 系统跳过空项,不添加到 target
|
||
|
||
### Requirement: ZIP 文件安全打开
|
||
系统 SHALL 提供安全的 ZIP 文件打开函数,防止路径遍历攻击。
|
||
|
||
#### Scenario: 打开合法文件
|
||
- **WHEN** 调用 `safe_open_zip(zip_file, "valid/file.txt")`
|
||
- **THEN** 系统返回对应的 ZipExtFile 对象
|
||
|
||
#### Scenario: 拒绝路径遍历攻击
|
||
- **WHEN** 路径包含 ".." 在 Path.parts 中
|
||
- **THEN** 系统返回 None
|
||
|
||
#### Scenario: 拒绝绝对路径
|
||
- **WHEN** 路径为绝对路径
|
||
- **THEN** 系统返回 None
|
||
|
||
#### Scenario: 处理路径异常
|
||
- **WHEN** Path() 抛出 ValueError 或 OSError
|
||
- **THEN** 系统捕获异常并返回 None
|
||
|
||
### Requirement: unstructured 元素转换
|
||
系统 SHALL 提供将 unstructured 库解析的元素转换为 Markdown 的工具函数。
|
||
|
||
#### Scenario: 转换标准元素
|
||
- **WHEN** 调用 `convert_unstructured_to_markdown(elements, trust_titles=True)`
|
||
- **THEN** 系统跳过 Header、Footer、PageBreak、PageNumber 元素
|
||
- **AND** 跳过 RGB 颜色值和页码噪声
|
||
- **AND** Table 元素转换为 Markdown 表格
|
||
- **AND** Title 元素转换为 # 标题(根据 category_depth 确定级别)
|
||
- **AND** ListItem 元素转换为 - 列表项
|
||
- **AND** Image 元素转换为  格式
|
||
|
||
#### Scenario: 库未安装时回退
|
||
- **WHEN** markdownify 或 unstructured 库未安装
|
||
- **THEN** 系统提取所有元素的 text 属性并用双换行连接
|
||
|
||
### Requirement: 噪声模式匹配
|
||
系统 SHALL 定义 unstructured 库的噪声匹配模式。
|
||
|
||
#### Scenario: 匹配 RGB 颜色值
|
||
- **WHEN** 文本匹配 `_UNSTRUCTURED_RGB_PATTERN`(如 "R:255 G:128 B:0")
|
||
- **THEN** 系统将其识别为噪声并过滤
|
||
|
||
#### Scenario: 匹配页码
|
||
- **WHEN** 文本匹配 `_UNSTRUCTURED_PAGE_NUMBER_PATTERN`(如 "— 3 —")
|
||
- **THEN** 系统将其识别为噪声并过滤
|