refactor: 重构解析器架构并添加编码检测和配置管理

简化 parse_input() 为纯调度器,通过遍历 readers 的 supports() 方法识别输入类型,移除 URL 特殊处理和文件检查逻辑。各 reader 的 parse() 方法负责完整验证(文件存在、格式有效性)。

新增功能:
- 添加 chardet 编码自动检测,支持多种中文编码回退机制
- 创建统一配置类管理编码、下载超时、日志等级等配置项
- HTML reader 支持本地文件编码检测和 URL 统一处理

安全性改进:
- 修复 safe_open_zip() 路径遍历漏洞,使用 pathlib 规范化路径
- 添加边界检查,search_markdown() 检查负数参数

其他改进:
- 修复类型注解(argparse.Namespace)
- 日志系统仅输出 ERROR 级别,避免干扰 Markdown 输出
- 更新 BaseReader 接口文档,明确 supports() 和 parse() 职责划分
- 同步 delta specs 到主 specs(document-reading、html-reader、configuration、encoding-detection)
This commit is contained in:
2026-03-08 16:33:40 +08:00
parent eb044d37d9
commit 750ef50a8d
17 changed files with 5567 additions and 76 deletions

View File

@@ -16,31 +16,109 @@
- **THEN** 系统识别输入类型并解析文档
### Requirement: 输入类型自动识别
系统 SHALL 自动识别输入类型,包括 URL 和本地文件
系统 SHALL 通过遍历所有 reader 并调用其 supports() 方法来识别输入类型,而非在 parse_input() 中进行特殊判断
#### Scenario: 识别 HTTP URL
- **WHEN** 输入以 `http://``https://` 开头
- **THEN** 系统识别为 URL使用 html-reader 处理
#### Scenario: 遍历 readers 判断支持
- **WHEN** 调用 parse_input(input_path, readers)
- **THEN** 系统遍历 readers 列表,对每个 reader 调用 supports(input_path)
#### Scenario: 识别 DOCX 文件
- **WHEN** 输入文件扩展名为 .docx 或 .doc或文件内容为 OOXML Word 格式
- **THEN** 系统识别为 DOCX使用 docx-reader 处理
#### Scenario: 找到支持的 reader
- **WHEN** 某个 reader 的 supports() 返回 True
- **THEN** 系统调用该 reader 的 parse() 方法
#### Scenario: 识别 XLSX 文件
- **WHEN** 输入文件扩展名为 .xlsx、.xls 或 .xlsm或文件内容为 OOXML Excel 格式
- **THEN** 系统识别为 XLSX使用 xlsx-reader 处理
#### Scenario: 无 reader 支持时抛出异常
- **WHEN** 所有 reader 的 supports() 均返回 False
- **THEN** 系统抛出 ReaderNotFoundError 异常
#### Scenario: 识别 PPTX 文件
- **WHEN** 输入文件扩展名为 .pptx 或 .ppt或文件内容为 OOXML PowerPoint 格式
- **THEN** 系统识别为 PPTX使用 pptx-reader 处理
#### Scenario: URL 由 HTML reader 判断
- **WHEN** 输入为 URL
- **THEN** HTML reader 的 supports() 返回 True由其处理
#### Scenario: 识别 PDF 文件
- **WHEN** 输入文件扩展名为 .pdf或文件头为 %PDF
- **THEN** 系统识别为 PDF使用 pdf-reader 处理
#### Scenario: 文件类型由各 reader 判断
- **WHEN** 输入为本地文件
- **THEN** 各 reader 的 supports() 根据扩展名判断是否支持
#### Scenario: 识别 HTML 文件
- **WHEN** 输入文件扩展名为 .html、.htm 或 .xhtml
- **THEN** 系统识别为 HTML使用 html-reader 处理
### Requirement: parse_input() 职责简化
系统 SHALL 将 parse_input() 简化为纯粹的 reader 遍历和调度器,移除 URL 特殊处理和文件存在检查。
#### Scenario: 仅检查输入是否为空
- **WHEN** 调用 parse_input(input_path, readers)
- **THEN** 系统仅检查 input_path 是否为空,为空则抛出 FileDetectionError
#### Scenario: 不检查文件是否存在
- **WHEN** 调用 parse_input() 时
- **THEN** 系统不检查文件是否存在,由各 reader 的 parse() 方法自行检查
#### Scenario: 不进行文件类型检测
- **WHEN** 调用 parse_input() 时
- **THEN** 系统不调用 detect_file_type(),由各 reader 的 parse() 方法自行验证
#### Scenario: 不特殊处理 URL
- **WHEN** 输入为 URL 时
- **THEN** 系统不使用 hasattr 检查 download_and_parse而是通过 supports() 和 parse() 统一处理
### Requirement: BaseReader 接口语义明确
系统 SHALL 明确 BaseReader 的 supports() 和 parse() 方法的职责划分。
#### Scenario: supports() 为轻量检查
- **WHEN** 调用 reader.supports(file_path)
- **THEN** 方法仅做轻量检查如扩展名、URL 模式),不访问文件系统
#### Scenario: parse() 为完整验证
- **WHEN** 调用 reader.parse(file_path)
- **THEN** 方法进行完整验证(文件存在、格式有效性)并执行解析
#### Scenario: BaseReader 文档字符串更新
- **WHEN** 查看 BaseReader 源码
- **THEN** supports() 和 parse() 的文档字符串明确说明职责划分
### Requirement: 边界检查改进
系统 SHALL 对无效参数进行边界检查,抛出明确异常。
#### Scenario: search_markdown 检查空内容
- **WHEN** 调用 search_markdown(content, pattern, context_lines) 且 content 为空
- **THEN** 系统返回 None
#### Scenario: search_markdown 检查负数 context_lines
- **WHEN** 调用 search_markdown() 且 context_lines < 0
- **THEN** 系统抛出 ValueError 异常,消息为 "context_lines 必须为非负整数"
### Requirement: safe_open_zip 安全性修复
系统 SHALL 使用 pathlib 规范化路径并检查路径遍历攻击。
#### Scenario: 使用 pathlib 规范化路径
- **WHEN** 调用 safe_open_zip(zip_file, name)
- **THEN** 系统使用 Path(name).as_posix() 规范化路径
#### Scenario: 检查父目录引用
- **WHEN** 路径包含 ".." 在 Path.parts 中
- **THEN** 系统返回 None拒绝打开
#### Scenario: 检查绝对路径
- **WHEN** 路径为绝对路径
- **THEN** 系统返回 None拒绝打开
#### Scenario: 处理路径异常
- **WHEN** Path() 抛出 ValueError 或 OSError
- **THEN** 系统捕获异常并返回 None
### Requirement: 类型注解修复
系统 SHALL 修复类型注解不一致问题。
#### Scenario: output_result 使用正确类型
- **WHEN** 定义 output_result(content, args)
- **THEN** args 参数类型注解为 argparse.Namespace
### Requirement: 日志系统改进
系统 SHALL 只输出 ERROR 级别日志,避免干扰 Markdown 输出。
#### Scenario: 配置日志等级为 ERROR
- **WHEN** 系统启动时
- **THEN** 配置 logging.basicConfig(level=logging.ERROR)
#### Scenario: 禁用第三方库日志
- **WHEN** 系统启动时
- **THEN** 设置第三方库docling、unstructured 等)日志等级为 ERROR
### Requirement: 输出完整 Markdown
系统 SHALL 能够输出解析后的完整 Markdown 内容。