Files
lyxy-document/openspec/specs/document-reading/spec.md
lanyuanxiaoyao 750ef50a8d 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)
2026-03-08 16:33:40 +08:00

220 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Purpose
统一文档读取核心能力,包含 CLI 入口、解析调度、Markdown 后处理。
## Requirements
### Requirement: 统一 CLI 入口
系统 SHALL 提供 lyxy_document_reader.py 作为统一的命令行入口,支持处理所有文档类型。
#### Scenario: 调用 CLI 帮助信息
- **WHEN** 用户执行 `uv run python lyxy_document_reader.py --help`
- **THEN** 系统显示完整的命令行参数帮助信息
#### Scenario: CLI 接受输入路径
- **WHEN** 用户执行 `uv run python lyxy_document_reader.py <input_path>`
- **THEN** 系统识别输入类型并解析文档
### Requirement: 输入类型自动识别
系统 SHALL 通过遍历所有 reader 并调用其 supports() 方法来识别输入类型,而非在 parse_input() 中进行特殊判断。
#### Scenario: 遍历 readers 判断支持
- **WHEN** 调用 parse_input(input_path, readers)
- **THEN** 系统遍历 readers 列表,对每个 reader 调用 supports(input_path)
#### Scenario: 找到支持的 reader
- **WHEN** 某个 reader 的 supports() 返回 True
- **THEN** 系统调用该 reader 的 parse() 方法
#### Scenario: 无 reader 支持时抛出异常
- **WHEN** 所有 reader 的 supports() 均返回 False
- **THEN** 系统抛出 ReaderNotFoundError 异常
#### Scenario: URL 由 HTML reader 判断
- **WHEN** 输入为 URL
- **THEN** HTML reader 的 supports() 返回 True由其处理
#### Scenario: 文件类型由各 reader 判断
- **WHEN** 输入为本地文件
- **THEN** 各 reader 的 supports() 根据扩展名判断是否支持
### 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 内容。
#### Scenario: 无查询参数时输出完整内容
- **WHEN** 用户不使用任何查询参数(-c/-l/-t/-tc/-s
- **THEN** 系统输出完整的 Markdown 文档内容
### Requirement: 字数统计
系统 SHALL 支持统计解析后文档的总字数。
#### Scenario: 使用 -c 参数统计字数
- **WHEN** 用户使用 `-c``--count` 参数
- **THEN** 系统输出解析后文档的总字数(不含换行符)
### Requirement: 行数统计
系统 SHALL 支持统计解析后文档的总行数。
#### Scenario: 使用 -l 参数统计行数
- **WHEN** 用户使用 `-l``--lines` 参数
- **THEN** 系统输出解析后文档的总行数
### Requirement: 标题提取
系统 SHALL 支持提取文档中的所有标题行。
#### Scenario: 使用 -t 参数提取标题
- **WHEN** 用户使用 `-t``--titles` 参数
- **THEN** 系统输出解析后文档的所有 1-6 级标题行
### Requirement: 指定标题内容提取
系统 SHALL 支持提取指定标题及其下级内容。
#### Scenario: 使用 -tc 参数提取标题内容
- **WHEN** 用户使用 `-tc <name>``--title-content <name>` 参数
- **THEN** 系统输出所有匹配标题名称的标题及其下级内容,包含上级标题上下文,多个匹配用 --- 分隔
#### Scenario: 标题未找到时提示错误
- **WHEN** 指定的标题名称未找到
- **THEN** 系统输出错误信息并退出非零状态码
### Requirement: 正则搜索
系统 SHALL 支持使用正则表达式搜索文档内容。
#### Scenario: 使用 -s 参数搜索
- **WHEN** 用户使用 `-s <pattern>``--search <pattern>` 参数
- **THEN** 系统输出所有匹配结果及其上下文,多个匹配用 --- 分隔
#### Scenario: 使用 -n 参数指定上下文行数
- **WHEN** 用户同时使用 `-s <pattern>``-n <num>``--context <num>` 参数
- **THEN** 系统输出匹配结果及其前后指定行数的上下文(不含空行)
#### Scenario: 无效正则表达式提示错误
- **WHEN** 提供的正则表达式无效
- **THEN** 系统输出错误信息并退出非零状态码
#### Scenario: 无匹配结果提示错误
- **WHEN** 未找到任何匹配
- **THEN** 系统输出错误信息并退出非零状态码
### Requirement: Markdown 图片移除
系统 SHALL 自动移除解析结果中的 Markdown 图片标记。
#### Scenario: 移除图片标记
- **WHEN** 解析结果包含 `![alt](url)` 格式的图片标记
- **THEN** 系统移除这些图片标记
### Requirement: Markdown 空白规范化
系统 SHALL 规范化解析结果中的空白字符,保留单行空行。
#### Scenario: 规范化连续空行
- **WHEN** 解析结果包含连续 3 个或更多换行符
- **THEN** 系统将其替换为 2 个换行符(保留单行空行)
### Requirement: 使用 logging 模块
系统 SHALL 使用 Python logging 模块进行日志输出,而非简单 print。
#### Scenario: 日志输出
- **WHEN** 系统运行时
- **THEN** 所有日志信息通过 logging 模块输出
### Requirement: 自定义异常
系统 SHALL 使用自定义异常类处理错误,提供清晰的错误信息和位置上下文。
#### Scenario: 抛出 FileDetectionError
- **WHEN** 文件类型检测失败
- **THEN** 系统抛出 FileDetectionError 异常
#### Scenario: 抛出 ReaderNotFoundError
- **WHEN** 未找到适配的阅读器
- **THEN** 系统抛出 ReaderNotFoundError 异常
#### Scenario: 抛出 ParseError
- **WHEN** 文档解析失败
- **THEN** 系统抛出 ParseError 异常
#### Scenario: 抛出 DownloadError
- **WHEN** URL 下载失败
- **THEN** 系统抛出 DownloadError 异常