Metadata-Version: 2.4
Name: textag
Version: 1.0.0
Summary: 文本标记解析工具 - 支持多状态合并与多格式导出
Author: textag team
Author-email: Chandler Song <songqiang2014@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/textag/textag
Project-URL: Documentation, https://github.com/textag/textag#readme
Project-URL: Repository, https://github.com/textag/textag.git
Project-URL: Issues, https://github.com/textag/textag/issues
Keywords: text,tag,parser,export,contacts
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
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: Topic :: Text Processing
Classifier: Topic :: Utilities
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pandas>=1.5.0
Requires-Dist: openpyxl>=3.0.0
Provides-Extra: rich
Requires-Dist: rich>=12.0.0; extra == "rich"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: black>=22.0.0; extra == "dev"
Requires-Dist: flake8>=5.0.0; extra == "dev"
Requires-Dist: mypy>=0.990; extra == "dev"
Dynamic: author
Dynamic: license-file
Dynamic: requires-python

# textag - 文本标记解析工具

<div align="center">

![Python Version](https://img.shields.io/pypi/pyversions/textag)
![PyPI version](https://img.shields.io/pypi/v/textag)
![License](https://img.shields.io/pypi/l/textag)

**基于 SOLID 原则设计的文本标记/标注处理工具**

支持多状态合并 · 多格式导出 · 可扩展架构

</div>

---

## 📖 目录

- [功能特性](#-功能特性)
- [安装说明](#-安装说明)
- [快速开始](#-快速开始)
- [使用指南](#-使用指南)
- [API 文档](#-api-文档)
- [架构设计](#-架构设计)
- [扩展开发](#-扩展开发)
- [测试与开发](#-测试与开发)
- [常见问题](#-常见问题)

---

## ✨ 功能特性

### 核心功能

- **文本标记解析**：解析带标签的文本文件，自动识别状态行和 URL 行
- **多状态合并**：同一 URL 在不同状态下自动合并，保留所有状态信息
- **多格式导出**：支持 Excel (.xlsx)、CSV (.json)、JSON (.json) 格式
- **智能去重**：自动检测重复 URL 并生成统计报告

### 技术特性

- ✅ **SOLID 原则**：严格遵循面向对象设计原则
- ✅ **模块化架构**：清晰的分层设计，易于理解和维护
- ✅ **依赖倒置**：高层模块不依赖低层模块的具体实现
- ✅ **开闭原则**：新增功能无需修改现有代码
- ✅ **类型安全**：完整的类型注解，支持 IDE 智能提示
- ✅ **优雅降级**：Rich 美化日志不可用时自动降级到标准日志

---

## 📦 安装说明

### 基础安装

```bash
# 从 PyPI 安装（推荐）
pip install textag

# 从源码安装
git clone https://github.com/textag/textag.git
cd textag
pip install -e .
```

### 可选依赖

```bash
# 完整功能版（包含 Rich 美化日志）
pip install textag[rich]

# 开发版（包含测试和开发工具）
pip install textag[dev]
```

### 环境要求

- Python 3.8+
- pandas >= 1.5.0
- openpyxl >= 3.0.0
- rich >= 12.0.0 (可选，用于美化输出)

---

## 🚀 快速开始

### 1. 准备输入文件

创建 `contacts.txt` 文件：

```text
#contacted
https://linkedin.com/in/user1
https://linkedin.com/in/user2

#replied
https://linkedin.com/in/user1

#followup urgent
https://linkedin.com/in/user3
```

### 2. 命令行使用

```bash
# 导出为 Excel（默认）
textag -i contacts.txt -o output.xlsx

# 导出为 CSV
textag -i contacts.txt -o output.csv

# 导出为 JSON
textag -i contacts.txt -o output.json

# 使用标准日志（无 Rich 依赖）
textag -i contacts.txt -o output.xlsx --no-rich

# 查看帮助
textag --help
```

### 3. 编程式使用

```python
from pathlib import Path
from textag import ContactService, RichLogger

# 创建服务
service = ContactService(logger=RichLogger())

# 执行处理
result = service.process(
    input_file=Path("contacts.txt"),
    output_file=Path("output.xlsx")
)

# 查看结果
print(f"唯一联系人：{result.unique_count}")
print(f"重复 URL: {result.duplicate_count}")
print(f"警告信息：{len(result.warnings)}")
```

---

## 📚 使用指南

### 输入文件格式规范

#### 基本结构

```text
#状态标签
URL 地址
URL 地址

#另一个状态
URL 地址
```

#### 详细说明

1. **状态行**：以 `#` 开头，可包含多个标签
   ```text
   #contacted
   #replied
   #followup urgent  # 多个标签用空格分隔
   ```

2. **URL 行**：必须以 `http://` 或 `https://` 开头
   ```text
   https://linkedin.com/in/user1
   http://example.com/contact
   ```

3. **注释处理**：URL 后的 `#` 注释会被自动移除
   ```text
   https://linkedin.com/in/user1  # 这是注释，会被移除
   ```

4. **空行**：会被自动忽略

### 输出格式示例

#### Excel 输出 (.xlsx)

| URL | Status | Status_Count |
|-----|--------|--------------|
| https://linkedin.com/in/user1 | contacted, replied | 2 |
| https://linkedin.com/in/user2 | contacted | 1 |
| https://linkedin.com/in/user3 | followup urgent | 1 |

#### CSV 输出 (.csv)

```csv
URL,Status,Status_Count
https://linkedin.com/in/user1,"contacted, replied",2
https://linkedin.com/in/user2,contacted,1
https://linkedin.com/in/user3,"followup urgent",1
```

#### JSON 输出 (.json)

```json
{
  "contacts": [
    {
      "URL": "https://linkedin.com/in/user1",
      "Status": "contacted, replied",
      "Status_Count": "2"
    }
  ],
  "meta": {
    "count": 3,
    "version": "1.0"
  }
}
```

---

## 🔧 API 文档

### 核心类

#### ContactService

服务外观类，协调解析和导出流程。

```python
from textag import ContactService, RichLogger
from pathlib import Path

service = ContactService(logger=RichLogger())
result = service.process(
    input_file=Path("input.txt"),
    output_file=Path("output.xlsx")
)
```

**方法：**
- `process(input_file, output_file)` → `ParseResult`: 执行完整处理流程
- `register_exporter(extension, exporter_class)`: 注册新的导出器
- `get_supported_extensions()` → `List[str]`: 获取支持的扩展名列表

#### ContactParser

解析器类，负责解析文本文件。

```python
from textag import ContactParser, StandardLogger
from pathlib import Path

parser = ContactParser(logger=StandardLogger())
result = parser.parse(Path("contacts.txt"))

for contact in result.contacts:
    print(f"{contact.url}: {contact.merged_status}")
```

**方法：**
- `parse(input_path)` → `ParseResult`: 解析输入文件

#### Contact

不可变联系人数据对象。

```python
from textag import Contact

contact = Contact(url="https://example.com", statuses=frozenset({"tag1", "tag2"}))
print(contact.merged_status)  # "tag1, tag2"
print(contact.status_count)   # 2
```

**属性：**
- `url` → `str`: 联系人 URL
- `statuses` → `FrozenSet[str]`: 状态集合
- `merged_status` → `str`: 合并后的状态字符串
- `status_count` → `int`: 状态数量

**方法：**
- `to_dict()` → `Dict[str, str]`: 转换为字典

#### ParseResult

解析结果容器。

```python
result = parser.parse(Path("input.txt"))
print(f"唯一联系人：{result.unique_count}")
print(f"重复 URL: {result.duplicate_count}")
print(f"是否有警告：{result.has_warnings}")
```

**属性：**
- `contacts` → `List[Contact]`: 联系人列表
- `duplicates` → `Dict[str, int]`: 重复 URL 计数
- `warnings` → `List[str]`: 警告信息
- `unique_count` → `int`: 唯一联系人数量
- `duplicate_count` → `int`: 重复 URL 数量
- `has_warnings` → `bool`: 是否有警告

### 导出器

#### IExporter (抽象基类)

所有导出器必须继承的接口。

```python
from textag import IExporter
from pathlib import Path

class CustomExporter(IExporter):
    @property
    def supported_extension(self) -> str:
        return ".custom"
    
    def export(self, contacts, output_path):
        # 实现导出逻辑
        pass
```

#### 内置导出器

- `ExcelExporter`: Excel 导出器（需要 pandas 和 openpyxl）
- `CSVExporter`: CSV 导出器
- `JSONExporter`: JSON 导出器

### 日志系统

#### ILogger (协议)

日志接口定义。

```python
from textag import ILogger

logger.info("这是一条信息")
logger.warning("这是一条警告")
logger.error("这是一个错误")
logger.success("操作成功")
logger.header("这是标题")
```

#### 日志实现

- `StandardLogger`: 标准日志（使用 Python logging）
- `RichLogger`: Rich 美化日志（自动降级）

---

## 🏗️ 架构设计

### 分层架构图

```
┌─────────────────────────────────────────────────────────────┐
│                    CLI 层 (Presentation)                    │
│  - 参数解析 (argparse)                                      │
│  - 程序入口 (main)                                          │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                   服务层 (Service Layer)                    │
│  - ContactService: 外观模式，协调解析与导出                  │
│  - 导出器注册表 (Registry Pattern)                          │
└─────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│   解析层        │ │   导出层        │ │   日志层        │
│ ContactParser   │ │ IExporter       │ │ ILogger         │
│ (SRP: 只解析)   │ │ ├─ ExcelExporter│ │ ├─ RichLogger   │
│                 │ │ ├─ CSVExporter  │ │ └─ StandardLogger│
│                 │ │ └─ JSONExporter │ │                 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                   数据层 (Data Layer)                       │
│  - Contact (不可变数据对象)                                  │
│  - ParseResult (结果容器)                                    │
└─────────────────────────────────────────────────────────────┘
```

### SOLID 原则应用

| 原则 | 原代码问题 | 重构后方案 |
|------|-----------|-----------|
| **SRP** | 一个类同时处理解析、导出、CLI、日志 | 拆分为 ContactParser、IExporter、ContactService、ILogger |
| **OCP** | 新增格式需要修改原有代码 | 通过 IExporter 接口扩展，register_exporter 注册新格式 |
| **LSP** | 无继承结构 | 所有导出器实现同一接口，可互换使用 |
| **ISP** | 原代码强制依赖 Rich 的所有功能 | ILogger 只定义必要方法，RichLogger 内部处理降级 |
| **DIP** | 高层模块依赖 pandas、rich 等具体实现 | ContactService 依赖 ILogger/IExporter 抽象 |

---

## 🔨 扩展开发

### 添加新导出格式

```python
from textag import IExporter, ContactService, Contact
from pathlib import Path
import xml.etree.ElementTree as ET

class XMLExporter(IExporter):
    """自定义 XML 导出器"""
    
    @property
    def supported_extension(self) -> str:
        return ".xml"
    
    def export(self, contacts, output_path):
        root = ET.Element("contacts")
        for contact in contacts:
            child = ET.SubElement(root, "contact")
            child.set("url", contact.url)
            child.set("status", contact.merged_status)
        
        tree = ET.ElementTree(root)
        tree.write(output_path, encoding='utf-8', xml_declaration=True)

# 注册新导出器（无需修改原代码）
ContactService.register_exporter('.xml', XMLExporter)

# 使用
service = ContactService()
service.process(Path("input.txt"), Path("output.xml"))
```

### 自定义日志系统

```python
from textag import ILogger

class FileLogger(ILogger):
    """文件日志记录器"""
    
    def __init__(self, log_file):
        self.log_file = log_file
    
    def info(self, msg): self._write("INFO", msg)
    def warning(self, msg): self._write("WARN", msg)
    def error(self, msg): self._write("ERROR", msg)
    def success(self, msg): self._write("SUCCESS", msg)
    def header(self, msg): self._write("HEADER", f"\n{'='*40}\n{msg}\n{'='*40}")
    
    def _write(self, level, msg):
        with open(self.log_file, 'a', encoding='utf-8') as f:
            f.write(f"[{level}] {msg}\n")

# 使用
service = ContactService(logger=FileLogger("app.log"))
```

---

## 🧪 测试与开发

### 运行测试

```bash
# 安装开发依赖
pip install textag[dev]

# 运行所有测试
pytest

# 运行测试并生成覆盖率报告
pytest --cov=textag --cov-report=html

# 查看 HTML 覆盖率报告
open htmlcov/index.html
```

### 代码质量检查

```bash
# 代码格式化检查
black src/ tests/

# 代码风格检查
flake8 src/ tests/

# 类型检查
mypy src/
```

### 本地开发安装

```bash
# 克隆仓库
git clone https://github.com/textag/textag.git
cd textag

# 开发模式安装
pip install -e ".[dev]"

# 运行测试
pytest
```

---

## ❓ 常见问题

### Q: 中文乱码问题？

A: 确保输入文件保存为 UTF-8 编码。Windows 用户可使用记事本打开文件，选择"另存为"，在编码中选择"UTF-8"。

### Q: 如何禁用 Rich 美化输出？

A: 使用 `--no-rich` 参数：
```bash
textag -i input.txt -o output.xlsx --no-rich
```

### Q: 如何添加自定义导出格式？

A: 继承 `IExporter` 接口并注册：
```python
ContactService.register_exporter('.xml', XMLExporter)
```

### Q: 返回码的含义？

A: 
- `0`: 成功完成，无警告
- `1`: 成功完成，但有警告（如存在重复 URL）
- `2`: 失败（文件不存在、格式错误等）

### Q: 支持的最大文件大小？

A: 当前版本将所有数据加载到内存，建议处理不超过 10 万行的文件。如需处理更大文件，建议使用流式处理版本。

---

## 📝 变更日志

### v1.0.0 (2026-03-21)

- ✨ 初始版本发布
- ✅ 实现核心解析功能
- ✅ 支持 Excel、CSV、JSON 导出
- ✅ 完整 SOLID 架构重构
- ✅ Rich 美化日志支持
- ✅ 命令行工具

---

## 🤝 贡献指南

欢迎提交 Issue 和 Pull Request！

1. Fork 本项目
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
3. 提交更改 (`git commit -m 'Add some amazing feature'`)
4. 推送到分支 (`git push origin feature/amazing-feature`)
5. 创建 Pull Request

---

## 📄 许可证

MIT License - 详见 [LICENSE](LICENSE) 文件

---

## 👥 作者

- textag team

---

<div align="center">

**如果这个项目对你有帮助，请给一个 ⭐️ Star 支持！**

</div>
