简化 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)
220 lines
8.4 KiB
Markdown
220 lines
8.4 KiB
Markdown
## 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** 解析结果包含 `` 格式的图片标记
|
||
- **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 异常
|