63 lines
2.0 KiB
Go
63 lines
2.0 KiB
Go
package kb
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/ledongthuc/pdf"
|
||
)
|
||
|
||
// ========================================================================
|
||
// PDF 解析器:从 PDF 字节流抽取纯文本
|
||
// ========================================================================
|
||
// 选型说明:
|
||
// - 用 github.com/ledongthuc/pdf(rsc.io/pdf 的维护分支,纯 Go 无 CGO),
|
||
// 许可证 BSD,可商用;不引 unioffice/unipdf(AGPL 商用有坑)
|
||
// - 只能抽「文字型 PDF」的文本;扫描版/图片型 PDF 没有文字层,
|
||
// 需要 OCR,超出本服务范围,由上层给出友好报错
|
||
// ========================================================================
|
||
|
||
// extractPDFText 逐页抽取 PDF 文本,页与页之间用双换行分隔
|
||
//
|
||
// 为什么逐页而不是整本 GetPlainText:
|
||
// 页边界是天然的段落边界,配合 ChunkPlainText 按双换行切段,
|
||
// 能避免跨页内容被硬拼成一大段
|
||
//
|
||
// 为什么要 recover:
|
||
// ledongthuc/pdf 遇到畸形/加密 PDF 时内部会直接 panic(继承自 rsc.io/pdf 的风格),
|
||
// 不兜底会把整个 HTTP 请求打崩
|
||
func extractPDFText(data []byte) (text string, err error) {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
err = fmt.Errorf("kb: PDF 解析异常(文件可能损坏或加密): %v", r)
|
||
}
|
||
}()
|
||
|
||
reader, err := pdf.NewReader(bytes.NewReader(data), int64(len(data)))
|
||
if err != nil {
|
||
return "", fmt.Errorf("kb: 打开 PDF 失败: %w", err)
|
||
}
|
||
|
||
var sb strings.Builder
|
||
totalPage := reader.NumPage()
|
||
for i := 1; i <= totalPage; i++ {
|
||
page := reader.Page(i)
|
||
if page.V.IsNull() {
|
||
continue
|
||
}
|
||
// 单页解析失败不中断整本导入(常见于个别页含特殊字体)
|
||
pageText, perr := page.GetPlainText(nil)
|
||
if perr != nil {
|
||
continue
|
||
}
|
||
pageText = strings.TrimSpace(pageText)
|
||
if pageText == "" {
|
||
continue
|
||
}
|
||
sb.WriteString(pageText)
|
||
sb.WriteString("\n\n")
|
||
}
|
||
return strings.TrimSpace(sb.String()), nil
|
||
}
|