Complete Unified API refactoring (v1.3.0)
This commit is contained in:
parent
3d74c78d8b
commit
baf0b3835d
@ -1,5 +1,12 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## v1.3.0 (2026-05-12)
|
||||||
|
|
||||||
|
- 核心架构重构:引入统一的 `Document` 接口,支持 **Open/Create/Save** 的极简 API 范式。
|
||||||
|
- 自动识别:`office.Open(path)` 可根据后缀名自动选择解析器。
|
||||||
|
- 标准化载体:所有文档类型均支持 `ToJSON()` 和 `ToMarkdown()`,完美对齐 AI 工作流。
|
||||||
|
- 零摩擦设计:Excel 导出自动处理 Sheet 和列扩展;PDF/Word/PPT 提取自动结构化。
|
||||||
|
|
||||||
## v1.2.0 (2026-05-12)
|
## v1.2.0 (2026-05-12)
|
||||||
|
|
||||||
- 新增 Excel 与 JSON 的双向转换支持 (`ToJSON`, `FromJSON`)。
|
- 新增 Excel 与 JSON 的双向转换支持 (`ToJSON`, `FromJSON`)。
|
||||||
|
|||||||
95
README.md
95
README.md
@ -1,57 +1,76 @@
|
|||||||
# office
|
# office
|
||||||
|
|
||||||
极简、高效的 Go Office 文档处理库,符合 `@go` 设计哲学。支持将 Office 文档与 JSON/Markdown 等通用格式无缝转换,作为 AI 时代的数据桥梁。
|
极简、高效的 Go Office 文档处理桥梁,符合 `@go` 设计哲学。
|
||||||
|
|
||||||
## 特性
|
## 核心设计
|
||||||
|
|
||||||
- **统一 API**: 提供极简的 `Open`, `Save`, `ToJSON`, `ToMarkdown` 等操作。
|
`office` 包将所有复杂的文档格式(Excel, Word, PPT, PDF)抽象为统一的 `Document` 接口。你只需要关心 **数据载体** (JSON/Markdown),而不需要关心底层实现。
|
||||||
- **载体转换**:
|
|
||||||
- **Excel <-> JSON**: 自动将表格映射为对象数组,支持双向转换。
|
|
||||||
- **Docx/Pptx/PDF -> Markdown**: 提取并转化为 AI 友好的 Markdown 格式。
|
|
||||||
- **纯 Go 实现**: 无 CGo 依赖,极致的跨平台与部署性能。
|
|
||||||
- **Excel 增强**: 自动处理工作表对齐,支持动态列扩展。
|
|
||||||
|
|
||||||
## 快速开始
|
- **结构化数据 (Excel)** <-> **JSON**
|
||||||
|
- **半结构化内容 (Word/PPT/PDF)** -> **Markdown**
|
||||||
|
|
||||||
### 安装
|
## 统一 API
|
||||||
|
|
||||||
```bash
|
|
||||||
go get apigo.cc/go/office
|
|
||||||
```
|
|
||||||
|
|
||||||
### Excel <-> JSON (结构化数据载体)
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Excel 转 JSON
|
import "apigo.cc/go/office"
|
||||||
xls, _ := office.Open("data.xlsx")
|
|
||||||
jsonStr, _ := xls.ToJSON("Sheet1", "A1", "")
|
|
||||||
|
|
||||||
// JSON 转 Excel
|
// 1. 打开文档 (自动识别类型)
|
||||||
newXls := office.New()
|
doc, _ := office.Open("data.xlsx")
|
||||||
newXls.FromJSON("Sheet1", jsonStr, "A1", "")
|
|
||||||
newXls.Save("restored.xlsx")
|
// 2. 转换为通用载体
|
||||||
|
jsonStr := doc.ToJSON() // 适合 Excel
|
||||||
|
mdStr := doc.ToMarkdown() // 适合 Word/PPT/PDF
|
||||||
|
|
||||||
|
// 3. 修改并保存 (Excel 支持数据回写)
|
||||||
|
// (对于 Excel,你可以强制断言获取更多方法)
|
||||||
|
if xls, ok := doc.(*office.Excel); ok {
|
||||||
|
xls.SetData("Sheet1", newData, "A1", "")
|
||||||
|
}
|
||||||
|
doc.Save("updated.xlsx")
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docx/Pptx/PDF -> Markdown (内容载体)
|
## 支持格式
|
||||||
|
|
||||||
|
| 格式 | 后缀 | `ToJSON` 表现 | `ToMarkdown` 表现 | `Save` 行为 |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| **Excel** | `.xlsx` | 返回第一个 Sheet 的对象数组 | 返回第一个 Sheet 的 MD 表格 | 保存为 Excel 文件 |
|
||||||
|
| **Word** | `.docx` | 返回 `{"metadata":..., "content":...}` | 返回提取的全文文本 | 保存为纯文本文件 |
|
||||||
|
| **PPT** | `.pptx` | 返回 `{"metadata":..., "content":...}` | 返回提取的全文文本 | 保存为纯文本文件 |
|
||||||
|
| **PDF** | `.pdf` | 返回 `{"metadata":..., "content":...}` | 返回提取的全文文本 | 保存为纯文本文件 |
|
||||||
|
|
||||||
|
## 快速示例
|
||||||
|
|
||||||
|
### AI 友好型转换
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Word 转 Markdown
|
// 一行代码将 PDF 转为 AI 易读的 Markdown
|
||||||
doc, _ := office.OpenDocx("contract.docx")
|
md, _ := office.Open("report.pdf").ToMarkdown()
|
||||||
md, _ := doc.ToMarkdown()
|
|
||||||
|
|
||||||
// PDF 转 Markdown
|
// 一行代码将 Excel 转为结构化 JSON
|
||||||
pdf, _ := office.OpenPDF("report.pdf")
|
json, _ := office.Open("sales.xlsx").ToJSON()
|
||||||
md, _ := pdf.ToMarkdown()
|
```
|
||||||
|
|
||||||
|
### 极简读写 (Excel)
|
||||||
|
|
||||||
|
```go
|
||||||
|
xls, _ := office.OpenExcel("config.xlsx")
|
||||||
|
data, _ := xls.GetData("Sheet1", "A1", "")
|
||||||
|
// ... 修改 data
|
||||||
|
xls.SetData("Sheet1", data, "A1", "")
|
||||||
|
xls.Save()
|
||||||
```
|
```
|
||||||
|
|
||||||
## API 参考
|
## API 参考
|
||||||
|
|
||||||
### Excel (JSON 转换)
|
### 顶级函数
|
||||||
- `ToJSON(sheetName string, start, end string) (string, error)`
|
- `Open(filename string, password ...string) (Document, error)`
|
||||||
- `FromJSON(sheetName string, jsonStr string, start, end string) error`
|
- `Create(ext string) (Document, error)`
|
||||||
- `GetData(sheetName string, start, end string) ([]map[string]any, error)`
|
|
||||||
- `SetData(sheetName string, data []map[string]any, start, end string) error`
|
|
||||||
|
|
||||||
### Word/PPT/PDF (Markdown 提取)
|
### Document 接口
|
||||||
- `ToMarkdown() (string, error)`: 将文档内容提取为 Markdown 格式字符串。
|
- `ToJSON() string`
|
||||||
- `Text() (string, error)`: 提取纯文本。
|
- `ToMarkdown() string`
|
||||||
|
- `Save(filename ...string) error`
|
||||||
|
|
||||||
|
### 类型特定 (导出)
|
||||||
|
- `Excel`: 支持 `SetData`, `GetData`, `Sheets` 等。
|
||||||
|
- `Docx`, `Pptx`, `PDF`: 支持 `Content` 和 `Metadata` 直接访问。
|
||||||
|
|||||||
57
docx.go
57
docx.go
@ -4,13 +4,16 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"apigo.cc/go/cast"
|
||||||
"apigo.cc/go/file"
|
"apigo.cc/go/file"
|
||||||
"github.com/young2j/oxmltotext/docxtotext"
|
"github.com/young2j/oxmltotext/docxtotext"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Docx 封装了 Word 文档的读取操作。
|
// Docx 封装了 Word 文档的读取与识别。
|
||||||
type Docx struct {
|
type Docx struct {
|
||||||
filename string
|
filename string
|
||||||
|
Content string
|
||||||
|
Metadata map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenDocx 打开一个 Word 文档 (.docx)。
|
// OpenDocx 打开一个 Word 文档 (.docx)。
|
||||||
@ -18,18 +21,43 @@ func OpenDocx(filename string) (*Docx, error) {
|
|||||||
if !file.Exists(filename) {
|
if !file.Exists(filename) {
|
||||||
return nil, os.ErrNotExist
|
return nil, os.ErrNotExist
|
||||||
}
|
}
|
||||||
return &Docx{filename: filename}, nil
|
d := &Docx{
|
||||||
|
filename: filename,
|
||||||
|
Metadata: make(map[string]any),
|
||||||
|
}
|
||||||
|
|
||||||
|
dp, err := docxtotext.Open(filename)
|
||||||
|
if err == nil {
|
||||||
|
defer dp.Close()
|
||||||
|
d.Content, _ = dp.ExtractTexts()
|
||||||
|
}
|
||||||
|
|
||||||
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text 提取文档中的所有文本。
|
// ToJSON 返回包含元数据和内容的 JSON 字符串。
|
||||||
func (d *Docx) Text() (string, error) {
|
func (d *Docx) ToJSON() string {
|
||||||
dp, err := docxtotext.Open(d.filename)
|
res, _ := cast.ToJSON(map[string]any{
|
||||||
if err != nil {
|
"metadata": d.Metadata,
|
||||||
return "", err
|
"content": d.Content,
|
||||||
}
|
})
|
||||||
defer dp.Close()
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
return dp.ExtractTexts()
|
// ToMarkdown 返回 Markdown 格式的内容。
|
||||||
|
func (d *Docx) ToMarkdown() string {
|
||||||
|
return d.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save 保存文档。目前主要支持保存提取后的文本。
|
||||||
|
func (d *Docx) Save(filename ...string) error {
|
||||||
|
path := d.filename
|
||||||
|
if len(filename) > 0 && filename[0] != "" {
|
||||||
|
path = filename[0]
|
||||||
|
}
|
||||||
|
// 如果是原格式保存,目前仅作为文本保存或保持原样
|
||||||
|
// 复杂的 Docx 写入暂未集成
|
||||||
|
return file.Write(path, d.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadText 从 io.Reader 中读取并提取 Word 文本。
|
// ReadText 从 io.Reader 中读取并提取 Word 文本。
|
||||||
@ -42,12 +70,3 @@ func (d *Docx) ReadText(r io.ReaderAt, size int64) (string, error) {
|
|||||||
|
|
||||||
return dp.ExtractTexts()
|
return dp.ExtractTexts()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMarkdown 将 Word 文档内容转换为 Markdown 格式。
|
|
||||||
func (d *Docx) ToMarkdown() (string, error) {
|
|
||||||
text, err := d.Text()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return text, nil
|
|
||||||
}
|
|
||||||
|
|||||||
149
excel.go
149
excel.go
@ -5,29 +5,29 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
"apigo.cc/go/file"
|
"apigo.cc/go/file"
|
||||||
"github.com/xuri/excelize/v2"
|
"github.com/xuri/excelize/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Excel 封装了 Excel 文件的核心操作,提供极简且高效的 API。
|
// Excel 封装了 Excel 文件的核心操作。
|
||||||
type Excel struct {
|
type Excel struct {
|
||||||
filename string
|
filename string
|
||||||
password string
|
password string
|
||||||
excel *excelize.File
|
excel *excelize.File
|
||||||
}
|
}
|
||||||
|
|
||||||
// New 创建一个新的 Excel 对象。
|
// NewExcel 创建一个新的 Excel 对象。
|
||||||
func New() *Excel {
|
func NewExcel() *Excel {
|
||||||
return &Excel{
|
return &Excel{
|
||||||
excel: excelize.NewFile(),
|
excel: excelize.NewFile(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open 打开一个现有的 Excel 文件。
|
// OpenExcel 打开一个现有的 Excel 文件。
|
||||||
// 如果文件不存在,将创建一个新的 Excel 对象。
|
func OpenExcel(filename string, password ...string) (*Excel, error) {
|
||||||
func Open(filename string, password ...string) (*Excel, error) {
|
|
||||||
pwd := ""
|
pwd := ""
|
||||||
if len(password) > 0 {
|
if len(password) > 0 {
|
||||||
pwd = password[0]
|
pwd = password[0]
|
||||||
@ -52,7 +52,6 @@ func Open(filename string, password ...string) (*Excel, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save 保存 Excel 文件。
|
// Save 保存 Excel 文件。
|
||||||
// 如果提供了 filename,将另存为该文件名。
|
|
||||||
func (xls *Excel) Save(filename ...string) error {
|
func (xls *Excel) Save(filename ...string) error {
|
||||||
if len(filename) > 0 && filename[0] != "" {
|
if len(filename) > 0 && filename[0] != "" {
|
||||||
xls.filename = filename[0]
|
xls.filename = filename[0]
|
||||||
@ -63,19 +62,51 @@ func (xls *Excel) Save(filename ...string) error {
|
|||||||
return xls.excel.SaveAs(xls.filename, excelize.Options{Password: xls.password})
|
return xls.excel.SaveAs(xls.filename, excelize.Options{Password: xls.password})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bytes 将 Excel 内容写入字节切片。
|
// ToJSON 将第一个工作表的数据转换为 JSON 字符串。
|
||||||
func (xls *Excel) Bytes() ([]byte, error) {
|
func (xls *Excel) ToJSON() string {
|
||||||
buf, err := xls.excel.WriteToBuffer()
|
sheets := xls.Sheets()
|
||||||
if err != nil {
|
if len(sheets) == 0 {
|
||||||
return nil, err
|
return "[]"
|
||||||
}
|
}
|
||||||
return buf.Bytes(), nil
|
data, _ := xls.GetData(sheets[0], "A1", "")
|
||||||
|
res, _ := cast.ToJSON(data)
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToMarkdown 将第一个工作表的数据转换为 Markdown 表格。
|
||||||
|
func (xls *Excel) ToMarkdown() string {
|
||||||
|
sheets := xls.Sheets()
|
||||||
|
if len(sheets) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
rows, _ := xls.Get(sheets[0], "A1", "")
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
for i, row := range rows {
|
||||||
|
sb.WriteString("| ")
|
||||||
|
for _, col := range row {
|
||||||
|
sb.WriteString(cast.To[string](col))
|
||||||
|
sb.WriteString(" | ")
|
||||||
|
}
|
||||||
|
sb.WriteString("\n")
|
||||||
|
if i == 0 {
|
||||||
|
// 分隔线
|
||||||
|
sb.WriteString("|")
|
||||||
|
for range row {
|
||||||
|
sb.WriteString(" --- |")
|
||||||
|
}
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 基础操作方法 ---
|
||||||
|
|
||||||
// Set 设置指定单元格范围的值。
|
// Set 设置指定单元格范围的值。
|
||||||
// table: 二维数组,代表行和列。
|
|
||||||
// start: 起始单元格 ID(如 "A1"),默认为 "A1"。
|
|
||||||
// end: 结束单元格 ID(如 "C3"),用于限制写入范围。
|
|
||||||
func (xls *Excel) Set(sheetName string, table [][]any, start, end string) error {
|
func (xls *Excel) Set(sheetName string, table [][]any, start, end string) error {
|
||||||
sheet := xls.getOrCreateSheet(sheetName)
|
sheet := xls.getOrCreateSheet(sheetName)
|
||||||
startX, startY := ParseCellID(start)
|
startX, startY := ParseCellID(start)
|
||||||
@ -127,7 +158,6 @@ func (xls *Excel) Get(sheetName string, start, end string) ([][]any, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试根据单元格类型转换数据
|
|
||||||
cellID := MakeCellID(x, y)
|
cellID := MakeCellID(x, y)
|
||||||
cellType, _ := xls.excel.GetCellType(sheet, cellID)
|
cellType, _ := xls.excel.GetCellType(sheet, cellID)
|
||||||
|
|
||||||
@ -140,9 +170,6 @@ func (xls *Excel) Get(sheetName string, start, end string) ([][]any, error) {
|
|||||||
}
|
}
|
||||||
case excelize.CellTypeBool:
|
case excelize.CellTypeBool:
|
||||||
rowData = append(rowData, cast.To[bool](v))
|
rowData = append(rowData, cast.To[bool](v))
|
||||||
case excelize.CellTypeDate:
|
|
||||||
// TODO: 更好的日期处理
|
|
||||||
rowData = append(rowData, v)
|
|
||||||
default:
|
default:
|
||||||
rowData = append(rowData, v)
|
rowData = append(rowData, v)
|
||||||
}
|
}
|
||||||
@ -152,53 +179,6 @@ func (xls *Excel) Get(sheetName string, start, end string) ([][]any, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveSheet 删除工作表。
|
|
||||||
func (xls *Excel) RemoveSheet(sheetName string) error {
|
|
||||||
return xls.excel.DeleteSheet(sheetName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sheets 返回所有工作表名称。
|
|
||||||
func (xls *Excel) Sheets() []string {
|
|
||||||
return xls.excel.GetSheetList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetColWidth 设置列宽。
|
|
||||||
func (xls *Excel) SetColWidth(sheetName string, startCol, endCol string, width float64) error {
|
|
||||||
return xls.excel.SetColWidth(xls.getOrCreateSheet(sheetName), startCol, endCol, width)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetColWidths 批量设置列宽。
|
|
||||||
func (xls *Excel) SetColWidths(sheetName string, widths []float64) error {
|
|
||||||
sheet := xls.getOrCreateSheet(sheetName)
|
|
||||||
for i, w := range widths {
|
|
||||||
col := MakeColID(i)
|
|
||||||
if err := xls.excel.SetColWidth(sheet, col, col, w); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCellStyle 设置单元格样式。
|
|
||||||
func (xls *Excel) SetCellStyle(sheetName string, start, end string, styleID int) error {
|
|
||||||
return xls.excel.SetCellStyle(xls.getOrCreateSheet(sheetName), start, end, styleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeStyle 创建样式并返回样式 ID。
|
|
||||||
func (xls *Excel) MakeStyle(style *excelize.Style) (int, error) {
|
|
||||||
return xls.excel.NewStyle(style)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPanes 设置冻结窗格。
|
|
||||||
func (xls *Excel) SetPanes(sheetName string, panes *excelize.Panes) error {
|
|
||||||
return xls.excel.SetPanes(xls.getOrCreateSheet(sheetName), panes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAutoFilter 设置自动筛选。
|
|
||||||
func (xls *Excel) SetAutoFilter(sheetName string, start, end string, options []excelize.AutoFilterOptions) error {
|
|
||||||
return xls.excel.AutoFilter(xls.getOrCreateSheet(sheetName), start+":"+end, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetData 将对象列表写入 Excel。
|
// SetData 将对象列表写入 Excel。
|
||||||
func (xls *Excel) SetData(sheetName string, data []map[string]any, start, end string) error {
|
func (xls *Excel) SetData(sheetName string, data []map[string]any, start, end string) error {
|
||||||
table, err := xls.Get(sheetName, start, end)
|
table, err := xls.Get(sheetName, start, end)
|
||||||
@ -227,7 +207,6 @@ func (xls *Excel) SetData(sheetName string, data []map[string]any, start, end st
|
|||||||
idx = fieldNum
|
idx = fieldNum
|
||||||
fieldIndex[k] = idx
|
fieldIndex[k] = idx
|
||||||
fieldNum++
|
fieldNum++
|
||||||
// 扩展所有行以匹配新的列数
|
|
||||||
for r := range table {
|
for r := range table {
|
||||||
for len(table[r]) < fieldNum {
|
for len(table[r]) < fieldNum {
|
||||||
table[r] = append(table[r], "")
|
table[r] = append(table[r], "")
|
||||||
@ -276,22 +255,9 @@ func (xls *Excel) GetData(sheetName string, start, end string) ([]map[string]any
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToJSON 将指定工作表的数据转换为 JSON 字符串。
|
// Sheets 返回所有工作表名称。
|
||||||
func (xls *Excel) ToJSON(sheetName string, start, end string) (string, error) {
|
func (xls *Excel) Sheets() []string {
|
||||||
data, err := xls.GetData(sheetName, start, end)
|
return xls.excel.GetSheetList()
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return cast.ToJSON(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromJSON 从 JSON 字符串加载数据到指定工作表。
|
|
||||||
func (xls *Excel) FromJSON(sheetName string, jsonStr string, start, end string) error {
|
|
||||||
data, err := cast.FromJSON[[]map[string]any](jsonStr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return xls.SetData(sheetName, data, start, end)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助方法:获取或创建工作表
|
// 辅助方法:获取或创建工作表
|
||||||
@ -299,12 +265,10 @@ func (xls *Excel) getOrCreateSheet(name string) string {
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
name = "Sheet1"
|
name = "Sheet1"
|
||||||
}
|
}
|
||||||
// 如果是数字索引
|
|
||||||
if matched, _ := regexp.MatchString(`^\d+$`, name); matched {
|
if matched, _ := regexp.MatchString(`^\d+$`, name); matched {
|
||||||
idx := cast.To[int](name)
|
idx := cast.To[int](name)
|
||||||
return xls.excel.GetSheetName(idx)
|
return xls.excel.GetSheetName(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
idx, _ := xls.excel.GetSheetIndex(name)
|
idx, _ := xls.excel.GetSheetIndex(name)
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
xls.excel.NewSheet(name)
|
xls.excel.NewSheet(name)
|
||||||
@ -312,12 +276,12 @@ func (xls *Excel) getOrCreateSheet(name string) string {
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeCellID 根据行列索引生成单元格 ID(如 0, 0 -> "A1")。
|
// MakeCellID 生成单元格 ID。
|
||||||
func MakeCellID(col, row int) string {
|
func MakeCellID(col, row int) string {
|
||||||
return MakeColID(col) + strconv.Itoa(row+1)
|
return MakeColID(col) + strconv.Itoa(row+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeColID 根据列索引生成列 ID(如 0 -> "A", 26 -> "AA")。
|
// MakeColID 生成列 ID。
|
||||||
func MakeColID(col int) string {
|
func MakeColID(col int) string {
|
||||||
colName := ""
|
colName := ""
|
||||||
for col >= 0 {
|
for col >= 0 {
|
||||||
@ -327,13 +291,11 @@ func MakeColID(col int) string {
|
|||||||
return colName
|
return colName
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseCellID 解析单元格 ID(如 "A1" -> 0, 0)。
|
// ParseCellID 解析单元格 ID。
|
||||||
func ParseCellID(cell string) (col, row int) {
|
func ParseCellID(cell string) (col, row int) {
|
||||||
if cell == "" {
|
if cell == "" {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找到第一个数字的位置
|
|
||||||
numIdx := -1
|
numIdx := -1
|
||||||
for i, r := range cell {
|
for i, r := range cell {
|
||||||
if r >= '0' && r <= '9' {
|
if r >= '0' && r <= '9' {
|
||||||
@ -341,15 +303,12 @@ func ParseCellID(cell string) (col, row int) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if numIdx == -1 {
|
if numIdx == -1 {
|
||||||
// 只有字母,当做列处理
|
|
||||||
return parseCol(cell), 0
|
return parseCol(cell), 0
|
||||||
}
|
}
|
||||||
|
|
||||||
col = parseCol(cell[:numIdx])
|
col = parseCol(cell[:numIdx])
|
||||||
row, _ = strconv.Atoi(cell[numIdx:])
|
row, _ = strconv.Atoi(cell[numIdx:])
|
||||||
row-- // 转为 0 索引
|
row--
|
||||||
if row < 0 {
|
if row < 0 {
|
||||||
row = 0
|
row = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package office
|
package office
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"apigo.cc/go/file"
|
"apigo.cc/go/file"
|
||||||
@ -10,7 +11,7 @@ func TestExcel_Basic(t *testing.T) {
|
|||||||
filename := "test_basic.xlsx"
|
filename := "test_basic.xlsx"
|
||||||
defer file.Remove(filename)
|
defer file.Remove(filename)
|
||||||
|
|
||||||
xls := New()
|
xls := NewExcel()
|
||||||
table := [][]any{
|
table := [][]any{
|
||||||
{"Name", "Age", "City"},
|
{"Name", "Age", "City"},
|
||||||
{"Alice", 25, "New York"},
|
{"Alice", 25, "New York"},
|
||||||
@ -27,10 +28,11 @@ func TestExcel_Basic(t *testing.T) {
|
|||||||
t.Fatalf("Save failed: %v", err)
|
t.Fatalf("Save failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
xls2, err := Open(filename)
|
doc2, err := Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Open failed: %v", err)
|
t.Fatalf("Open failed: %v", err)
|
||||||
}
|
}
|
||||||
|
xls2 := doc2.(*Excel)
|
||||||
|
|
||||||
data, err := xls2.Get("Sheet1", "A1", "C3")
|
data, err := xls2.Get("Sheet1", "A1", "C3")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -50,7 +52,7 @@ func TestExcel_Data(t *testing.T) {
|
|||||||
filename := "test_data.xlsx"
|
filename := "test_data.xlsx"
|
||||||
defer file.Remove(filename)
|
defer file.Remove(filename)
|
||||||
|
|
||||||
xls := New()
|
xls := NewExcel()
|
||||||
data := []map[string]any{
|
data := []map[string]any{
|
||||||
{"ID": 1, "Value": "A"},
|
{"ID": 1, "Value": "A"},
|
||||||
{"ID": 2, "Value": "B"},
|
{"ID": 2, "Value": "B"},
|
||||||
@ -66,10 +68,11 @@ func TestExcel_Data(t *testing.T) {
|
|||||||
t.Fatalf("Save failed: %v", err)
|
t.Fatalf("Save failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
xls2, err := Open(filename)
|
doc2, err := Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Open failed: %v", err)
|
t.Fatalf("Open failed: %v", err)
|
||||||
}
|
}
|
||||||
|
xls2 := doc2.(*Excel)
|
||||||
|
|
||||||
readData, err := xls2.GetData("Data", "A1", "")
|
readData, err := xls2.GetData("Data", "A1", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -85,6 +88,37 @@ func TestExcel_Data(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnifiedAPI(t *testing.T) {
|
||||||
|
// Test Excel with Unified API
|
||||||
|
filename := "test_unified.xlsx"
|
||||||
|
defer file.Remove(filename)
|
||||||
|
|
||||||
|
doc, err := Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if xls, ok := doc.(*Excel); ok {
|
||||||
|
xls.SetData("Sheet1", []map[string]any{{"Name": "Unified", "Value": 100}}, "A1", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = doc.Save()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Save failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify JSON/Markdown
|
||||||
|
jsonStr := doc.ToJSON()
|
||||||
|
if !strings.Contains(jsonStr, "Unified") {
|
||||||
|
t.Errorf("ToJSON failed: %s", jsonStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
mdStr := doc.ToMarkdown()
|
||||||
|
if !strings.Contains(mdStr, "Unified") {
|
||||||
|
t.Errorf("ToMarkdown failed: %s", mdStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIDGeneration(t *testing.T) {
|
func TestIDGeneration(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
col, row int
|
col, row int
|
||||||
|
|||||||
48
office.go
Normal file
48
office.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package office
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Document 定义了所有办公文档的统一行为。
|
||||||
|
type Document interface {
|
||||||
|
// ToJSON 将文档转换为结构化 JSON 字符串。
|
||||||
|
ToJSON() string
|
||||||
|
// ToMarkdown 将文档内容转换为 Markdown 格式。
|
||||||
|
ToMarkdown() string
|
||||||
|
// Save 保存文档到文件系统。
|
||||||
|
Save(filename ...string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open 根据文件后缀名自动打开文档并返回标准接口。
|
||||||
|
func Open(filename string, password ...string) (Document, error) {
|
||||||
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
|
switch ext {
|
||||||
|
case ".xlsx", ".xlsm":
|
||||||
|
return OpenExcel(filename, password...)
|
||||||
|
case ".docx":
|
||||||
|
return OpenDocx(filename)
|
||||||
|
case ".pptx":
|
||||||
|
return OpenPptx(filename)
|
||||||
|
case ".pdf":
|
||||||
|
return OpenPDF(filename)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported file type: %s", ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建指定类型的空白文档。
|
||||||
|
func Create(ext string) (Document, error) {
|
||||||
|
ext = strings.ToLower(strings.TrimPrefix(ext, "."))
|
||||||
|
switch ext {
|
||||||
|
case "xlsx":
|
||||||
|
return NewExcel(), nil
|
||||||
|
case "docx":
|
||||||
|
// TODO: 支持创建 Word
|
||||||
|
return nil, fmt.Errorf("creating docx not supported yet")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported creation type: %s", ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
86
pdf.go
86
pdf.go
@ -6,13 +6,16 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"apigo.cc/go/cast"
|
||||||
"apigo.cc/go/file"
|
"apigo.cc/go/file"
|
||||||
"github.com/dslipak/pdf"
|
"github.com/dslipak/pdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PDF 封装了 PDF 文档的读取操作。
|
// PDF 封装了 PDF 文档的读取与识别。
|
||||||
type PDF struct {
|
type PDF struct {
|
||||||
filename string
|
filename string
|
||||||
|
Content string
|
||||||
|
Metadata map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenPDF 打开一个 PDF 文档。
|
// OpenPDF 打开一个 PDF 文档。
|
||||||
@ -20,57 +23,54 @@ func OpenPDF(filename string) (*PDF, error) {
|
|||||||
if !file.Exists(filename) {
|
if !file.Exists(filename) {
|
||||||
return nil, os.ErrNotExist
|
return nil, os.ErrNotExist
|
||||||
}
|
}
|
||||||
return &PDF{filename: filename}, nil
|
p := &PDF{
|
||||||
}
|
filename: filename,
|
||||||
|
Metadata: make(map[string]any),
|
||||||
// Text 提取 PDF 中的所有文本。
|
|
||||||
func (p *PDF) Text() (string, error) {
|
|
||||||
f, err := pdf.Open(p.filename)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var b bytes.Buffer
|
f, err := pdf.Open(filename)
|
||||||
t, err := f.GetPlainText()
|
if err == nil {
|
||||||
if err != nil {
|
p.Metadata["pages"] = f.NumPage()
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.Copy(&b, t)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToMarkdown 将 PDF 内容转换为 Markdown 格式。
|
|
||||||
func (p *PDF) ToMarkdown() (string, error) {
|
|
||||||
text, err := p.Text()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return text, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info 获取 PDF 的元数据。
|
|
||||||
func (p *PDF) Info() map[string]any {
|
|
||||||
f, err := pdf.Open(p.filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
info := make(map[string]any)
|
|
||||||
trailer := f.Trailer()
|
trailer := f.Trailer()
|
||||||
infoDict := trailer.Key("Info")
|
infoDict := trailer.Key("Info")
|
||||||
if !infoDict.IsNull() {
|
if !infoDict.IsNull() {
|
||||||
for _, field := range infoDict.Keys() {
|
for _, field := range infoDict.Keys() {
|
||||||
val := infoDict.Key(field).Text()
|
val := infoDict.Key(field).Text()
|
||||||
if val != "" {
|
if val != "" {
|
||||||
info[strings.ToLower(field)] = val
|
p.Metadata[strings.ToLower(field)] = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info["pages"] = f.NumPage()
|
|
||||||
return info
|
var b bytes.Buffer
|
||||||
|
if t, err := f.GetPlainText(); err == nil {
|
||||||
|
io.Copy(&b, t)
|
||||||
|
p.Content = b.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON 返回结构化 JSON。
|
||||||
|
func (p *PDF) ToJSON() string {
|
||||||
|
res, _ := cast.ToJSON(map[string]any{
|
||||||
|
"metadata": p.Metadata,
|
||||||
|
"content": p.Content,
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMarkdown 返回 Markdown。
|
||||||
|
func (p *PDF) ToMarkdown() string {
|
||||||
|
return p.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save 保存(目前保存为提取后的文本)。
|
||||||
|
func (p *PDF) Save(filename ...string) error {
|
||||||
|
path := p.filename
|
||||||
|
if len(filename) > 0 && filename[0] != "" {
|
||||||
|
path = filename[0]
|
||||||
|
}
|
||||||
|
return file.Write(path, p.Content)
|
||||||
}
|
}
|
||||||
|
|||||||
55
pptx.go
55
pptx.go
@ -4,13 +4,16 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"apigo.cc/go/cast"
|
||||||
"apigo.cc/go/file"
|
"apigo.cc/go/file"
|
||||||
"github.com/young2j/oxmltotext/pptxtotext"
|
"github.com/young2j/oxmltotext/pptxtotext"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pptx 封装了 PowerPoint 文档的读取操作。
|
// Pptx 封装了 PowerPoint 文档的读取与识别。
|
||||||
type Pptx struct {
|
type Pptx struct {
|
||||||
filename string
|
filename string
|
||||||
|
Content string
|
||||||
|
Metadata map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenPptx 打开一个 PowerPoint 文档 (.pptx)。
|
// OpenPptx 打开一个 PowerPoint 文档 (.pptx)。
|
||||||
@ -18,18 +21,41 @@ func OpenPptx(filename string) (*Pptx, error) {
|
|||||||
if !file.Exists(filename) {
|
if !file.Exists(filename) {
|
||||||
return nil, os.ErrNotExist
|
return nil, os.ErrNotExist
|
||||||
}
|
}
|
||||||
return &Pptx{filename: filename}, nil
|
p := &Pptx{
|
||||||
|
filename: filename,
|
||||||
|
Metadata: make(map[string]any),
|
||||||
|
}
|
||||||
|
|
||||||
|
pp, err := pptxtotext.Open(filename)
|
||||||
|
if err == nil {
|
||||||
|
defer pp.Close()
|
||||||
|
p.Content, _ = pp.ExtractTexts()
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text 提取文档中的所有文本。
|
// ToJSON 返回结构化 JSON。
|
||||||
func (p *Pptx) Text() (string, error) {
|
func (p *Pptx) ToJSON() string {
|
||||||
pp, err := pptxtotext.Open(p.filename)
|
res, _ := cast.ToJSON(map[string]any{
|
||||||
if err != nil {
|
"metadata": p.Metadata,
|
||||||
return "", err
|
"content": p.Content,
|
||||||
}
|
})
|
||||||
defer pp.Close()
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
return pp.ExtractTexts()
|
// ToMarkdown 返回 Markdown。
|
||||||
|
func (p *Pptx) ToMarkdown() string {
|
||||||
|
return p.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save 保存文档(目前保存为提取后的文本)。
|
||||||
|
func (p *Pptx) Save(filename ...string) error {
|
||||||
|
path := p.filename
|
||||||
|
if len(filename) > 0 && filename[0] != "" {
|
||||||
|
path = filename[0]
|
||||||
|
}
|
||||||
|
return file.Write(path, p.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadText 从 io.Reader 中读取并提取 PPT 文本。
|
// ReadText 从 io.Reader 中读取并提取 PPT 文本。
|
||||||
@ -42,12 +68,3 @@ func (p *Pptx) ReadText(r io.ReaderAt, size int64) (string, error) {
|
|||||||
|
|
||||||
return pp.ExtractTexts()
|
return pp.ExtractTexts()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMarkdown 将 PowerPoint 文档内容转换为 Markdown 格式。
|
|
||||||
func (p *Pptx) ToMarkdown() (string, error) {
|
|
||||||
text, err := p.Text()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return text, nil
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user