ARTICLE · 1051897
PDF 这么复杂,Go 是怎么把它搞定的?
但对于程序员来说,PDF 其实是一个挺麻烦的东西。
里面有:
文字图片字体页面对象压缩数据加密签名附件
更麻烦的是,不同软件生成的 PDF,还可能存在各种奇奇怪怪的问题。
所以我最近在 GitHub 上发现一个挺有意思的 Go 项目:pdfcpu。
它干的事情非常简单:用 Go 处理 PDF。
但真正有意思的是,它不是简单调用一个现成的 PDF 程序,而是自己实现了一套 PDF 处理能力。
01 一个 Go 项目,能处理多少 PDF 操作?
先看看它能干什么。
安装:
go install github.com/pdfcpu/pdfcpu/cmd/pdfcpu@latest 然后:
查看版本 pdfcpu version验证 pdfcpu validate test.pdf合并 pdfcpu merge merged.pdf a.pdf b.pdf压缩 pdfcpu optimize input.pdf output.pdf加密 pdfcpu encrypt input.pdf output.pdf
除此之外,还支持:
拆分提取图片提取字体添加水印数字签名附件管理PDF 信息查看
也就是说,它不是一个简单的“PDF 合并工具”,而是一套完整的 PDF 工具链。
02 PDF 为什么难?
如果你以为 PDF 就是:
第一页第二页第三页
那就低估它了,PDF 内部更像这样:
├── Catalog├── Pages│ ├── Page│ ├── Page│ └── Page├── Fonts├── Images├── Streams├── Metadata├── Encryption└── Cross Reference
页面可能引用对象,对象可能引用另外一个对象,文字可能放在 Stream 里,图片可能经过压缩,最后还有 xref 来记录对象的位置。
所以 PDF 解析,本质上是在做:
PDF文件↓解析↓理解对象↓建立关系↓修改↓重新生成PDF
这已经不是简单的:os.ReadFile(”test.pdf”)了。
03 Go 是怎么处理这种复杂格式的?
Go 有一个非常适合做这件事情的特点:类型系统简单,而且特别适合把复杂的数据结构抽象出来。
比如一个简化版的 PDF 对象模型,可以想象成:
type Object interface {Type() string}type Integer struct {Value int64}type String struct {Value string}type Array []Objecttype Dict map[string]Object
PDF 里面各种对象:
IntegerStringArrayDictionaryStream
就可以映射成 Go 的类型,这样程序处理 PDF 时,就不是在操作一堆:[]byte
而是在操作一个真正的 PDF 对象模型。
这也是写解析器时非常重要的一步:先把复杂的文件格式变成程序能够理解的数据结构。
04 真正麻烦的,是“坏 PDF”
正常 PDF 并不可怕,最麻烦的是:
损坏的 PDF超大的 PDF结构异常的 PDF恶意构造的 PDF
假设你的服务器提供一个:POST /upload
用户上传 PDF,你直接:
data, _ := io.ReadAll(r.Body) 然后开始解析。
如果对方上传一个超大文件怎么办?
如果 PDF 解压之后突然膨胀几十倍怎么办?
如果里面有非常深的对象引用怎么办?
如果图片像素数量巨大怎么办?
这时候 PDF 解析器就不只是“功能代码”了。
它还必须考虑:
内存CPU递归深度对象数量解压数据大小图片尺寸
这也是 pdfcpu 近几个版本比较值得关注的地方。
它逐渐增加了各种 parser/resource limits,用来限制:
Stream 大小解码后的数据大小图片像素对象数量xref 数量递归深度
目的就一个:别让一个 PDF 把你的服务搞死。
05 这其实很像我们平时做安全产品
看到这里,是不是有点熟悉?一个文件进来:
用户输入↓解析↓识别↓处理
安全产品也是一样。
漏洞扫描器面对:
HTTPHTMLJSONXMLJavaScript证书文件协议
同样需要处理大量:
异常输入畸形数据超大数据恶意数据
所以一个成熟的解析器,最重要的能力往往不是:“正常情况下能不能解析。”
而是:“面对一个故意搞你的输入,还能不能安全退出。”
这一点,pdfcpu 很值得研究。
06 为什么我觉得这个项目值得 Go 程序员看看?
因为很多人提到 Go,想到的还是:
GinMySQLRedisCRUDREST API
但 Go 真正有意思的地方远不止这些,你看 GitHub 上一些经典 Go 项目:
Caddy Web ServerPrometheus 监控rclone 文件同步Hugo 静态网站PocketBase 后端pdfcpu PDF
它们有一个共同点:都不是简单写几个 API,而是在解决一个明确的问题。
而 pdfcpu 给我的最大感受就是:PDF 这么复杂,Go 一样可以啃下来。
07 一个很有意思的启发
我们平时写项目,很容易把架构想复杂。
一个需求来了:PDF处理
第一反应可能是:
Go服务+Python服务+第三方PDF服务+消息队列+对象存储
但其实也可以先看看:Go + pdfcpu能不能解决?
如果能,那为什么不能先这么做?
这其实也是很多优秀开源项目给我们的启发:
不是所有问题都需要分布式。
不是所有功能都需要一个独立服务。
更不是所有项目都需要一堆基础设施。
最后
我越来越喜欢研究这种 GitHub 上的 Go 项目。
不是为了记住:pdfcpu 有哪些 API
而是想看看:别人是怎么用 Go,把一个看起来很复杂的问题解决掉的。
PDF 只是一个例子。
下一次遇到一个复杂问题,也许可以先问自己一句:
“如果用 Go 自己做,会不会其实没有想象中那么难?”
这可能才是这些开源项目最值得我们学习的地方。