根据 npm 下载数据,pdf.js(pdfjs-dist)周下载量超过 300 万次,是前端生态中使用最广泛的 PDF 处理库。在 RAG(检索增强生成)系统爆发的 2026 年,PDF 文本提取已经成为知识库构建的第一步——但大多数开发者对 pdf.js 的理解还停留在"渲染一下"的层面。本文将从底层 API 出发,系统讲解如何在浏览器端完成 PDF 解析、文本提取、页面渲染、搜索定位,并最终集成到 RAG 管道中。
🔧 一、pdf.js 核心 API 与基础解析
1.1 初始化与文档加载
pdf.js 的核心入口是 getDocument() 方法,它返回一个 PDFDocumentProxy 对象。加载方式支持 URL、ArrayBuffer、Base64 三种,选择取决于你的数据来源。
// 从 URL 加载 PDF(最常见场景)
import * as pdfjsLib from 'pdfjs-dist'
// ⚠️ 必须设置 Worker 路径,否则会在主线程解析导致卡顿
pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.worker.min.mjs`
async function loadPDFFromURL(url) {
const loadingTask = pdfjsLib.getDocument(url)
const pdf = await loadingTask.promise
console.log(`PDF 共 ${pdf.numPages} 页`)
return pdf
}
// 从 ArrayBuffer 加载(适合用户上传场景)
async function loadPDFFromFile(file) {
const arrayBuffer = await file.arrayBuffer()
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer })
const pdf = await loadingTask.promise
return pdf
}
// 从 Base64 加载(适合 API 返回场景)
async function loadPDFFromBase64(base64String) {
const raw = atob(base64String)
const uint8Array = new Uint8Array(raw.length)
for (let i = 0; i < raw.length; i++) {
uint8Array[i] = raw.charCodeAt(i)
}
const loadingTask = pdfjsLib.getDocument({ data: uint8Array })
return await loadingTask.promise
}
⚠️ **警告:**永远不要在不设置
workerSrc的情况下加载大 PDF。pdf.js 默认在主线程运行解析,超过 5 页的 PDF 就会导致明显的 UI 卡顿。
1.2 获取文档元数据
PDF 文件包含丰富的元数据——标题、作者、创建时间、关键词等。这些信息对于 RAG 系统的文档分类和索引非常有价值。
// 提取 PDF 元数据
async function extractMetadata(pdf) {
const metadata = await pdf.getMetadata()
const { info, metadata: xmpMetadata } = metadata
return {
title: info.Title || '未知标题',
author: info.Author || '未知作者',
subject: info.Subject || '',
creator: info.Creator || '',
producer: info.Producer || '',
creationDate: info.CreationDate || '',
modDate: info.ModDate || '',
pageCount: pdf.numPages,
// XMP 元数据(如果存在)
keywords: xmpMetadata
? xmpMetadata.get('dc:subject') || ''
: ''
}
}
| 属性 | 类型 | 说明 | RAG 场景用途 |
|---|---|---|---|
Title |
string | 文档标题 | 索引标题 |
Author |
string | 作者 | 来源标注 |
CreationDate |
string | 创建时间 | 时间排序 |
Keywords |
string | 关键词 | 初始标签 |
numPages |
number | 总页数 | 分批策略 |
📄 二、文本提取与高级处理
2.1 逐页提取文本
文本提取是 PDF 解析最核心的功能。pdf.js 的 getTextContent() 返回一个包含 items 数组的对象,每个 item 代表一个文本片段(TextItem),包含文本内容、位置坐标和字体信息。
// 提取单页文本
async function extractPageText(pdf, pageNumber) {
const page = await pdf.getPage(pageNumber)
const textContent = await page.getTextContent()
// textContent.items 中每个 item 包含:
// - str: 文本内容
// - transform: 位置变换矩阵 [scaleX, skewX, skewY, scaleY, translateX, translateY]
// - width, height: 文本尺寸
// - fontName: 字体名称
const textItems = textContent.items
const lines = []
let currentLine = ''
let lastY = null
for (const item of textItems) {
const y = item.transform[5] // translateY
// Y 坐标变化超过阈值视为换行
if (lastY !== null && Math.abs(y - lastY) > 5) {
if (currentLine.trim()) {
lines.push(currentLine.trim())
}
currentLine = item.str
} else {
// 同一行内拼接,添加适当的空格
if (currentLine && item.str && !currentLine.endsWith(' ') && !item.str.startsWith(' ')) {
currentLine += ' '
}
currentLine += item.str
}
lastY = y
}
if (currentLine.trim()) {
lines.push(currentLine.trim())
}
return lines.join('\n')
}
// 提取全文(所有页面)
async function extractFullText(pdf) {
const pages = []
for (let i = 1; i <= pdf.numPages; i++) {
const pageText = await extractPageText(pdf, i)
pages.push({ page: i, text: pageText })
}
return pages
}
💡 提示:
getTextContent()返回的文本顺序是按 PDF 内部结构排列的,不一定是阅读顺序。对于多栏排版的 PDF,需要根据坐标进行重排序。
2.2 处理多栏布局与文本重排
学术论文和技术文档经常采用双栏排版。直接拼接 getTextContent() 的结果会得到混乱的文本。解决方案是根据 X 坐标将文本分为左栏和右栏,分别提取后再合并。
// 处理双栏 PDF 的文本提取
async function extractTwoColumnText(pdf, pageNumber) {
const page = await pdf.getPage(pageNumber)
const textContent = await page.getTextContent()
const viewport = page.getViewport({ scale: 1.0 })
const pageWidth = viewport.width
const leftColumn = []
const rightColumn = []
const midPoint = pageWidth / 2
for (const item of textContent.items) {
const x = item.transform[4] // translateX
if (x < midPoint) {
leftColumn.push(item)
} else {
rightColumn.push(item)
}
}
// 分别按 Y 坐标排序并拼接
const sortAndJoin = (items) => {
items.sort((a, b) => b.transform[5] - a.transform[5]) // Y 从上到下
return items.map(i => i.str).join(' ')
}
return {
left: sortAndJoin(leftColumn),
right: sortAndJoin(rightColumn),
full: sortAndJoin(leftColumn) + '\n' + sortAndJoin(rightColumn)
}
}
⚠️ **警告:**扫描版 PDF(图片型)的
getTextContent()会返回空数组。需要使用 OCR 方案(如 Tesseract.js)进行文本识别。检测方法:如果某页的textContent.items.length === 0且page.getViewport()有尺寸,说明该页是扫描件。
2.3 文本搜索与定位
在 PDF 中搜索文本并定位到具体位置,是构建文档查看器的核心功能。
// PDF 全文搜索,返回匹配位置
async function searchTextInPDF(pdf, keyword) {
const results = []
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i)
const textContent = await page.getTextContent()
for (const item of textContent.items) {
const index = item.str.toLowerCase().indexOf(keyword.toLowerCase())
if (index !== -1) {
// 计算匹配文本在视口中的位置
const viewport = page.getViewport({ scale: 1.5 })
const tx = pdfjsLib.Util.transform(
viewport.transform,
item.transform
)
results.push({
page: i,
text: item.str,
position: {
x: tx[4],
y: tx[5],
width: item.width * viewport.scale,
height: item.height * viewport.scale
}
})
}
}
}
return results
}
🎨 三、页面渲染与可视化
3.1 Canvas 渲染
将 PDF 页面渲染到 Canvas 是最常见的可视化需求。关键是正确处理分辨率和缩放。
// 高清渲染 PDF 页面到 Canvas
async function renderPageToCanvas(pdf, pageNumber, canvas, options = {}) {
const {
scale = 2.0, // 2x 缩放适配 Retina 屏幕
rotation = 0, // 旋转角度(0/90/180/270)
backgroundColor = '#ffffff'
} = options
const page = await pdf.getPage(pageNumber)
const viewport = page.getViewport({ scale, rotation })
// 设置 Canvas 尺寸
canvas.width = viewport.width
canvas.height = viewport.height
const ctx = canvas.getContext('2d')
ctx.fillStyle = backgroundColor
ctx.fillRect(0, 0, canvas.width, canvas.height)
const renderContext = {
canvasContext: ctx,
viewport: viewport,
// 可选:启用文本层(用于文本选择)
enableWebGL: true, // 启用 WebGL 加速(大页面提升明显)
}
const renderTask = page.render(renderContext)
// 支持取消渲染(页面切换时有用)
return {
promise: renderTask.promise,
cancel: () => renderTask.cancel()
}
}
| 缩放倍数 | 分辨率效果 | 文件大小影响 | 推荐场景 |
|---|---|---|---|
| 1.0 | 标清(72 DPI) | 最小 | 缩略图预览 |
| 1.5 | 中清(108 DPI) | 适中 | 移动端展示 |
| 2.0 | 高清(144 DPI) | 较大 | 桌面端查看(推荐) |
| 3.0 | 超清(216 DPI) | 最大 | 打印预览 |
💡 提示:
scale = 2.0是性价比最高的选择。在 Retina 屏幕上清晰可辨,同时 Canvas 内存占用可控。超过 3.0 会导致大页面(如 A0 图纸)内存溢出。
3.2 渲染缩略图与虚拟滚动
当需要展示 PDF 所有页面的缩略图时,逐页渲染会导致大量内存占用。使用虚拟滚动 + 按需渲染是最佳方案。
// 生成 PDF 缩略图(低分辨率,节省内存)
async function generateThumbnail(pdf, pageNumber, maxWidth = 200) {
const page = await pdf.getPage(pageNumber)
const unscaledViewport = page.getViewport({ scale: 1 })
const scale = maxWidth / unscaledViewport.width
const viewport = page.getViewport({ scale })
const canvas = document.createElement('canvas')
canvas.width = viewport.width
canvas.height = viewport.height
const ctx = canvas.getContext('2d')
await page.render({
canvasContext: ctx,
viewport
}).promise
// 转为 Blob 以便后续管理内存
return new Promise((resolve) => {
canvas.toBlob((blob) => {
resolve({
url: URL.createObjectURL(blob),
width: viewport.width,
height: viewport.height,
revoke: () => URL.revokeObjectURL(url)
})
}, 'image/png', 0.8)
})
}
🔒 四、加密 PDF 与错误处理
4.1 处理密码保护的 PDF
pdf.js 支持打开密码保护的 PDF,密码通过 password 参数传入。
// 打开加密 PDF
async function loadEncryptedPDF(url, password) {
try {
const loadingTask = pdfjsLib.getDocument({
url,
password, // 所有者密码或用户密码
})
const pdf = await loadingTask.promise
return { success: true, pdf }
} catch (error) {
if (error.name === 'PasswordException') {
if (error.code === 1) {
// 需要密码但未提供
return { success: false, needPassword: true, message: '此 PDF 需要密码' }
} else if (error.code === 2) {
// 密码错误
return { success: false, wrongPassword: true, message: '密码错误' }
}
}
return { success: false, message: `加载失败: ${error.message}` }
}
}
4.2 常见错误类型与处理策略
| 错误类型 | error.name |
原因 | 处理策略 |
|---|---|---|---|
| 密码缺失 | PasswordException (code=1) |
PDF 需要密码 | 弹窗让用户输入 |
| 密码错误 | PasswordException (code=2) |
密码不匹配 | 提示重试 |
| 文件损坏 | InvalidPDFException |
文件传输不完整 | 提示重新上传 |
| 格式不支持 | MissingPDFException |
非 PDF 文件 | 文件类型校验 |
| 网络错误 | UnexpectedResponseException |
404/500 | 重试机制 |
🤖 五、集成到 RAG 管道
5.1 文本分块策略
将 PDF 全文提取出来后,需要切分为合适大小的文本块(Chunk)才能送入向量数据库。分块策略直接影响检索质量。
// PDF 文本分块器
function chunkText(text, options = {}) {
const {
chunkSize = 500, // 每块最大字符数
overlap = 50, // 块之间的重叠字符数
separator = '\n' // 优先在换行处切分
} = options
const chunks = []
const paragraphs = text.split(separator)
let currentChunk = ''
for (const paragraph of paragraphs) {
if ((currentChunk + paragraph).length > chunkSize && currentChunk) {
chunks.push(currentChunk.trim())
// 保留末尾 overlap 个字符作为上下文重叠
currentChunk = currentChunk.slice(-overlap) + paragraph
} else {
currentChunk += (currentChunk ? separator : '') + paragraph
}
}
if (currentChunk.trim()) {
chunks.push(currentChunk.trim())
}
return chunks.map((content, index) => ({
id: `chunk-${index}`,
content,
startIndex: text.indexOf(content.slice(0, 20)),
tokenEstimate: Math.ceil(content.length / 2) // 粗略估算:中文约 2 字符/token
}))
}
| 分块策略 | 块大小 | 重叠 | 适用场景 | 检索精度 |
|---|---|---|---|---|
| 小块 | 200-300 字符 | 30 字符 | 精准问答 | ⭐⭐⭐⭐⭐ |
| 中块 | 500-800 字符 | 50 字符 | 通用 RAG(推荐) | ⭐⭐⭐⭐ |
| 大块 | 1000-1500 字符 | 100 字符 | 上下文理解 | ⭐⭐⭐ |
| 按段落 | 自然段落 | 0 | 结构化文档 | ⭐⭐⭐⭐ |
⚠️ **警告:**块大小和重叠比例需要根据你的 Embedding 模型的上下文窗口调整。OpenAI
text-embedding-3-small的上下文是 8192 token,每个块不应超过 2000 token。
5.2 完整的 PDF → RAG 管道
将以上所有技术组合起来,构建一个端到端的 PDF 文档处理管道:
// 完整的 PDF → RAG 处理管道
class PDFRAGPipeline {
constructor(options = {}) {
this.chunkSize = options.chunkSize || 500
this.chunkOverlap = options.chunkOverlap || 50
this.maxPages = options.maxPages || 1000
}
async process(file) {
// 第一步:加载 PDF
const arrayBuffer = await file.arrayBuffer()
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise
// 第二步:提取元数据
const metadata = await this.extractMetadata(pdf)
// 第三步:逐页提取文本
const pageTexts = []
const totalPages = Math.min(pdf.numPages, this.maxPages)
for (let i = 1; i <= totalPages; i++) {
const page = await pdf.getPage(i)
const textContent = await page.getTextContent()
const pageText = textContent.items
.map(item => item.str)
.join(' ')
.replace(/\s+/g, ' ')
.trim()
if (pageText) {
pageTexts.push({ page: i, text: pageText })
}
}
// 第四步:合并文本并分块
const fullText = pageTexts.map(p => p.text).join('\n')
const chunks = chunkText(fullText, {
chunkSize: this.chunkSize,
overlap: this.chunkOverlap
})
// 第五步:附加来源信息
return {
metadata,
chunks: chunks.map(chunk => ({
...chunk,
source: metadata.title,
author: metadata.author,
file: file.name
})),
stats: {
totalPages: pdf.numPages,
processedPages: pageTexts.length,
totalChunks: chunks.length,
totalCharacters: fullText.length,
avgChunkSize: Math.round(fullText.length / chunks.length)
}
}
}
async extractMetadata(pdf) {
const { info } = await pdf.getMetadata()
return {
title: info.Title || '未知标题',
author: info.Author || '未知作者',
pageCount: pdf.numPages
}
}
}
// 使用示例
const pipeline = new PDFRAGPipeline({ chunkSize: 500, chunkOverlap: 50 })
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0]
const result = await pipeline.process(file)
console.log(`处理完成:${result.stats.totalChunks} 个文本块`)
console.log(`来源:${result.metadata.title} (${result.metadata.author})`)
// 将 result.chunks 送入 Embedding 模型 → 向量数据库
})
💡 六、性能优化与注意事项
6.1 性能优化要点
- ✅ 始终使用 Web Worker:pdf.js Worker 避免主线程阻塞,对大文件尤为重要
- ✅ 按需加载页面:不要一次性
Promise.all加载所有页面,使用队列控制并发 - ✅ 及时释放内存:渲染完的 Canvas 调用
canvas.width = 0释放 GPU 内存 - ✅ 使用
getPage()缓存:pdf.js 内部会缓存已解析的页面对象 - ❌ 避免在主线程解析超过 50 页的 PDF:会导致明显卡顿
- ❌ 不要忽略
renderTask.cancel():页面切换时必须取消未完成的渲染
📌 **记住:**pdf.js 的
getDocument()会将整个 PDF 文件读入内存。对于 100MB 以上的大文件,考虑使用 Range Request 分片加载(range: { length: 65536 })。
6.2 浏览器兼容性
| 功能 | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| Canvas 渲染 | ✅ | ✅ | ✅ | ✅ |
| WebGL 加速 | ✅ | ✅ | ⚠️ 部分 | ✅ |
| TextContent | ✅ | ✅ | ✅ | ✅ |
| Worker 加载 | ✅ | ✅ | ✅ | ✅ |
| Stream 加载 | ✅ | ✅ | ✅ | ✅ |
💡 **提示:**Safari 16 以下版本对 pdf.js 的 WebGL 渲染支持不完整,建议在生产环境中做特性检测,降级到 Canvas 2D 渲染。
✅ 总结
在浏览器端解析 PDF 不再是"能做到"的问题,而是"怎么做得更好"的问题。pdf.js 提供了完整的解析和渲染能力,关键在于:
- 文本提取:
getTextContent()是基础,但多栏布局需要坐标重排序 - 渲染优化:2x 缩放 + WebGL 加速 + 虚拟滚动是最佳组合
- RAG 集成:文本分块策略直接影响检索质量,推荐 500 字符 + 50 字符重叠
- 错误处理:密码保护、文件损坏、格式错误都需要友好处理
🔗 相关工具推荐
- 📦 pdfjs-dist — Mozilla 官方 PDF 解析库
- 📦 react-pdf — 基于 pdf.js 的 React 组件
- 📦 tesseract.js — 浏览器端 OCR,处理扫描版 PDF
- 📦 pdf-lib — PDF 创建与编辑(非解析)
- 🛠️ jsjson.com 在线 JSON 工具 — JSON 格式化、转换、验证一站式工具箱