最近和不少做业务系统的 Java 开发者聊文档处理,有三类问题反复出现:
- 动态生成 PDF,数据一长就跨页乱版,试过几款开源工具都没解决
- 积累多年的行业资料,想做信息抽取,变成企业自己的知识库
- PDF 能力要接进现有业务系统,不知道选 SDK 还是 API
下面按这三个问题展开,附上可以参考的代码。
先说结论
- 跨页乱版多半是"按数据刚好一页设计的版面 + 不固定长度的数据"造成的,选择支持自动分页、表头重复的生成方式,比事后调样式更省事。
- ComPDF 的 Java 生成 SDK 有两条路:HTML 模板 + JSON 适合版式固定的单据报告,Layout API 适合结构动态、需要精确控制版面的文档。
- 做知识库时,先把文档解析成 Markdown/JSON,再按标题层级切块,表格整体保留,页眉页脚和水印先过滤掉。
- 选型时先问部署方式和数据流向,再比较效果。
一、动态生成 PDF:数据一长,版面怎么不乱
1. 为什么一跨页就乱
业务系统按模板生成报告、单据、清单时,明细行数和文本长度都不固定。常见的乱版现象有:
- 表格行在页尾被从中间截断
- 表头只出现在第一页,后面几页对不上列
- 用固定高度或绝对定位摆放内容,数据一长就溢出或重叠
- 页脚、页码被正文挤压
这些问题的共同点是:版面是按"数据刚好一页"设计的。更稳妥的思路是把分页交给排版引擎,模板里只描述内容和结构,不去写死位置和高度。
2. 两条路线怎么选
ComPDF 的 PDF 生成方案,Java 版本提供两条路线:
| 路线 | 思路 | 适合场景 |
|---|---|---|
| HTML 模板 + JSON | 用 HTML 设计模板,JSON 提供数据,渲染后转 PDF | 版式相对固定、由设计或前端人员维护模板的报告和单据 |
| Layout API | 用代码直接构建文档,由排版引擎自动分页 | 需要精确控制版面、动态生成复杂结构化文档 |
官方文档对 Layout API 的说明是:支持自动分页与内容重排,多页表格可以重复表头和表尾。
一个简单的选择办法:模板由非开发人员维护、版式固定,选 HTML;文档结构由业务数据决定(比如章节、表格数量都不固定),或者需要和现有 Java 业务逻辑深度结合,选 Layout API。
3. 方案 A:HTML 模板 + JSON
模板里用{{ }}取变量,用注释包裹的for循环渲染明细行:
html
<table width="100%" style="border-collapse: collapse;"> <!-- {% set totalprice = 0 %} --> <!-- {% for item in items %} --> <tr> <td>{{ item.index }}</td> <td>{{ item.product }}</td> <td>{{ item.price }}</td> </tr> <!-- {% set totalprice = totalprice + item.price %} --> <!-- {% endfor %} --> <tr> <td colspan="2">Total:</td> <td>{{ round(totalprice, 2) }}</td> </tr> </table>数据是标准 JSON:
json
{ "billNo": "2345678901", "items": [ { "index": 1, "product": "Item A", "price": 800 }, { "index": 2, "product": "Item B", "price": 400 } ] }Java 端一步完成渲染和转换:
java
String templatePath = "/data/template.html"; String jsonPath = "/data/data.json"; String outPdfPath = "/data/out.pdf"; CHtmlConverter.CErrorCode code = CHtmlConverter.RenderAndConvertToPdf(templatePath, jsonPath, outPdfPath);模板和 JSON 也可以用字符串、InputStream或File传入,输出可以是文件路径或OutputStream,方便直接写进 Web 响应。页面尺寸、字体目录等参数通过CConverterProperties配置,模板里支持变量、if判断、for循环、四则运算和常用函数。
4. 方案 B:Layout API 构建长表格
对于行数不确定的明细表,可以直接用代码构建,分页由排版引擎处理:
java
import CPDFGeneration.layout.*; public class LongTableDemo { public static void main(String[] args) { CDocument document = new CDocument(); CPage page = document.createPage(595, 842); // A4 CDiv div = page.createDivElement(); // 三列,按百分比分配宽度 CUnitValue[] widths = new CUnitValue[] { CUnitValue.CreatePercentValue(20), CUnitValue.CreatePercentValue(50), CUnitValue.CreatePercentValue(30) }; CTable table = div.createTableElement(widths); table.useAllAvailableWidth(); // 表头 table.startNewHeaderRow(); table.createHeaderCell(1, 1).createTextElement("No."); table.createHeaderCell(1, 1).createTextElement("Description"); table.createHeaderCell(1, 1).createTextElement("Amount"); // 明细行:数量不固定,由引擎自动分页 for (int i = 1; i <= 200; i++) { table.startNewRow(); table.createCell(1, 1).createTextElement(String.valueOf(i)); table.createCell(1, 1).createTextElement("Item " + i); table.createCell(1, 1).createTextElement(String.format("%.2f", i * 12.5)); } // 表尾 table.startNewFooterRow(); table.createFooterCell(1, 1).createTextElement("Total"); table.createFooterCell(1, 1).createTextElement(""); table.createFooterCell(1, 1).createTextElement("25125.00"); document.saveToFile("long-table.pdf"); } }批量创建元素时,官方建议预先创建可复用的样式(createStyle()),再统一addStyle,避免逐个设置;输出大文件时用流式输出,而不是先写进内存。
5. 评估时怎么验证
不要只用正常数据测,拿自己的真实模板配上极端数据来试:
- 明细从 10 行、100 行到 1000 行,看分页是否稳定
- 单元格放超长文本,看是否被截断或撑破版面
- 表头是否在每一页重复,表尾是否只出现在末页
- 页脚和页码是否被内容挤压
- 包含中文时,字体是否正常显示:HTML 方案通过
CFontProvider添加字体目录,提前准备好字体文件 - 运行环境:官网文档标注 Java 生成 SDK 支持 Linux,选型前先确认自己的部署环境
二、行业资料结构化:从"一堆文件"到可用的数据
合同、报告、技术资料和扫描件沉淀多年,想做成企业知识库,通常卡在三处:扫描件需要 OCR,版面复杂(多栏、表格、页眉页脚),解析结果必须结构化才能被检索和问答使用。
一条常见的处理链路是:
文档 → 版面分析 → OCR → 结构还原(阅读顺序、标题层级、表格) → 输出 Markdown / JSON → 切块 → 向量化 → 入库ComPDF AI 的文档解析引擎基于版面分析,官网列出的能力里,和知识库建设最相关的是:
- 阅读顺序还原:处理多栏、边注等复杂排版
- 表格识别:支持合并单元格、无边框表格和跨页表格重建
- 标题识别:识别 H1–H6 层级,用于构建文档大纲和 RAG 切块
- 页眉、页脚、印章、水印检测:提取关键内容的同时过滤页面噪声
官网提供了开源的 Python SDK docslight-lite,云端调用只需几行:
python
from docslight import DocSlight client = DocSlight( mode="cloud", api_key="your api key", base_url="https://api-server.compdf.com" ) file_path = "/Users/yourname/Documents/invoice.pdf" result = client.parse(file_path) print(result.to_markdown())拿到 Markdown 之后:按标题切块
做 RAG 时,按固定字数硬切容易把一段完整的论述拦腰截断。更稳的做法是按标题层级切,让每个片段语义完整,并把标题保留为元数据,便于检索和溯源:
python
import re def chunk_by_heading(markdown: str, max_level: int = 2): """按 1~max_level 级标题切块,保留标题作为元数据""" heading = re.compile(rf"^(#{{1,{max_level}}})\s+(.+)$", re.MULTILINE) matches = list(heading.finditer(markdown)) chunks = [] # 第一个标题之前的内容(前言、摘要等) if matches and matches[0].start() > 0: preface = markdown[:matches[0].start()].strip() if preface: chunks.append({"title": "(preface)", "level": 0, "text": preface}) for i, m in enumerate(matches): end = matches[i + 1].start() if i + 1 < len(matches) else len(markdown) chunks.append({ "title": m.group(2).strip(), "level": len(m.group(1)), "text": markdown[m.start():end].strip(), }) return chunks chunks = chunk_by_heading(result.to_markdown(), max_level=2) for c in chunks: print(c["level"], c["title"], len(c["text"]))几点实践建议:
- 表格整体保留:不要把表格拆成纯文本或按行切开,保留为 Markdown 表格或 JSON,检索时才能还原行列关系。
- 超长章节再二次切分:某个标题下内容特别长时,再按段落或长度细分,并沿用父标题作为上下文。
- 元数据要带上:来源文件名、页码、标题路径,问答时才能给出可追溯的出处。
- 注意代码块:上面的示例没有处理围栏代码块里以
#开头的注释行,含代码的文档在生产环境中要先跳过围栏代码块,否则会误判成标题。
部署方式怎么选
对能源、政务、档案这类对数据安全要求高的行业,部署方式往往比解析效果更先被问到:
| 方式 | 特点 | 适合 |
|---|---|---|
| 云 API | 接入最快,按用量计费,支持 Python、Java、Node.js、Go | 快速验证和中小规模应用 |
| 私有化部署 | Docker 容器化,数据留在自有环境,支持 GPU 加速 | 大规模处理和高安全要求 |
| 定制服务 | 针对特定文档类型做数据标注和模型调优 | 非标准文档和更高精度要求 |
一个务实的做法是:先用云 API 拿自己的真实文档做验证,效果确认后再评估是否需要私有化部署。
三、集成选型:SDK、云 API 还是 AI
把 PDF 能力放进 OA、ERP、档案系统或 SaaS 平台时,通常先要回答选哪种接入方式:
| 需求场景 | 对应方向 |
|---|---|
| 在产品中嵌入 PDF 查看、编辑、批注、表单、电子签名 | ComPDF SDK |
| 服务端批量转换、生成、页面处理,流程自动化 | ComPDF Cloud(云 API) |
| 复杂文档解析、关键信息抽取、知识库和 RAG 数据准备 | ComPDF AI |
三者可以组合:SDK 负责界面交互,云 API 承接批处理,AI 处理解析与抽取。
选型自查清单:
- 技术栈和平台是否覆盖(Java、.NET、Web、移动端),运行环境是否受限
- 转换和生成后的版式、字体、表格还原程度,用真实文档验证
- 大文件、批量和并发场景下的表现
- 部署方式与数据流向,是否满足安全合规要求
- 授权模式是否匹配业务规模
- 文档、示例代码和技术支持是否完善
写在最后
文档处理看起来是个"小功能",但生成、转换、解析、集成每一环都有细节要打磨。核心思路可以总结成一句话:用自己的真实文档和极端数据先做小范围验证,再决定技术路线。无论是长表格跨页排版,还是行业资料的结构化解析,只看正常样例、不测边界情况,是大多数线上问题的根源。
如果这篇对你有帮助,欢迎评论区交流你在实际项目中遇到的类似问题。
参考文档:Java 生成 SDK