Metadata-Version: 2.4
Name: power-doc
Version: 0.1.0
Summary: 多格式文档智能文本提取工具
Author-email: Chandler <275737875@qq.com>
License-Expression: MIT
Keywords: ocr,pdf,docx,doc,html,text-extraction
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: typer>=0.9.0
Requires-Dist: rich>=13.0.0
Provides-Extra: all
Requires-Dist: rapidocr>=3.0.0; extra == "all"
Requires-Dist: onnxruntime>=1.15.0; extra == "all"
Requires-Dist: pymupdf4llm>=0.0.25; extra == "all"
Requires-Dist: docx2txt>=0.8; extra == "all"
Requires-Dist: doc2txt>=0.4; extra == "all"
Requires-Dist: trafilatura>=1.6.0; extra == "all"
Requires-Dist: beautifulsoup4>=4.12.0; extra == "all"
Requires-Dist: lxml>=4.9.0; extra == "all"
Provides-Extra: ocr
Requires-Dist: rapidocr>=3.0.0; extra == "ocr"
Requires-Dist: onnxruntime>=1.15.0; extra == "ocr"
Provides-Extra: pdf
Requires-Dist: pymupdf4llm>=0.0.25; extra == "pdf"
Provides-Extra: doc
Requires-Dist: docx2txt>=0.8; extra == "doc"
Requires-Dist: doc2txt>=0.4; extra == "doc"
Provides-Extra: html
Requires-Dist: trafilatura>=1.6.0; extra == "html"
Requires-Dist: beautifulsoup4>=4.12.0; extra == "html"
Requires-Dist: lxml>=4.9.0; extra == "html"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: rapidocr>=3.0.0; extra == "dev"
Requires-Dist: onnxruntime>=1.15.0; extra == "dev"
Requires-Dist: pymupdf4llm>=0.0.25; extra == "dev"
Requires-Dist: docx2txt>=0.8; extra == "dev"
Requires-Dist: doc2txt>=0.4; extra == "dev"
Requires-Dist: trafilatura>=1.6.0; extra == "dev"
Requires-Dist: beautifulsoup4>=4.12.0; extra == "dev"
Requires-Dist: lxml>=4.9.0; extra == "dev"

# Power-Doc

**多格式文档智能文本提取工具**

Power-Doc 是一个 Python 工具包与命令行工具，专注于从多种常见文档格式（图片、PDF、Word、HTML）中提取纯文本内容。它采用模块化架构设计，每个格式对应独立的提取引擎，支持按需安装依赖，并针对每种格式选择了业界最优的底层库，实现高精度的内容提取。

---

## 目录

- [功能特性](#功能特性)
- [快速开始](#快速开始)
  - [安装](#安装)
  - [命令行使用](#命令行使用)
  - [Python API 使用](#python-api-使用)
- [项目架构](#项目架构)
  - [目录结构](#目录结构)
  - [核心模块详解](#核心模块详解)
  - [数据流向](#数据流向)
- [依赖与可选功能](#依赖与可选功能)
- [开发与测试](#开发与测试)
- [扩展指南](#扩展指南)
  - [新增提取器](#新增提取器)
  - [CLI 命令注册](#cli-命令注册)
- [许可证](#许可证)

---

## 功能特性

| 格式 | 扩展名 | 底层引擎 | 特点 |
|------|--------|----------|------|
| 图片 OCR | `.png`, `.jpg`, `.jpeg`, `.bmp`, `.tiff` | RapidOCR | 轻量级 ONNX 推理，支持中英混合识别 |
| PDF | `.pdf` | pymupdf4llm + PyMuPDF | 保留 Markdown 格式，支持按页提取 |
| Word 2007+ | `.docx` | docx2txt | 直接读取 XML 内容，无格式残留 |
| Word 97-2003 | `.doc` | doc2txt | 兼容旧版二进制格式 |
| HTML | `.html`, `.htm` | trafilatura + BeautifulSoup | 智能正文提取 + 降级去噪方案 |

- **模块化懒加载**：未安装的依赖不会阻塞其他功能
- **混合降级策略**：HTML 模块优先用 trafilatura，失败时自动 fallback 到 BeautifulSoup
- **链接保留**：HTML 提取支持将超链接保留为 `[文字](URL)` 格式
- **统一错误处理**：所有模块使用一致的 `FileNotFoundError` 与 `ImportError` 语义

---

## 快速开始

### 安装

基础安装（仅 CLI 框架，无提取能力）：

```bash
pip install power-doc
```

按需安装特定功能：

```bash
# 图片 OCR
pip install "power-doc[ocr]"

# PDF 提取
pip install "power-doc[pdf]"

# Word 文档
pip install "power-doc[doc]"

# HTML 解析
pip install "power-doc[html]"
```

一次性安装全部功能（开发推荐）：

```bash
pip install "power-doc[all]"
```

### 命令行使用

安装后获得 `power-doc` 命令：

```bash
# 查看帮助
power-doc --help

# 图片 OCR 转文本
power-doc image2txt ./photo.png -o output.txt

# PDF 转文本
power-doc pdf2txt ./document.pdf -v

# DOCX 转文本
power-doc docx2txt ./report.docx -o report.txt

# 旧版 DOC 转文本
power-doc doc2txt ./legacy.doc -o legacy.txt

# HTML 智能提取（保留链接）
power-doc html2txt ./page.html -o page.txt --verbose

# HTML 提取（不保留链接）
power-doc html2txt ./page.html --no-links
```

### Python API 使用

```python
from power_doc import ImageOCR, PDFExtractor, DocxExtractor, DocExtractor, HtmlExtractor

# 图片 OCR
ocr = ImageOCR()
text = ocr.extract_text("./image.png")
results = ocr.extract_text_with_confidence("./image.png")  # [(text, confidence), ...]

# PDF 提取
text = PDFExtractor.extract_text("./document.pdf")
pages = PDFExtractor.extract_text_with_pages("./document.pdf")  # 按页列表

# DOCX / DOC
text = DocxExtractor.extract_text("./report.docx")
text = DocExtractor.extract_text("./legacy.doc")

# HTML 提取
text = HtmlExtractor.extract_text_from_file("./page.html", fallback=True, include_links=True)
text = HtmlExtractor.extract_text_from_string("<html>...</html>", include_links=False)
```

### 完整使用示例

#### 场景一：批量图片 OCR 并过滤低置信度结果

```python
from pathlib import Path
from power_doc import ImageOCR

ocr = ImageOCR()
image_dir = Path("./scanned_documents")

for img_path in image_dir.glob("*.png"):
    results = ocr.extract_text_with_confidence(img_path)
    # 过滤置信度低于 0.85 的识别结果
    high_confidence_lines = [
        text for text, conf in results if conf >= 0.85
    ]
    print(f"[{img_path.name}] 高置信度文本 ({len(high_confidence_lines)} 行):")
    print("\n".join(high_confidence_lines))
    print("-" * 40)
```

#### 场景二：PDF 按页提取并生成页码索引

```python
from pathlib import Path
from power_doc import PDFExtractor

pdf_path = Path("./research_paper.pdf")
pages = PDFExtractor.extract_text_with_pages(pdf_path)

# 生成带页码的索引文件
with open("paper_index.txt", "w", encoding="utf-8") as f:
    for page_num, page_text in enumerate(pages, start=1):
        word_count = len(page_text.split())
        f.write(f"\n=== 第 {page_num} 页 (约 {word_count} 词) ===\n")
        f.write(page_text[:500])  # 只写入前 500 字符作为预览
        f.write("\n")

print(f"共提取 {len(pages)} 页，索引已保存到 paper_index.txt")
```

#### 场景三：HTML 网页正文提取并保留超链接

```python
from pathlib import Path
from power_doc import HtmlExtractor

html_path = Path("./saved_page.html")

# 启用 fallback 和链接保留
result = HtmlExtractor.robust_html_to_txt(
    html_path.read_text(encoding="utf-8"),
    fallback=True,
    include_links=True,
)

text = result["text"]
links = result["links"]      # [(text, url), ...]
method = result["method"]    # "trafilatura" 或 "beautifulsoup"

print(f"提取方法: {method}")
print(f"正文长度: {len(text)} 字符")
print(f"发现链接: {len(links)} 个")
for link_text, url in links[:5]:
    print(f"  - [{link_text}]({url})")

# 保存纯文本
Path("article.txt").write_text(text, encoding="utf-8")
```

#### 场景四：批量转换 Word 文档到文本

```python
from pathlib import Path
from power_doc import DocxExtractor, DocExtractor

source_dir = Path("./word_files")
output_dir = Path("./text_output")
output_dir.mkdir(exist_ok=True)

for docx_path in source_dir.glob("*.docx"):
    text = DocxExtractor.extract_text(docx_path)
    out_path = output_dir / f"{docx_path.stem}.txt"
    out_path.write_text(text, encoding="utf-8")
    print(f"[DOCX] {docx_path.name} -> {out_path.name}")

for doc_path in source_dir.glob("*.doc"):
    text = DocExtractor.extract_text(doc_path)
    out_path = output_dir / f"{doc_path.stem}.txt"
    out_path.write_text(text, encoding="utf-8")
    print(f"[DOC]  {doc_path.name} -> {out_path.name}")
```

#### 场景五：通用文档提取器（根据扩展名自动分发）

```python
from pathlib import Path
from power_doc import (
    ImageOCR, PDFExtractor, DocxExtractor,
    DocExtractor, HtmlExtractor,
)

class UniversalExtractor:
    """根据文件扩展名自动选择对应提取器的通用封装."""

    _ocr = None

    @classmethod
    def _get_ocr(cls):
        if cls._ocr is None:
            cls._ocr = ImageOCR()
        return cls._ocr

    @classmethod
    def extract(cls, file_path: str | Path) -> str:
        path = Path(file_path)
        if not path.exists():
            raise FileNotFoundError(f"文件不存在: {path}")

        ext = path.suffix.lower()

        if ext in {".png", ".jpg", ".jpeg", ".bmp", ".tiff"}:
            return cls._get_ocr().extract_text(path)
        elif ext == ".pdf":
            return PDFExtractor.extract_text(path)
        elif ext == ".docx":
            return DocxExtractor.extract_text(path)
        elif ext == ".doc":
            return DocExtractor.extract_text(path)
        elif ext in {".html", ".htm"}:
            return HtmlExtractor.extract_text_from_file(path)
        else:
            raise ValueError(f"不支持的文件格式: {ext}")


# 使用示例
if __name__ == "__main__":
    files = ["report.pdf", "scan.jpg", "notes.docx", "article.html"]
    for f in files:
        try:
            text = UniversalExtractor.extract(f)
            print(f"[{f}] 提取成功，共 {len(text)} 字符")
        except Exception as e:
            print(f"[{f}] 提取失败: {e}")
```

---

## 项目架构

### 目录结构

```
power_doc/
├── __init__.py              # 包入口：导出所有提取器类与版本号
├── cli.py                   # Typer 命令行接口：5 个子命令 + 统一错误处理
└── core/                    # 核心提取引擎（每个格式独立模块）
    ├── __init__.py          # core 子包入口
    ├── image_ocr.py         # 图片 OCR（RapidOCR）
    ├── pdf_extractor.py     # PDF 提取（pymupdf4llm / fitz）
    ├── docx_extractor.py    # DOCX 提取（docx2txt）
    ├── doc_extractor.py     # DOC 提取（doc2txt）
    └── html_extractor.py    # HTML 提取（trafilatura + BeautifulSoup 混合方案）

tests/                       # pytest 测试套件（覆盖率 > 85%）
    ├── test_cli.py
    ├── test_image_ocr.py
    ├── test_pdf_extractor.py
    ├── test_docx_extractor.py
    ├── test_doc_extractor.py
    └── test_html_extractor.py

pyproject.toml               # 项目配置、依赖、构建脚本、pytest-cov 配置
```

### 核心模块详解

#### `image_ocr.py` — 图片 OCR 引擎

- **底层库**：RapidOCR（基于 ONNX Runtime）
- **设计**：实例化时加载模型，支持 `extract_text`（纯文本）与 `extract_text_with_confidence`（带置信度）
- **懒加载**：模块顶部 `try/except` 导入，未安装时在 `__init__` 抛出明确 `ImportError`

#### `pdf_extractor.py` — PDF 提取引擎

- **底层库**：`pymupdf4llm`（全文转 Markdown）、`fitz`（PyMuPDF，按页提取）
- **设计**：全静态方法类，无需实例化
- **双模式**：
  - `extract_text`：返回完整 Markdown 格式文本
  - `extract_text_with_pages`：返回 `List[str]`，每页一个元素

#### `docx_extractor.py` / `doc_extractor.py` — Word 文档引擎

- **底层库**：`docx2txt`（DOCX）、`doc2txt`（DOC）
- **设计**：极简静态方法，直接调用第三方库的 `process()` 函数
- **兼容性**：DOCX 对应 Word 2007+（Office Open XML），DOC 对应 Word 97-2003（二进制）

#### `html_extractor.py` — HTML 智能提取引擎

- **底层库**：`trafilatura`（正文提取）+ `BeautifulSoup`（降级去噪）
- **混合策略**：
  1. 先用 `trafilatura.extract()` 提取正文；结果长度超过 100 字符则直接采用
  2. 若结果过短或失败，fallback 到 BeautifulSoup：移除 `script/style/nav/footer/header/aside/noscript` 黑名单标签，按 class 关键词过滤广告容器
  3. 完全失败时返回空字符串
- **链接处理**：`include_links=True` 时将 `<a>` 标签替换为 `[文字](URL)`，并收集到返回字典的 `links` 字段
- **空白规范化**：内部 `_normalize_whitespace` 方法通过多步正则压缩多余空格与换行

### 数据流向

```
用户输入（文件路径）
    │
    ▼
┌─────────────────┐     ┌─────────────────────────────┐
│   CLI (Typer)   │────▶│  命令分发到对应提取器模块   │
│   参数校验       │     │  image_ocr / pdf_extractor  │
│   -o / -v 处理   │     │  docx_extractor / ...       │
└─────────────────┘     └─────────────────────────────┘
    │                              │
    │                              ▼
    │                    ┌─────────────────┐
    │                    │  核心提取引擎    │
    │                    │  懒加载依赖检查  │
    │                    │  调用第三方库    │
    │                    └─────────────────┘
    │                              │
    ▼                              ▼
┌─────────────────┐     ┌─────────────────┐
│  结果输出到终端  │◀────│   纯文本结果     │
│  或写入文件      │     │  （可选链接列表）│
└─────────────────┘     └─────────────────┘
```

---

## 依赖与可选功能

| 功能组 | 依赖 | extras 标识 |
|--------|------|-------------|
| 图片 OCR | `rapidocr`, `onnxruntime` | `[ocr]` |
| PDF | `pymupdf4llm` | `[pdf]` |
| Word | `docx2txt`, `doc2txt` | `[doc]` |
| HTML | `trafilatura`, `beautifulsoup4`, `lxml` | `[html]` |
| 开发 | `pytest`, `pytest-cov` | `[dev]` |

**基础依赖**（必装）：`typer`, `rich`

---

## 开发与测试

```bash
# 安装开发依赖
pip install -e ".[dev]"

# 运行测试
pytest

# 运行测试并查看覆盖率报告
pytest --cov=power_doc --cov-report=term-missing

# 构建并发布
python -m build
twine check dist/*
twine upload dist/*
```

---

## 扩展指南

### 新增提取器

以新增 Markdown 提取器为例：

1. **新建模块**：在 `power_doc/core/` 下创建 `markdown_extractor.py`
2. **编写类**：实现静态方法 `extract_text(md_path)`，遵循懒加载模式：

```python
try:
    import markdown  # 或你的底层库
except ImportError:
    markdown = None  # type: ignore

class MarkdownExtractor:
    @staticmethod
    def extract_text(md_path):
        if markdown is None:
            raise ImportError("markdown 未安装...")
        # 提取逻辑...
```

3. **导出类**：在 `power_doc/core/__init__.py` 与 `power_doc/__init__.py` 中导入并加入 `__all__`
4. **新增测试**：在 `tests/` 下创建 `test_markdown_extractor.py`

### CLI 命令注册

在 `power_doc/cli.py` 中新增命令：

```python
@app.command()
def md2txt(
    md_path: Path = typer.Argument(..., help="输入 Markdown 路径", exists=True),
    output: Optional[Path] = typer.Option(None, "--output", "-o", help="保存结果到文件"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="显示处理详情"),
) -> None:
    """Markdown 转纯文本."""
    try:
        if verbose:
            console.print(f"[blue]正在提取 Markdown: {md_path}[/blue]")
        text = MarkdownExtractor.extract_text(md_path)
        _print_result(text, output, verbose)
    except Exception as exc:
        _handle_error(str(exc))
```

> 无需额外注册，`@app.command()` 装饰器会自动将命令注册到 Typer 应用。

---

## 许可证

MIT License

---

## 免责声明

1. **按原样提供**：本软件按「原样」（AS IS）提供，不附带任何明示或暗示的担保，包括但不限于对适销性、特定用途适用性及非侵权性的担保。

2. **责任限制**：在任何情况下，作者或版权持有人均不对因使用本软件或无法使用本软件而导致的任何直接、间接、附带、特殊、惩戒性或后果性损害（包括但不限于数据丢失、业务中断或利润损失）承担责任，无论该等损害基于合同、侵权（包括过失）或其他任何责任理论，即使已被告知存在该等损害的可能性。

3. **用户责任**：用户在使用本软件前应自行评估相关风险，并确保其使用行为符合所在地的法律法规。对于因用户违反当地法律或不当使用本软件而产生的任何后果，由用户自行承担全部责任。
