From cf10458dd618424b27b6f7df09fc8fbb0e036fc6 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 10 Mar 2026 23:09:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20doc/xls/ppt=20?= =?UTF-8?q?=E6=97=A7=E6=A0=BC=E5=BC=8F=E6=96=87=E6=A1=A3=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 DocReader,支持 markitdown 和 pypandoc-binary 解析器 - 新增 XlsReader,支持 unstructured、markitdown 和 pandas+xlrd 解析器 - 新增 PptReader,支持 markitdown 解析器 - 添加 olefile 依赖用于验证 OLE2 格式 - 更新 config.py 添加 doc/xls/ppt 依赖配置 - 更新 --advice 支持 doc/xls/ppt 格式 - 添加相应的测试用例 - 同步 specs 到主目录 --- README.md | 5 +- openspec/specs/doc-reader/spec.md | 53 +++++++++++++++ openspec/specs/ppt-reader/spec.md | 38 +++++++++++ openspec/specs/xls-reader/spec.md | 68 +++++++++++++++++++ scripts/config.py | 32 +++++++++ scripts/core/advice_generator.py | 6 ++ scripts/lyxy_document_reader.py | 6 +- scripts/readers/__init__.py | 9 +++ scripts/readers/doc/__init__.py | 48 +++++++++++++ scripts/readers/doc/markitdown.py | 10 +++ scripts/readers/doc/pypandoc.py | 29 ++++++++ scripts/readers/ppt/__init__.py | 46 +++++++++++++ scripts/readers/ppt/markitdown.py | 10 +++ scripts/readers/xls/__init__.py | 50 ++++++++++++++ scripts/readers/xls/markitdown.py | 10 +++ scripts/readers/xls/pandas.py | 41 +++++++++++ scripts/readers/xls/unstructured.py | 22 ++++++ scripts/utils/__init__.py | 6 ++ scripts/utils/file_detection.py | 35 +++++++++- tests/test_cli/test_main.py | 21 ++++++ tests/test_readers/test_doc/__init__.py | 1 + .../test_readers/test_doc/test_consistency.py | 21 ++++++ .../test_doc/test_markitdown_doc.py | 25 +++++++ .../test_doc/test_pypandoc_doc.py | 25 +++++++ tests/test_readers/test_ppt/__init__.py | 1 + .../test_readers/test_ppt/test_consistency.py | 18 +++++ .../test_ppt/test_markitdown_ppt.py | 25 +++++++ tests/test_readers/test_xls/__init__.py | 1 + .../test_readers/test_xls/test_consistency.py | 24 +++++++ .../test_xls/test_markitdown_xls.py | 25 +++++++ .../test_readers/test_xls/test_pandas_xls.py | 25 +++++++ .../test_xls/test_unstructured_xls.py | 25 +++++++ 32 files changed, 756 insertions(+), 5 deletions(-) create mode 100644 openspec/specs/doc-reader/spec.md create mode 100644 openspec/specs/ppt-reader/spec.md create mode 100644 openspec/specs/xls-reader/spec.md create mode 100644 scripts/readers/doc/__init__.py create mode 100644 scripts/readers/doc/markitdown.py create mode 100644 scripts/readers/doc/pypandoc.py create mode 100644 scripts/readers/ppt/__init__.py create mode 100644 scripts/readers/ppt/markitdown.py create mode 100644 scripts/readers/xls/__init__.py create mode 100644 scripts/readers/xls/markitdown.py create mode 100644 scripts/readers/xls/pandas.py create mode 100644 scripts/readers/xls/unstructured.py create mode 100644 tests/test_readers/test_doc/__init__.py create mode 100644 tests/test_readers/test_doc/test_consistency.py create mode 100644 tests/test_readers/test_doc/test_markitdown_doc.py create mode 100644 tests/test_readers/test_doc/test_pypandoc_doc.py create mode 100644 tests/test_readers/test_ppt/__init__.py create mode 100644 tests/test_readers/test_ppt/test_consistency.py create mode 100644 tests/test_readers/test_ppt/test_markitdown_ppt.py create mode 100644 tests/test_readers/test_xls/__init__.py create mode 100644 tests/test_readers/test_xls/test_consistency.py create mode 100644 tests/test_readers/test_xls/test_markitdown_xls.py create mode 100644 tests/test_readers/test_xls/test_pandas_xls.py create mode 100644 tests/test_readers/test_xls/test_unstructured_xls.py diff --git a/README.md b/README.md index 4f4635a..2e91ce8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # lyxy-document -统一文档解析工具 - 将 DOCX、XLSX、PPTX、PDF、HTML/URL 转换为 Markdown +统一文档解析工具 - 将 DOC、DOCX、XLS、XLSX、PPT、PPTX、PDF、HTML/URL 转换为 Markdown ## 项目概述 @@ -25,8 +25,11 @@ scripts/ │ └── exceptions.py # 异常定义 ├── readers/ # 格式阅读器 │ ├── base.py # Reader 基类 +│ ├── doc/ # DOC 解析器(旧格式) │ ├── docx/ # DOCX 解析器 +│ ├── xls/ # XLS 解析器(旧格式) │ ├── xlsx/ # XLSX 解析器 +│ ├── ppt/ # PPT 解析器(旧格式) │ ├── pptx/ # PPTX 解析器 │ ├── pdf/ # PDF 解析器 │ └── html/ # HTML/URL 解析器 diff --git a/openspec/specs/doc-reader/spec.md b/openspec/specs/doc-reader/spec.md new file mode 100644 index 0000000..25f51a0 --- /dev/null +++ b/openspec/specs/doc-reader/spec.md @@ -0,0 +1,53 @@ +## Purpose + +DOC 文档解析能力,支持解析 Microsoft Word 97-2003 旧格式文档。 + +## Requirements + +### Requirement: DOC 文档解析 +系统 SHALL 支持解析 .doc 格式文档,按优先级尝试多个解析器。 + +#### Scenario: 按优先级尝试解析器 +- **WHEN** 解析 DOC 文档 +- **THEN** 系统按 markitdown → pypandoc-binary 的顺序尝试 + +#### Scenario: 成功解析 +- **WHEN** 任一解析器成功 +- **THEN** 系统返回解析结果 + +#### Scenario: 所有解析器失败 +- **WHEN** 所有解析器均失败 +- **THEN** 系统返回失败列表并退出非零状态码 + +### Requirement: markitdown 解析器 +系统 SHALL 支持使用 markitdown 库解析 DOC。 + +#### Scenario: markitdown 解析成功 +- **WHEN** markitdown 库可用且文档有效 +- **THEN** 系统返回 Markdown 内容 + +#### Scenario: markitdown 库未安装 +- **WHEN** markitdown 库未安装 +- **THEN** 系统尝试下一个解析器 + +### Requirement: pypandoc-binary 解析器 +系统 SHALL 支持使用 pypandoc-binary 库解析 DOC。 + +#### Scenario: pypandoc 解析成功 +- **WHEN** pypandoc-binary 库可用且文档有效 +- **THEN** 系统返回 Markdown 内容 + +#### Scenario: pypandoc-binary 库未安装 +- **WHEN** pypandoc-binary 库未安装 +- **THEN** 系统尝试下一个解析器 + +### Requirement: 每个解析器独立文件 +系统 SHALL 将每个解析器实现为独立的单文件模块。 + +#### Scenario: markitdown 解析器在独立文件 +- **WHEN** 使用 markitdown 解析器 +- **THEN** 从 readers/doc/markitdown.py 导入 + +#### Scenario: pypandoc 解析器在独立文件 +- **WHEN** 使用 pypandoc 解析器 +- **THEN** 从 readers/doc/pypandoc.py 导入 diff --git a/openspec/specs/ppt-reader/spec.md b/openspec/specs/ppt-reader/spec.md new file mode 100644 index 0000000..ad21746 --- /dev/null +++ b/openspec/specs/ppt-reader/spec.md @@ -0,0 +1,38 @@ +## Purpose + +PPT 文档解析能力,支持解析 Microsoft PowerPoint 97-2003 旧格式文档。 + +## Requirements + +### Requirement: PPT 文档解析 +系统 SHALL 支持解析 .ppt 格式文档,按优先级尝试多个解析器。 + +#### Scenario: 按优先级尝试解析器 +- **WHEN** 解析 PPT 文档 +- **THEN** 系统按 markitdown 的顺序尝试 + +#### Scenario: 成功解析 +- **WHEN** 任一解析器成功 +- **THEN** 系统返回解析结果 + +#### Scenario: 所有解析器失败 +- **WHEN** 所有解析器均失败 +- **THEN** 系统返回失败列表并退出非零状态码 + +### Requirement: markitdown 解析器 +系统 SHALL 支持使用 markitdown 库解析 PPT。 + +#### Scenario: markitdown 解析成功 +- **WHEN** markitdown 库可用且文档有效 +- **THEN** 系统返回 Markdown 内容 + +#### Scenario: markitdown 库未安装 +- **WHEN** markitdown 库未安装 +- **THEN** 系统返回失败信息 + +### Requirement: 每个解析器独立文件 +系统 SHALL 将每个解析器实现为独立的单文件模块。 + +#### Scenario: markitdown 解析器在独立文件 +- **WHEN** 使用 markitdown 解析器 +- **THEN** 从 readers/ppt/markitdown.py 导入 diff --git a/openspec/specs/xls-reader/spec.md b/openspec/specs/xls-reader/spec.md new file mode 100644 index 0000000..3fc0155 --- /dev/null +++ b/openspec/specs/xls-reader/spec.md @@ -0,0 +1,68 @@ +## Purpose + +XLS 文档解析能力,支持解析 Microsoft Excel 97-2003 旧格式文档。 + +## Requirements + +### Requirement: XLS 文档解析 +系统 SHALL 支持解析 .xls 格式文档,按优先级尝试多个解析器。 + +#### Scenario: 按优先级尝试解析器 +- **WHEN** 解析 XLS 文档 +- **THEN** 系统按 unstructured → markitdown → pandas+xlrd 的顺序尝试 + +#### Scenario: 成功解析 +- **WHEN** 任一解析器成功 +- **THEN** 系统返回解析结果 + +#### Scenario: 所有解析器失败 +- **WHEN** 所有解析器均失败 +- **THEN** 系统返回失败列表并退出非零状态码 + +### Requirement: unstructured 解析器 +系统 SHALL 支持使用 unstructured 库解析 XLS。 + +#### Scenario: unstructured 解析成功 +- **WHEN** unstructured 库可用且文档有效 +- **THEN** 系统返回 Markdown 内容 + +#### Scenario: unstructured 库未安装 +- **WHEN** unstructured 库未安装 +- **THEN** 系统尝试下一个解析器 + +### Requirement: markitdown 解析器 +系统 SHALL 支持使用 markitdown 库解析 XLS。 + +#### Scenario: markitdown 解析成功 +- **WHEN** markitdown 库可用且文档有效 +- **THEN** 系统返回 Markdown 内容 + +#### Scenario: markitdown 库未安装 +- **WHEN** markitdown 库未安装 +- **THEN** 系统尝试下一个解析器 + +### Requirement: pandas+xlrd 解析器 +系统 SHALL 支持使用 pandas + xlrd 库解析 XLS。 + +#### Scenario: pandas+xlrd 解析成功 +- **WHEN** pandas 和 xlrd 库可用且文档有效 +- **THEN** 系统返回 Markdown 格式的表格内容,包含所有工作表 + +#### Scenario: pandas 或 xlrd 库未安装 +- **WHEN** pandas 或 xlrd 库未安装 +- **THEN** 系统尝试下一个解析器 + +### Requirement: 每个解析器独立文件 +系统 SHALL 将每个解析器实现为独立的单文件模块。 + +#### Scenario: unstructured 解析器在独立文件 +- **WHEN** 使用 unstructured 解析器 +- **THEN** 从 readers/xls/unstructured.py 导入 + +#### Scenario: markitdown 解析器在独立文件 +- **WHEN** 使用 markitdown 解析器 +- **THEN** 从 readers/xls/markitdown.py 导入 + +#### Scenario: pandas 解析器在独立文件 +- **WHEN** 使用 pandas 解析器 +- **THEN** 从 readers/xls/pandas.py 导入 diff --git a/scripts/config.py b/scripts/config.py index be38bfe..cb6e8fa 100644 --- a/scripts/config.py +++ b/scripts/config.py @@ -97,5 +97,37 @@ DEPENDENCIES = { "selenium" ] } + }, + "doc": { + "default": { + "python": None, + "dependencies": [ + "markitdown[docx]", + "pypandoc-binary", + "olefile" + ] + } + }, + "xls": { + "default": { + "python": None, + "dependencies": [ + "unstructured[xlsx]", + "markitdown[xls]", + "pandas", + "tabulate", + "xlrd", + "olefile" + ] + } + }, + "ppt": { + "default": { + "python": None, + "dependencies": [ + "markitdown[pptx]", + "olefile" + ] + } } } diff --git a/scripts/core/advice_generator.py b/scripts/core/advice_generator.py index 966bd16..e7a6e21 100644 --- a/scripts/core/advice_generator.py +++ b/scripts/core/advice_generator.py @@ -12,6 +12,9 @@ from readers import ( XlsxReader, PptxReader, HtmlReader, + DocReader, + XlsReader, + PptReader, ) @@ -22,6 +25,9 @@ _READER_KEY_MAP: Dict[Type[BaseReader], str] = { XlsxReader: "xlsx", PptxReader: "pptx", HtmlReader: "html", + DocReader: "doc", + XlsReader: "xls", + PptReader: "ppt", } diff --git a/scripts/lyxy_document_reader.py b/scripts/lyxy_document_reader.py index 00f7c2c..97c73ba 100644 --- a/scripts/lyxy_document_reader.py +++ b/scripts/lyxy_document_reader.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""文档解析器命令行交互模块,提供命令行接口。支持 DOCX、PPTX、XLSX、PDF、HTML 和 URL。""" +"""文档解析器命令行交互模块,提供命令行接口。支持 DOC、DOCX、XLS、XLSX、PPT、PPTX、PDF、HTML 和 URL。""" import argparse import logging @@ -39,10 +39,10 @@ from readers import READERS def main() -> None: parser = argparse.ArgumentParser( - description="将 DOCX、PPTX、XLSX、PDF、HTML 文件或 URL 解析为 Markdown" + description="将 DOC、DOCX、XLS、XLSX、PPT、PPTX、PDF、HTML 文件或 URL 解析为 Markdown" ) - parser.add_argument("input_path", help="DOCX、PPTX、XLSX、PDF、HTML 文件或 URL") + parser.add_argument("input_path", help="DOC、DOCX、XLS、XLSX、PPT、PPTX、PDF、HTML 文件或 URL") parser.add_argument( "-a", diff --git a/scripts/readers/__init__.py b/scripts/readers/__init__.py index 27d0fed..7d42e8f 100644 --- a/scripts/readers/__init__.py +++ b/scripts/readers/__init__.py @@ -6,6 +6,9 @@ from .xlsx import XlsxReader from .pptx import PptxReader from .pdf import PdfReader from .html import HtmlReader +from .doc import DocReader +from .xls import XlsReader +from .ppt import PptReader READERS = [ DocxReader, @@ -13,6 +16,9 @@ READERS = [ PptxReader, PdfReader, HtmlReader, + DocReader, + XlsReader, + PptReader, ] __all__ = [ @@ -22,5 +28,8 @@ __all__ = [ "PptxReader", "PdfReader", "HtmlReader", + "DocReader", + "XlsReader", + "PptReader", "READERS", ] diff --git a/scripts/readers/doc/__init__.py b/scripts/readers/doc/__init__.py new file mode 100644 index 0000000..09f96f1 --- /dev/null +++ b/scripts/readers/doc/__init__.py @@ -0,0 +1,48 @@ +"""DOC 文件阅读器,支持多种解析方法。""" + +import os +from typing import List, Optional, Tuple + +from readers.base import BaseReader +from utils import is_valid_doc + +from . import markitdown +from . import pypandoc + + +PARSERS = [ + ("MarkItDown", markitdown.parse), + ("pypandoc-binary", pypandoc.parse), +] + + +class DocReader(BaseReader): + """DOC 文件阅读器""" + + def supports(self, file_path: str) -> bool: + return file_path.lower().endswith('.doc') + + def parse(self, file_path: str) -> Tuple[Optional[str], List[str]]: + failures = [] + + # 检查文件是否存在 + if not os.path.exists(file_path): + return None, ["文件不存在"] + + # 验证文件格式 + if not is_valid_doc(file_path): + return None, ["不是有效的 DOC 文件"] + + content = None + + for parser_name, parser_func in PARSERS: + try: + content, error = parser_func(file_path) + if content is not None: + return content, failures + else: + failures.append(f"- {parser_name}: {error}") + except Exception as e: + failures.append(f"- {parser_name}: [意外异常] {type(e).__name__}: {str(e)}") + + return None, failures diff --git a/scripts/readers/doc/markitdown.py b/scripts/readers/doc/markitdown.py new file mode 100644 index 0000000..0e4d2b4 --- /dev/null +++ b/scripts/readers/doc/markitdown.py @@ -0,0 +1,10 @@ +"""使用 MarkItDown 库解析 DOC 文件""" + +from typing import Optional, Tuple + +from readers._utils import parse_via_markitdown + + +def parse(file_path: str) -> Tuple[Optional[str], Optional[str]]: + """使用 MarkItDown 库解析 DOC 文件""" + return parse_via_markitdown(file_path) diff --git a/scripts/readers/doc/pypandoc.py b/scripts/readers/doc/pypandoc.py new file mode 100644 index 0000000..5b111e9 --- /dev/null +++ b/scripts/readers/doc/pypandoc.py @@ -0,0 +1,29 @@ +"""使用 pypandoc-binary 库解析 DOC 文件""" + +from typing import Optional, Tuple + + +def parse(file_path: str) -> Tuple[Optional[str], Optional[str]]: + """使用 pypandoc-binary 库解析 DOC 文件""" + try: + import pypandoc + except ImportError: + return None, "pypandoc-binary 库未安装" + + try: + content = pypandoc.convert_file( + source_file=file_path, + to="md", + format="doc", + outputfile=None, + extra_args=["--wrap=none"], + ) + except OSError as exc: + return None, f"pypandoc-binary 缺少 Pandoc 可执行文件: {exc}" + except RuntimeError as exc: + return None, f"pypandoc-binary 解析失败: {exc}" + + content = content.strip() + if not content: + return None, "文档为空" + return content, None diff --git a/scripts/readers/ppt/__init__.py b/scripts/readers/ppt/__init__.py new file mode 100644 index 0000000..0a4273c --- /dev/null +++ b/scripts/readers/ppt/__init__.py @@ -0,0 +1,46 @@ +"""PPT 文件阅读器,支持多种解析方法。""" + +import os +from typing import List, Optional, Tuple + +from readers.base import BaseReader +from utils import is_valid_ppt + +from . import markitdown + + +PARSERS = [ + ("MarkItDown", markitdown.parse), +] + + +class PptReader(BaseReader): + """PPT 文件阅读器""" + + def supports(self, file_path: str) -> bool: + return file_path.lower().endswith('.ppt') + + def parse(self, file_path: str) -> Tuple[Optional[str], List[str]]: + failures = [] + + # 检查文件是否存在 + if not os.path.exists(file_path): + return None, ["文件不存在"] + + # 验证文件格式 + if not is_valid_ppt(file_path): + return None, ["不是有效的 PPT 文件"] + + content = None + + for parser_name, parser_func in PARSERS: + try: + content, error = parser_func(file_path) + if content is not None: + return content, failures + else: + failures.append(f"- {parser_name}: {error}") + except Exception as e: + failures.append(f"- {parser_name}: [意外异常] {type(e).__name__}: {str(e)}") + + return None, failures diff --git a/scripts/readers/ppt/markitdown.py b/scripts/readers/ppt/markitdown.py new file mode 100644 index 0000000..ed1edb3 --- /dev/null +++ b/scripts/readers/ppt/markitdown.py @@ -0,0 +1,10 @@ +"""使用 MarkItDown 库解析 PPT 文件""" + +from typing import Optional, Tuple + +from readers._utils import parse_via_markitdown + + +def parse(file_path: str) -> Tuple[Optional[str], Optional[str]]: + """使用 MarkItDown 库解析 PPT 文件""" + return parse_via_markitdown(file_path) diff --git a/scripts/readers/xls/__init__.py b/scripts/readers/xls/__init__.py new file mode 100644 index 0000000..2420a90 --- /dev/null +++ b/scripts/readers/xls/__init__.py @@ -0,0 +1,50 @@ +"""XLS 文件阅读器,支持多种解析方法。""" + +import os +from typing import List, Optional, Tuple + +from readers.base import BaseReader +from utils import is_valid_xls + +from . import unstructured +from . import markitdown +from . import pandas + + +PARSERS = [ + ("unstructured", unstructured.parse), + ("MarkItDown", markitdown.parse), + ("pandas+xlrd", pandas.parse), +] + + +class XlsReader(BaseReader): + """XLS 文件阅读器""" + + def supports(self, file_path: str) -> bool: + return file_path.lower().endswith('.xls') + + def parse(self, file_path: str) -> Tuple[Optional[str], List[str]]: + failures = [] + + # 检查文件是否存在 + if not os.path.exists(file_path): + return None, ["文件不存在"] + + # 验证文件格式 + if not is_valid_xls(file_path): + return None, ["不是有效的 XLS 文件"] + + content = None + + for parser_name, parser_func in PARSERS: + try: + content, error = parser_func(file_path) + if content is not None: + return content, failures + else: + failures.append(f"- {parser_name}: {error}") + except Exception as e: + failures.append(f"- {parser_name}: [意外异常] {type(e).__name__}: {str(e)}") + + return None, failures diff --git a/scripts/readers/xls/markitdown.py b/scripts/readers/xls/markitdown.py new file mode 100644 index 0000000..8137b2a --- /dev/null +++ b/scripts/readers/xls/markitdown.py @@ -0,0 +1,10 @@ +"""使用 MarkItDown 库解析 XLS 文件""" + +from typing import Optional, Tuple + +from readers._utils import parse_via_markitdown + + +def parse(file_path: str) -> Tuple[Optional[str], Optional[str]]: + """使用 MarkItDown 库解析 XLS 文件""" + return parse_via_markitdown(file_path) diff --git a/scripts/readers/xls/pandas.py b/scripts/readers/xls/pandas.py new file mode 100644 index 0000000..b1770dd --- /dev/null +++ b/scripts/readers/xls/pandas.py @@ -0,0 +1,41 @@ +"""使用 pandas+xlrd 库解析 XLS 文件""" + +from typing import Optional, Tuple + + +def parse(file_path: str) -> Tuple[Optional[str], Optional[str]]: + """使用 pandas+xlrd 库解析 XLS 文件""" + try: + import pandas as pd + from tabulate import tabulate + except ImportError as e: + if "pandas" in str(e): + missing_lib = "pandas" + elif "xlrd" in str(e): + missing_lib = "xlrd" + else: + missing_lib = "tabulate" + return None, f"{missing_lib} 库未安装" + + try: + sheets = pd.read_excel(file_path, sheet_name=None, engine="xlrd") + + markdown_parts = [] + for sheet_name, df in sheets.items(): + if len(df) == 0: + markdown_parts.append(f"## {sheet_name}\n\n*工作表为空*") + continue + + table_md = tabulate( + df, headers="keys", tablefmt="pipe", showindex=True, missingval="" + ) + markdown_parts.append(f"## {sheet_name}\n\n{table_md}") + + if not markdown_parts: + return None, "Excel 文件为空" + + markdown_content = "# Excel数据转换结果\n\n" + "\n\n".join(markdown_parts) + + return markdown_content, None + except Exception as e: + return None, f"pandas 解析失败: {str(e)}" diff --git a/scripts/readers/xls/unstructured.py b/scripts/readers/xls/unstructured.py new file mode 100644 index 0000000..b997a57 --- /dev/null +++ b/scripts/readers/xls/unstructured.py @@ -0,0 +1,22 @@ +"""使用 unstructured 库解析 XLS 文件""" + +from typing import Optional, Tuple + +from readers._utils import convert_unstructured_to_markdown + + +def parse(file_path: str) -> Tuple[Optional[str], Optional[str]]: + """使用 unstructured 库解析 XLS 文件""" + try: + from unstructured.partition.xlsx import partition_xlsx + except ImportError: + return None, "unstructured 库未安装" + + try: + elements = partition_xlsx(filename=file_path, infer_table_structure=True) + content = convert_unstructured_to_markdown(elements) + if not content.strip(): + return None, "文档为空" + return content, None + except Exception as e: + return None, f"unstructured 解析失败: {str(e)}" diff --git a/scripts/utils/__init__.py b/scripts/utils/__init__.py index fa2738e..64cd1bb 100644 --- a/scripts/utils/__init__.py +++ b/scripts/utils/__init__.py @@ -5,6 +5,9 @@ from .file_detection import ( is_valid_pptx, is_valid_xlsx, is_valid_pdf, + is_valid_doc, + is_valid_xls, + is_valid_ppt, is_html_file, is_url, detect_file_type, @@ -15,6 +18,9 @@ __all__ = [ "is_valid_pptx", "is_valid_xlsx", "is_valid_pdf", + "is_valid_doc", + "is_valid_xls", + "is_valid_ppt", "is_html_file", "is_url", "detect_file_type", diff --git a/scripts/utils/file_detection.py b/scripts/utils/file_detection.py index d6d0c82..2bcffb4 100644 --- a/scripts/utils/file_detection.py +++ b/scripts/utils/file_detection.py @@ -5,6 +5,19 @@ import zipfile from typing import List, Optional +def _is_valid_ole(file_path: str) -> bool: + """验证 OLE2 格式文件(DOC/XLS/PPT)""" + try: + import olefile + except ImportError: + # 如果 olefile 未安装,就不做严格验证 + return True + try: + return olefile.isOleFile(file_path) + except Exception: + return False + + def _is_valid_ooxml(file_path: str, required_files: List[str]) -> bool: """验证 OOXML 格式文件(DOCX/PPTX/XLSX)""" try: @@ -35,6 +48,21 @@ def is_valid_xlsx(file_path: str) -> bool: return _is_valid_ooxml(file_path, _XLSX_REQUIRED) +def is_valid_doc(file_path: str) -> bool: + """验证文件是否为有效的 DOC 格式""" + return _is_valid_ole(file_path) + + +def is_valid_xls(file_path: str) -> bool: + """验证文件是否为有效的 XLS 格式""" + return _is_valid_ole(file_path) + + +def is_valid_ppt(file_path: str) -> bool: + """验证文件是否为有效的 PPT 格式""" + return _is_valid_ole(file_path) + + def is_valid_pdf(file_path: str) -> bool: """验证文件是否为有效的 PDF 格式""" try: @@ -61,13 +89,18 @@ _FILE_TYPE_VALIDATORS = { ".pptx": is_valid_pptx, ".xlsx": is_valid_xlsx, ".pdf": is_valid_pdf, + ".doc": is_valid_doc, + ".xls": is_valid_xls, + ".ppt": is_valid_ppt, } def detect_file_type(file_path: str) -> Optional[str]: - """检测文件类型,返回 'docx'、'pptx'、'xlsx' 或 'pdf'""" + """检测文件类型,返回 'docx'、'pptx'、'xlsx'、'pdf'、'doc'、'xls' 或 'ppt'""" ext = os.path.splitext(file_path)[1].lower() validator = _FILE_TYPE_VALIDATORS.get(ext) if validator and validator(file_path): return ext.lstrip(".") return None + + diff --git a/tests/test_cli/test_main.py b/tests/test_cli/test_main.py index 1b55d1e..a6872da 100644 --- a/tests/test_cli/test_main.py +++ b/tests/test_cli/test_main.py @@ -38,6 +38,27 @@ class TestCLIAdviceOption: output = stdout + stderr assert "无法识别" in output or "错误" in output + def test_advice_option_doc(self, cli_runner): + """测试 --advice 选项对 DOC 文件。""" + stdout, stderr, exit_code = cli_runner(["test.doc", "--advice"]) + + assert exit_code == 0 + assert "文件类型: DOC" in stdout + + def test_advice_option_xls(self, cli_runner): + """测试 --advice 选项对 XLS 文件。""" + stdout, stderr, exit_code = cli_runner(["test.xls", "--advice"]) + + assert exit_code == 0 + assert "文件类型: XLS" in stdout + + def test_advice_option_ppt(self, cli_runner): + """测试 --advice 选项对 PPT 文件。""" + stdout, stderr, exit_code = cli_runner(["test.ppt", "--advice"]) + + assert exit_code == 0 + assert "文件类型: PPT" in stdout + class TestCLIDefaultOutput: """测试 CLI 默认输出功能。""" diff --git a/tests/test_readers/test_doc/__init__.py b/tests/test_readers/test_doc/__init__.py new file mode 100644 index 0000000..8f2660b --- /dev/null +++ b/tests/test_readers/test_doc/__init__.py @@ -0,0 +1 @@ +"""测试 DocReader 模块。""" diff --git a/tests/test_readers/test_doc/test_consistency.py b/tests/test_readers/test_doc/test_consistency.py new file mode 100644 index 0000000..04c714f --- /dev/null +++ b/tests/test_readers/test_doc/test_consistency.py @@ -0,0 +1,21 @@ +"""测试所有 DOC Readers 的一致性。""" + +import pytest +from readers.doc import markitdown, pypandoc + + +class TestDocReadersConsistency: + """验证 DOC Readers 模块结构正确。""" + + def test_parsers_importable(self): + """测试所有 parser 模块可以正确导入。""" + # 验证模块导入成功 + assert markitdown is not None + assert pypandoc is not None + assert hasattr(markitdown, 'parse') + assert hasattr(pypandoc, 'parse') + + def test_parser_functions_callable(self): + """测试 parse 函数是可调用的。""" + assert callable(markitdown.parse) + assert callable(pypandoc.parse) diff --git a/tests/test_readers/test_doc/test_markitdown_doc.py b/tests/test_readers/test_doc/test_markitdown_doc.py new file mode 100644 index 0000000..765c7bc --- /dev/null +++ b/tests/test_readers/test_doc/test_markitdown_doc.py @@ -0,0 +1,25 @@ +"""测试 MarkItDown DOC Reader 的解析功能。""" + +import pytest +import os +from readers.doc import markitdown + + +class TestMarkitdownDocReaderParse: + """测试 MarkItDown DOC Reader 的 parse 方法。""" + + def test_module_importable(self): + """测试模块可以正确导入。""" + assert markitdown is not None + assert hasattr(markitdown, 'parse') + assert callable(markitdown.parse) + + def test_file_not_exists(self, tmp_path): + """测试文件不存在的情况。""" + non_existent_file = str(tmp_path / "non_existent.doc") + + content, error = markitdown.parse(non_existent_file) + + # 验证返回 None 和错误信息 + assert content is None + assert error is not None diff --git a/tests/test_readers/test_doc/test_pypandoc_doc.py b/tests/test_readers/test_doc/test_pypandoc_doc.py new file mode 100644 index 0000000..2f0044e --- /dev/null +++ b/tests/test_readers/test_doc/test_pypandoc_doc.py @@ -0,0 +1,25 @@ +"""测试 pypandoc DOC Reader 的解析功能。""" + +import pytest +import os +from readers.doc import pypandoc + + +class TestPypandocDocReaderParse: + """测试 pypandoc DOC Reader 的 parse 方法。""" + + def test_module_importable(self): + """测试模块可以正确导入。""" + assert pypandoc is not None + assert hasattr(pypandoc, 'parse') + assert callable(pypandoc.parse) + + def test_file_not_exists(self, tmp_path): + """测试文件不存在的情况。""" + non_existent_file = str(tmp_path / "non_existent.doc") + + content, error = pypandoc.parse(non_existent_file) + + # 验证返回 None 和错误信息 + assert content is None + assert error is not None diff --git a/tests/test_readers/test_ppt/__init__.py b/tests/test_readers/test_ppt/__init__.py new file mode 100644 index 0000000..bb435a6 --- /dev/null +++ b/tests/test_readers/test_ppt/__init__.py @@ -0,0 +1 @@ +"""测试 PptReader 模块。""" diff --git a/tests/test_readers/test_ppt/test_consistency.py b/tests/test_readers/test_ppt/test_consistency.py new file mode 100644 index 0000000..78df0ab --- /dev/null +++ b/tests/test_readers/test_ppt/test_consistency.py @@ -0,0 +1,18 @@ +"""测试所有 PPT Readers 的一致性。""" + +import pytest +from readers.ppt import markitdown + + +class TestPptReadersConsistency: + """验证 PPT Readers 模块结构正确。""" + + def test_parsers_importable(self): + """测试所有 parser 模块可以正确导入。""" + # 验证模块导入成功 + assert markitdown is not None + assert hasattr(markitdown, 'parse') + + def test_parser_functions_callable(self): + """测试 parse 函数是可调用的。""" + assert callable(markitdown.parse) diff --git a/tests/test_readers/test_ppt/test_markitdown_ppt.py b/tests/test_readers/test_ppt/test_markitdown_ppt.py new file mode 100644 index 0000000..7c59240 --- /dev/null +++ b/tests/test_readers/test_ppt/test_markitdown_ppt.py @@ -0,0 +1,25 @@ +"""测试 MarkItDown PPT Reader 的解析功能。""" + +import pytest +import os +from readers.ppt import markitdown + + +class TestMarkitdownPptReaderParse: + """测试 MarkItDown PPT Reader 的 parse 方法。""" + + def test_module_importable(self): + """测试模块可以正确导入。""" + assert markitdown is not None + assert hasattr(markitdown, 'parse') + assert callable(markitdown.parse) + + def test_file_not_exists(self, tmp_path): + """测试文件不存在的情况。""" + non_existent_file = str(tmp_path / "non_existent.ppt") + + content, error = markitdown.parse(non_existent_file) + + # 验证返回 None 和错误信息 + assert content is None + assert error is not None diff --git a/tests/test_readers/test_xls/__init__.py b/tests/test_readers/test_xls/__init__.py new file mode 100644 index 0000000..dbad65d --- /dev/null +++ b/tests/test_readers/test_xls/__init__.py @@ -0,0 +1 @@ +"""测试 XlsReader 模块。""" diff --git a/tests/test_readers/test_xls/test_consistency.py b/tests/test_readers/test_xls/test_consistency.py new file mode 100644 index 0000000..f6336bd --- /dev/null +++ b/tests/test_readers/test_xls/test_consistency.py @@ -0,0 +1,24 @@ +"""测试所有 XLS Readers 的一致性。""" + +import pytest +from readers.xls import unstructured, markitdown, pandas + + +class TestXlsReadersConsistency: + """验证 XLS Readers 模块结构正确。""" + + def test_parsers_importable(self): + """测试所有 parser 模块可以正确导入。""" + # 验证模块导入成功 + assert unstructured is not None + assert markitdown is not None + assert pandas is not None + assert hasattr(unstructured, 'parse') + assert hasattr(markitdown, 'parse') + assert hasattr(pandas, 'parse') + + def test_parser_functions_callable(self): + """测试 parse 函数是可调用的。""" + assert callable(unstructured.parse) + assert callable(markitdown.parse) + assert callable(pandas.parse) diff --git a/tests/test_readers/test_xls/test_markitdown_xls.py b/tests/test_readers/test_xls/test_markitdown_xls.py new file mode 100644 index 0000000..e7a5ff1 --- /dev/null +++ b/tests/test_readers/test_xls/test_markitdown_xls.py @@ -0,0 +1,25 @@ +"""测试 MarkItDown XLS Reader 的解析功能。""" + +import pytest +import os +from readers.xls import markitdown + + +class TestMarkitdownXlsReaderParse: + """测试 MarkItDown XLS Reader 的 parse 方法。""" + + def test_module_importable(self): + """测试模块可以正确导入。""" + assert markitdown is not None + assert hasattr(markitdown, 'parse') + assert callable(markitdown.parse) + + def test_file_not_exists(self, tmp_path): + """测试文件不存在的情况。""" + non_existent_file = str(tmp_path / "non_existent.xls") + + content, error = markitdown.parse(non_existent_file) + + # 验证返回 None 和错误信息 + assert content is None + assert error is not None diff --git a/tests/test_readers/test_xls/test_pandas_xls.py b/tests/test_readers/test_xls/test_pandas_xls.py new file mode 100644 index 0000000..36d29df --- /dev/null +++ b/tests/test_readers/test_xls/test_pandas_xls.py @@ -0,0 +1,25 @@ +"""测试 pandas XLS Reader 的解析功能。""" + +import pytest +import os +from readers.xls import pandas + + +class TestPandasXlsReaderParse: + """测试 pandas XLS Reader 的 parse 方法。""" + + def test_module_importable(self): + """测试模块可以正确导入。""" + assert pandas is not None + assert hasattr(pandas, 'parse') + assert callable(pandas.parse) + + def test_file_not_exists(self, tmp_path): + """测试文件不存在的情况。""" + non_existent_file = str(tmp_path / "non_existent.xls") + + content, error = pandas.parse(non_existent_file) + + # 验证返回 None 和错误信息 + assert content is None + assert error is not None diff --git a/tests/test_readers/test_xls/test_unstructured_xls.py b/tests/test_readers/test_xls/test_unstructured_xls.py new file mode 100644 index 0000000..a18b0c8 --- /dev/null +++ b/tests/test_readers/test_xls/test_unstructured_xls.py @@ -0,0 +1,25 @@ +"""测试 unstructured XLS Reader 的解析功能。""" + +import pytest +import os +from readers.xls import unstructured + + +class TestUnstructuredXlsReaderParse: + """测试 unstructured XLS Reader 的 parse 方法。""" + + def test_module_importable(self): + """测试模块可以正确导入。""" + assert unstructured is not None + assert hasattr(unstructured, 'parse') + assert callable(unstructured.parse) + + def test_file_not_exists(self, tmp_path): + """测试文件不存在的情况。""" + non_existent_file = str(tmp_path / "non_existent.xls") + + content, error = unstructured.parse(non_existent_file) + + # 验证返回 None 和错误信息 + assert content is None + assert error is not None