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 }