文档解析器:支持 PDF、DOCX、Markdown

张开发
2026/4/15 20:23:52 15 分钟阅读

分享文章

文档解析器:支持 PDF、DOCX、Markdown
文档解析器支持 PDF、DOCX、Markdown本文是《从零构建 InkWordsAI 驱动的技术博客生成器》系列的第 14 章。本系列完整源码可在 GitHub 仓库 获取。引言为什么需要文档解析器想象一下你正在准备一篇技术博客手头有各种格式的资料一份 PDF 格式的官方 API 文档、一个 Word 文档写的设计稿、以及几个 Markdown 格式的笔记。如果有一个工具能自动帮你把这些不同格式的内容统一提取成纯文本然后交给 AI 去整理润色那该多省事这正是 InkWords 文档解析器 (DocParser) 的使命。它作为整个系统的“前哨”负责将用户上传的各种格式文件PDF、DOCX、Markdown/TXT转换成统一的纯文本格式为后续的 AI 分析和博客生成铺平道路。本章我们将深入剖析DocParser的设计与实现看看它如何优雅地处理多种文件格式并贯彻“阅后即焚”的安全策略。核心架构接口与实现在 Go 语言中接口 (interface) 是定义行为契约的绝佳方式。InkWords 的解析器模块首先定义了一个通用的Parser接口// Parser defines the interface for all document parserstypeParserinterface{Parse(src io.Reader,filenamestring)(string,error)}这个接口非常简洁只包含一个Parse方法。它接收一个数据流 (io.Reader) 和文件名返回提取出的文本字符串或一个错误。这种设计带来了两大好处灵活性任何实现了Parse方法的结构体都可以成为解析器方便未来扩展比如支持 PPT、Excel。统一性无论底层处理的是 PDF 还是 DOCX对上层调用者来说都是同一个Parse方法大大降低了使用复杂度。我们的主角DocParser结构体就实现了这个接口它是一个“多面手”内部集成了对多种格式的处理逻辑。// DocParser implements Parser interface for PDF and Markdown filestypeDocParserstruct{}// NewDocParser creates a new instance of DocParserfuncNewDocParser()*DocParser{returnDocParser{}}解析流程总览“阅后即焚”策略DocParser的Parse方法是整个解析过程的总调度中心。它的核心流程可以用下面的时序图来清晰展示具体格式解析器(PDF/DOCX/Text)临时文件Parse方法用户/上游服务具体格式解析器(PDF/DOCX/Text)临时文件Parse方法用户/上游服务7. 函数返回后defer语句自动删除临时文件调用Parse(数据流, 文件名)1. 创建临时文件2. 写入数据流3. 刷新并重置指针4. 根据后缀名路由5. 返回提取的文本6. 返回纯文本结果这个流程中最精妙的设计莫过于“阅后即焚” (Burn After Reading)策略。用户上传的文件内容在处理完成后会立即从服务器磁盘上删除不留痕迹。这是如何实现的呢关键就在defer语句和临时文件操作上。让我们结合代码逐段分析func(p*DocParser)Parse(src io.Reader,filenamestring)(string,error){// 1. 获取文件后缀名用于后续路由判断ext:strings.ToLower(filepath.Ext(filename))// 2. 创建临时文件。这是“阅后即焚”的载体。// os.CreateTemp 会在系统临时目录生成唯一文件名如 /tmp/inkwords-parse-123456.pdftempFile,err:os.CreateTemp(,inkwords-parse-*ext)iferr!nil{return,fmt.Errorf(failed to create temp file: %w,err)}// 3. 核心安全措施使用 defer 确保函数退出时无论成功或失败一定执行清理。deferfunc(){tempFile.Close()// 关闭文件句柄os.Remove(tempFile.Name())// 删除临时文件}()// 4. 将上传的数据流拷贝到临时文件size,err:io.Copy(tempFile,src)iferr!nil{return,fmt.Errorf(failed to write to temp file: %w,err)}// 5. 确保数据从缓冲区完全写入磁盘避免后续读取时数据不完整iferr:tempFile.Sync();err!nil{return,fmt.Errorf(failed to sync temp file: %w,err)}// 6. 将文件指针重置回开头因为 io.Copy 后指针在文件末尾if_,err:tempFile.Seek(0,0);err!nil{return,fmt.Errorf(failed to seek temp file: %w,err)}// 7. 根据文件后缀分发给对应的具体解析函数switchext{case.pdf:returnp.parsePDF(tempFile,size)case.md,.markdown,.txt:returnp.parsePlainText(tempFile)case.docx:returnp.parseDocx(tempFile)default:return,fmt.Errorf(unsupported file extension: %s,ext)}}生活化比喻这个过程就像一个高效的快递分拣中心。你用户送来一个包裹数据流上面贴着标签“PDF”文件名。分拣中心Parse方法准备了一个临时货架临时文件来存放它。包裹被放上货架后系统根据标签决定将它送往“PDF处理线”。处理线工人parsePDF函数拆开包裹取出里面的信件文本内容。关键一步信件被取出后系统立即销毁临时货架和空包裹defer删除文件绝不保留用户隐私。分格式解析各显神通总调度完成后文件会被路由到三个具体的解析函数。它们分别针对不同格式调用了专门的第三方库。1. 解析 PDF 文件PDF 解析使用了github.com/ledongthuc/pdf这个纯 Go 实现的库。它的优点是无需依赖外部的 PDF 渲染引擎如 Poppler。func(p*DocParser)parsePDF(file*os.File,sizeint64)(string,error){// 传入已打开的文件指针和文件大小创建PDF阅读器reader,err:pdf.NewReader(file,size)iferr!nil{// 友好地处理常见错误损坏的PDF文件ifstrings.Contains(err.Error(),missing %%EOF){return,fmt.Errorf(解析失败该文件似乎已损坏或不是标准的PDF格式)}return,fmt.Errorf(解析 PDF 失败: %w,err)}varbuf bytes.Buffer// 调用库方法获取纯文本内容返回的是一个 io.Readerb,err:reader.GetPlainText()iferr!nil{return,fmt.Errorf(提取 PDF 文本失败: %w,err)}// 将文本读取到缓冲区buf.ReadFrom(b)// 去除首尾空白字符后返回returnstrings.TrimSpace(buf.String()),nil}2. 解析 DOCX 文件DOCX 文件本质是一个 ZIP 压缩包里面包含了 XML 格式的文档内容、样式等。我们使用github.com/nguyenthenguyen/docx库来处理它。这里有个小陷阱该库需要通过文件路径来打开文档而不是我们已经打开的*os.File指针。因此我们需要先获取临时文件的路径 (file.Name())。func(p*DocParser)parseDocx(file*os.File)(string,error){// 通过文件路径打开DOCX文档doc,err:docx.ReadDocxFile(file.Name())iferr!nil{return,fmt.Errorf(failed to open docx file: %w,err)}deferdoc.Close()// 记得关闭文档资源// 获取可编辑对象并提取内容text:doc.Editable().GetContent()// 由于GetContent()可能返回带XML标签的内容我们需要一个简单的清洗函数textstripXMLTags(text)returnstrings.TrimSpace(text),nil}// stripXMLTags 是一个简单的辅助函数用于剥离XML标签funcstripXMLTags(contentstring)string{varbuf bytes.Buffer inTag:false// 标记当前字符是否位于XML标签内部for_,r:rangecontent{ifr{inTagtrue// 遇到‘’进入标签}elseifr{inTagfalse// 遇到‘’离开标签}elseif!inTag{// 只有不在标签内的字符才被保留buf.WriteRune(r)}}returnbuf.String()}3. 解析纯文本文件 (Markdown/TXT)这是最简单的情况我们只需要将文件内容原样读取出来即可。func(p*DocParser)parsePlainText(file*os.File)(string,error){// 确保文件指针在开头if_,err:file.Seek(0,0);err!nil{return,fmt.Errorf(failed to seek file: %w,err)}varbuf bytes.Buffer// 将文件内容直接拷贝到缓冲区if_,err:io.Copy(buf,file);err!nil{return,fmt.Errorf(failed to read plain text file: %w,err)}returnstrings.TrimSpace(buf.String()),nil}如何在你的项目中实践如果你想在自己的 Go 项目中集成类似的多格式文档解析功能可以遵循以下步骤初始化项目创建一个新的 Go 模块。mkdirmy-doc-parsercdmy-doc-parser go mod init github.com/yourname/my-doc-parser安装依赖引入本文提到的两个第三方库。go get github.com/ledongthuc/pdf go get github.com/nguyenthenguyen/docx复制代码将本章分析的DocParser相关代码包括Parser接口、DocParser结构体及其所有方法复制到你的项目文件中例如parser/doc_parser.go。编写测试创建一个简单的main.go来测试功能。packagemainimport(fmtlogospath/filepath// 假设你的parser包放在 ./parser 目录下github.com/yourname/my-doc-parser/parser)funcmain(){dp:parser.NewDocParser()// 测试一个PDF文件pdfFile,_:os.Open(sample.pdf)deferpdfFile.Close()text,err:dp.Parse(pdfFile,filepath.Base(pdfFile.Name()))iferr!nil{log.Fatal(PDF解析失败:,err)}fmt.Printf(解析成功前100字符\n%.100s...\n,text)}运行测试go run main.go总结与展望本章我们深入探讨了 InkWords 的文档解析核心 ——DocParser。它通过清晰的接口设计、统一的处理流程、“阅后即焚”的安全策略以及对多种格式的适配为系统提供了稳定可靠的数据输入能力。核心要点回顾接口化设计通过Parser接口实现了解析器的可扩展和易用性。策略模式Parse方法作为调度中心根据文件后缀将任务分发给不同的具体解析器。安全第一使用临时文件和defer语句确保用户文件内容在处理后被立即销毁。依赖优秀库合理利用成熟的开源库 (ledongthuc/pdf,nguyenthenguyen/docx) 处理复杂格式避免重复造轮子。DocParser目前完美解决了单篇本地文档的解析问题。然而在实际的技术学习场景中我们更常面对的是完整的、结构复杂的Git 代码仓库。如何自动克隆一个仓库分析其项目结构并智能地将其拆解成一系列连贯的技术博客呢下期预告Git 仓库抓取与内容提取在下一章我们将揭开 InkWords 更强大的能力。你将看到系统如何实现GitFetcher从 GitHub/Gitee 等平台抓取源码如何过滤无关文件以及如何为庞大的项目评估复杂度并规划出系列博客的生成大纲。敬请期待

更多文章