Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1897b701f |
@ -1,5 +1,12 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## v1.0.6 (2026-05-13)
|
||||||
|
- **新特性**: 支持 `.csv` 格式,支持对象数组与 Markdown 表格转换。
|
||||||
|
- **新特性**: 支持 `.md` 和 `.txt` 格式,统一纳入 `Document` 接口管理。
|
||||||
|
- **功能增强**: `Excel.ToMarkdown` 现在支持提取所有工作表 (Sheet),并自动添加标题。
|
||||||
|
- **功能增强**: `PDF.ToMarkdown` 增加页码标记 (`<!-- Page X -->`),方便 RAG 场景按页分片。
|
||||||
|
- **功能增强**: Markdown 表格转换增加对管道符 (`|`) 的转义处理。
|
||||||
|
|
||||||
## v1.0.5 (2026-05-13)
|
## v1.0.5 (2026-05-13)
|
||||||
- **新特性**: 引入 `Graph` 关系型文档类型 (`.graph`),支持节点内容与关系的统一建模。
|
- **新特性**: 引入 `Graph` 关系型文档类型 (`.graph`),支持节点内容与关系的统一建模。
|
||||||
- **新特性**: 关系型文档支持一键转换为带 **Mermaid** 图表的 Markdown,适用于脑图与分镜。
|
- **新特性**: 关系型文档支持一键转换为带 **Mermaid** 图表的 Markdown,适用于脑图与分镜。
|
||||||
|
|||||||
@ -33,11 +33,14 @@ doc.Save("updated.xlsx")
|
|||||||
|
|
||||||
| 格式 | 后缀 | `ToJSON` 表现 | `ToMarkdown` 表现 | `Save` 行为 |
|
| 格式 | 后缀 | `ToJSON` 表现 | `ToMarkdown` 表现 | `Save` 行为 |
|
||||||
| :--- | :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- | :--- |
|
||||||
| **Excel** | `.xlsx` | 返回第一个 Sheet 的对象数组 | 返回第一个 Sheet 的 MD 表格 | 保存为 Excel 文件 |
|
| **Excel** | `.xlsx` | 返回第一个 Sheet 的对象数组 | 返回**所有 Sheet** 的 MD 表格 | 保存为 Excel |
|
||||||
| **Word** | `.docx` | 返回 `{"metadata":..., "content":...}` | 返回**带层级标题**的 Markdown | 保存为文本 |
|
| **Word** | `.docx` | 返回 `{"metadata":..., "content":...}` | 返回**带层级标题**的 Markdown | 保存为文本 |
|
||||||
| **PPT** | `.pptx` | 返回 `{"metadata":..., "content":...}` | 返回提取的全文文本 | 保存为文本 |
|
| **PPT** | `.pptx` | 返回 `{"metadata":..., "content":...}` | 返回提取的全文文本 | 保存为文本 |
|
||||||
| **PDF** | `.pdf` | 返回 `{"metadata":..., "content":...}` | 返回提取的全文文本 | 保存为文本 |
|
| **PDF** | `.pdf` | 返回 `{"metadata":..., "content":...}` | 返回**带页码标记**的 Markdown | 保存为文本 |
|
||||||
| **Graph** | `.graph` | 返回节点与关系的结构化 JSON | 返回 **Mermaid 关系图** + 节点详情 | 保存为 JSON |
|
| **Graph** | `.graph` | 返回节点与关系的结构化 JSON | 返回 **Mermaid 关系图** + 节点详情 | 保存为 JSON |
|
||||||
|
| **CSV** | `.csv` | 返回对象数组 | 返回 MD 表格 | 保存为 CSV |
|
||||||
|
| **Markdown**| `.md` | 返回 `{"content":...}` | 返回原始文本 | 保存为 Markdown |
|
||||||
|
| **Text** | `.txt` | 返回 `{"content":...}` | 返回原始文本 | 保存为文本 |
|
||||||
|
|
||||||
## 快速示例
|
## 快速示例
|
||||||
|
|
||||||
|
|||||||
3
TEST.md
3
TEST.md
@ -7,6 +7,9 @@
|
|||||||
- [x] **ID 生成解析**: 验证 `MakeCellID` 和 `ParseCellID` 的准确性。
|
- [x] **ID 生成解析**: 验证 `MakeCellID` 和 `ParseCellID` 的准确性。
|
||||||
- [x] **Docx 标题识别**: 通过 XML 提取带 `#` 的层级 Markdown。
|
- [x] **Docx 标题识别**: 通过 XML 提取带 `#` 的层级 Markdown。
|
||||||
- [x] **Graph 关系文档**: 验证 `Node` 增删、`ToMarkdown` (Mermaid) 及 `Save/Open`。
|
- [x] **Graph 关系文档**: 验证 `Node` 增删、`ToMarkdown` (Mermaid) 及 `Save/Open`。
|
||||||
|
- [x] **CSV/Markdown 支持**: 验证 CSV 转对象/表格,Markdown 文件的统一接口读写。
|
||||||
|
- [x] **多 Sheet Excel**: 验证 `ToMarkdown` 导出所有工作表。
|
||||||
|
- [x] **PDF 页码标记**: 验证提取内容中包含页码注释。
|
||||||
|
|
||||||
## 性能测试 (Benchmark)
|
## 性能测试 (Benchmark)
|
||||||
- **环境**: Darwin / Apple M3 Max
|
- **环境**: Darwin / Apple M3 Max
|
||||||
|
|||||||
101
csv.go
Normal file
101
csv.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package document
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"apigo.cc/go/cast"
|
||||||
|
"apigo.cc/go/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CSV 封装了 CSV 文件的读写。
|
||||||
|
type CSV struct {
|
||||||
|
filename string
|
||||||
|
Data [][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenCSV 打开一个 CSV 文件。
|
||||||
|
func OpenCSV(filename string) (*CSV, error) {
|
||||||
|
if !file.Exists(filename) {
|
||||||
|
return &CSV{filename: filename}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := file.Read(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r := csv.NewReader(strings.NewReader(content))
|
||||||
|
data, err := r.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CSV{
|
||||||
|
filename: filename,
|
||||||
|
Data: data,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON 返回 JSON 数组。
|
||||||
|
func (c *CSV) ToJSON() string {
|
||||||
|
if len(c.Data) == 0 {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
|
|
||||||
|
header := c.Data[0]
|
||||||
|
var res []map[string]string
|
||||||
|
for i := 1; i < len(c.Data); i++ {
|
||||||
|
row := make(map[string]string)
|
||||||
|
for j, h := range header {
|
||||||
|
if j < len(c.Data[i]) {
|
||||||
|
row[h] = c.Data[i][j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res = append(res, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonStr, _ := cast.ToJSON(res)
|
||||||
|
return jsonStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMarkdown 返回 Markdown 表格。
|
||||||
|
func (c *CSV) ToMarkdown() string {
|
||||||
|
if len(c.Data) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
for i, row := range c.Data {
|
||||||
|
sb.WriteString("| ")
|
||||||
|
for _, col := range row {
|
||||||
|
val := strings.ReplaceAll(col, "|", "\\|")
|
||||||
|
sb.WriteString(val)
|
||||||
|
sb.WriteString(" | ")
|
||||||
|
}
|
||||||
|
sb.WriteString("\n")
|
||||||
|
if i == 0 {
|
||||||
|
sb.WriteString("|")
|
||||||
|
for range row {
|
||||||
|
sb.WriteString(" --- |")
|
||||||
|
}
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save 保存为 CSV 文件。
|
||||||
|
func (c *CSV) Save(filename ...string) error {
|
||||||
|
path := c.filename
|
||||||
|
if len(filename) > 0 && filename[0] != "" {
|
||||||
|
path = filename[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
w := csv.NewWriter(&sb)
|
||||||
|
w.WriteAll(c.Data)
|
||||||
|
w.Flush()
|
||||||
|
|
||||||
|
return file.Write(path, sb.String())
|
||||||
|
}
|
||||||
@ -30,6 +30,10 @@ func Open(filename string, password ...string) (Document, error) {
|
|||||||
return OpenPDF(filename)
|
return OpenPDF(filename)
|
||||||
case ".graph":
|
case ".graph":
|
||||||
return OpenGraph(filename)
|
return OpenGraph(filename)
|
||||||
|
case ".csv":
|
||||||
|
return OpenCSV(filename)
|
||||||
|
case ".md", ".markdown", ".txt":
|
||||||
|
return OpenMarkdown(filename)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported file type: %s", ext)
|
return nil, fmt.Errorf("unsupported file type: %s", ext)
|
||||||
}
|
}
|
||||||
@ -46,6 +50,10 @@ func Create(ext string) (Document, error) {
|
|||||||
return nil, fmt.Errorf("creating docx not supported yet")
|
return nil, fmt.Errorf("creating docx not supported yet")
|
||||||
case "graph":
|
case "graph":
|
||||||
return NewGraph(), nil
|
return NewGraph(), nil
|
||||||
|
case "csv":
|
||||||
|
return &CSV{}, nil
|
||||||
|
case "md", "markdown":
|
||||||
|
return &Markdown{}, nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported creation type: %s", ext)
|
return nil, fmt.Errorf("unsupported creation type: %s", ext)
|
||||||
}
|
}
|
||||||
|
|||||||
25
excel.go
25
excel.go
@ -72,22 +72,31 @@ func (xls *Excel) ToJSON() string {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMarkdown 将第一个工作表的数据转换为 Markdown 表格。
|
// ToMarkdown 将所有工作表的数据转换为 Markdown 格式。
|
||||||
func (xls *Excel) ToMarkdown() string {
|
func (xls *Excel) ToMarkdown() string {
|
||||||
sheets := xls.Sheets()
|
sheets := xls.Sheets()
|
||||||
if len(sheets) == 0 {
|
if len(sheets) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
rows, _ := xls.Get(sheets[0], "A1", "")
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
for _, sheetName := range sheets {
|
||||||
|
rows, _ := xls.Get(sheetName, "A1", "")
|
||||||
|
if len(rows) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sheets) > 1 {
|
||||||
|
sb.WriteString("## Sheet: " + sheetName + "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
for i, row := range rows {
|
for i, row := range rows {
|
||||||
sb.WriteString("| ")
|
sb.WriteString("| ")
|
||||||
for _, col := range row {
|
for _, col := range row {
|
||||||
sb.WriteString(cast.To[string](col))
|
val := cast.To[string](col)
|
||||||
|
// 转义 Markdown 表格中的管道符
|
||||||
|
val = strings.ReplaceAll(val, "|", "\\|")
|
||||||
|
sb.WriteString(val)
|
||||||
sb.WriteString(" | ")
|
sb.WriteString(" | ")
|
||||||
}
|
}
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
@ -100,7 +109,9 @@ func (xls *Excel) ToMarkdown() string {
|
|||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.String()
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 基础操作方法 ---
|
// --- 基础操作方法 ---
|
||||||
|
|||||||
52
markdown.go
Normal file
52
markdown.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package document
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"apigo.cc/go/cast"
|
||||||
|
"apigo.cc/go/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Markdown 封装了纯 Markdown 文档的读写,使其符合 Document 接口。
|
||||||
|
type Markdown struct {
|
||||||
|
filename string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenMarkdown 打开一个 Markdown 文件。
|
||||||
|
func OpenMarkdown(filename string) (*Markdown, error) {
|
||||||
|
m := &Markdown{filename: filename}
|
||||||
|
if file.Exists(filename) {
|
||||||
|
content, err := file.Read(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m.Content = content
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON 返回包含内容的 JSON 字符串。
|
||||||
|
func (m *Markdown) ToJSON() string {
|
||||||
|
res, _ := cast.ToJSON(map[string]string{
|
||||||
|
"content": m.Content,
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMarkdown 直接返回其内容。
|
||||||
|
func (m *Markdown) ToMarkdown() string {
|
||||||
|
return m.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save 保存为 Markdown 文件。
|
||||||
|
func (m *Markdown) Save(filename ...string) error {
|
||||||
|
path := m.filename
|
||||||
|
if len(filename) > 0 && filename[0] != "" {
|
||||||
|
path = filename[0]
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
return fmt.Errorf("no filename specified")
|
||||||
|
}
|
||||||
|
return file.Write(path, m.Content)
|
||||||
|
}
|
||||||
54
misc_test.go
Normal file
54
misc_test.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package document
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"apigo.cc/go/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCSV(t *testing.T) {
|
||||||
|
filename := "test.csv"
|
||||||
|
defer file.Remove(filename)
|
||||||
|
|
||||||
|
c, _ := OpenCSV(filename)
|
||||||
|
c.Data = [][]string{
|
||||||
|
{"Name", "Age"},
|
||||||
|
{"Alice", "25"},
|
||||||
|
{"Bob", "30"},
|
||||||
|
}
|
||||||
|
c.Save()
|
||||||
|
|
||||||
|
doc, err := Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
md := doc.ToMarkdown()
|
||||||
|
if !strings.Contains(md, "Alice") || !strings.Contains(md, "| --- |") {
|
||||||
|
t.Errorf("Unexpected Markdown: %s", md)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonStr := doc.ToJSON()
|
||||||
|
if !strings.Contains(jsonStr, "\"Name\":\"Alice\"") {
|
||||||
|
t.Errorf("Unexpected JSON: %s", jsonStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarkdown(t *testing.T) {
|
||||||
|
filename := "test.md"
|
||||||
|
defer file.Remove(filename)
|
||||||
|
|
||||||
|
m, _ := OpenMarkdown(filename)
|
||||||
|
m.Content = "# Hello\nWorld"
|
||||||
|
m.Save()
|
||||||
|
|
||||||
|
doc, err := Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if doc.ToMarkdown() != "# Hello\nWorld" {
|
||||||
|
t.Errorf("Unexpected Markdown: %s", doc.ToMarkdown())
|
||||||
|
}
|
||||||
|
}
|
||||||
23
pdf.go
23
pdf.go
@ -1,9 +1,7 @@
|
|||||||
package document
|
package document
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
@ -42,11 +40,24 @@ func OpenPDF(filename string) (*PDF, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var b bytes.Buffer
|
var sb strings.Builder
|
||||||
if t, err := f.GetPlainText(); err == nil {
|
for i := 1; i <= f.NumPage(); i++ {
|
||||||
io.Copy(&b, t)
|
p_ := f.Page(i)
|
||||||
p.Content = b.String()
|
if p_.V.IsNull() {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
t := p_.Content().Text
|
||||||
|
if len(t) > 0 {
|
||||||
|
if i > 1 {
|
||||||
|
sb.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("<!-- Page %d -->\n", i))
|
||||||
|
for _, text := range t {
|
||||||
|
sb.WriteString(text.S)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.Content = strings.TrimSpace(sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user