如何结合PDFKit与Vision OCR提取带结构化格式的单页PDF表格文本?
如何结合PDFKit与Vision OCR提取带结构化格式的单页PDF表格文本?
我太懂你这种折腾了好几天却卡在半路上的感觉了!PDF的“无结构化”特性确实是个大坑,用PDFKit直接拿string只能得到一堆乱序的文本,完全没法还原表格结构。你想到用Vision OCR是对的方向,但问题确实出在PDF转图片的质量和OCR的结构化处理上,咱们一步步来解决:
一、先搞定PDF转高清图片的问题
你当前的转图代码有几个影响质量的小问题:比如背景色用了lightText(偏灰),和文本对比度不够;渲染分辨率没做针对性提升;还有坐标系的处理可以更简洁。试试下面这个优化后的版本:
func convertPDFToHighResImage(url: URL) -> UIImage? { guard let pdfDocument = PDFDocument(url: url) else { return nil } guard let pdfPage = pdfDocument.page(at: 0) else { return nil } let mediaBox = pdfPage.bounds(for: .mediaBox) // 提高渲染分辨率,这里用2x(视网膜屏级别),可以根据需求调到3x let scale: CGFloat = UIScreen.main.scale * 2 let scaledSize = CGSize(width: mediaBox.width * scale, height: mediaBox.height * scale) // 配置渲染器,优先用广色域,保证颜色对比度 let rendererFormat = UIGraphicsImageRendererFormat() rendererFormat.preferredRange = .extended rendererFormat.scale = scale let renderer = UIGraphicsImageRenderer(size: scaledSize, format: rendererFormat) let image = renderer.image { ctx in // 用纯白色背景,和黑色文本形成最高对比度,利于OCR识别 UIColor.white.setFill() ctx.fill(CGRect(origin: .zero, size: scaledSize)) // 调整坐标系,不用手动翻转,直接用PDF的原生坐标系 ctx.cgContext.concatenate(pdfPage.transform(for: .mediaBox)) pdfPage.draw(with: .mediaBox, to: ctx.cgContext) } return image }
关键改进点:
- 用纯白色背景替代浅灰色,最大化文本与背景的对比度,OCR识别准确率会提升很多
- 基于屏幕分辨率再放大2倍(可按需调整到3x),保证图片足够清晰
- 用
pdfPage.transform(for: .mediaBox)直接处理坐标系,避免手动翻转出错 - 开启广色域渲染,减少颜色失真
二、优化Vision OCR的识别与结构化处理
你当前的OCR请求太基础了,没有开启高精度识别,也没利用位置信息来还原表格结构。咱们调整请求参数,同时通过文本块的位置来重建表格的行列:
func extractStructuredTableText(from image: UIImage, completion: @escaping ([[String]]?) -> Void) { guard let cgImage = image.cgImage else { completion(nil) return } // 配置高精度OCR请求 let request = VNRecognizeTextRequest { request, error in guard error == nil, let observations = request.results as? [VNRecognizedTextObservation] else { completion(nil) return } // 1. 把每个观察结果转换成「文本+位置」的元组,同时转换坐标系 var textWithFrames: [(text: String, frame: CGRect)] = [] let imageSize = CGSize(width: cgImage.width, height: cgImage.height) for observation in observations { guard let topCandidate = observation.topCandidates(1).first else { continue } // 把Vision的左下角原点坐标系,转换成UIKit的左上角原点 let normalizedFrame = observation.boundingBox let convertedFrame = CGRect( x: normalizedFrame.minX * imageSize.width, y: (1 - normalizedFrame.minY - normalizedFrame.height) * imageSize.height, width: normalizedFrame.width * imageSize.width, height: normalizedFrame.height * imageSize.height ) textWithFrames.append((text: topCandidate.string, frame: convertedFrame)) } // 2. 按行分组:把Y坐标接近的文本块归为同一行 textWithFrames.sort { $0.frame.minY > $1.frame.minY } // 从上到下排序 var rows: [[(text: String, frame: CGRect)]] = [] let rowThreshold: CGFloat = 15 // 可根据PDF字体大小调整,判断是否属于同一行的Y差阈值 for textFrame in textWithFrames { if let lastRow = rows.last, let firstInLastRow = lastRow.first { let yDiff = abs(textFrame.frame.minY - firstInLastRow.frame.minY) if yDiff <= rowThreshold { rows[rows.count - 1].append(textFrame) } else { rows.append([textFrame]) } } else { rows.append([textFrame]) } } // 3. 每行内按X坐标排序(从左到右),还原表格列顺序 let structuredTable = rows.map { row in let sortedRow = row.sorted { $0.frame.minX < $1.frame.minX } return sortedRow.map { $0.text } } completion(structuredTable) } // 配置OCR核心参数 request.recognitionLevel = .accurate request.recognitionLanguages = ["en-US"] // 对应PDF的实际语言,中文用["zh-Hans"] request.usesLanguageCorrection = true // 开启语言纠错,提升文本准确率 let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) DispatchQueue.global(qos: .userInitiated).async { do { try handler.perform([request]) } catch { print("OCR执行失败:\(error.localizedDescription)") completion(nil) } } }
核心逻辑说明:
- 先给每个OCR识别到的文本块绑定位置信息,这是还原表格的关键
- 通过Y坐标的差值判断文本块是否属于同一行,聚类出表格的行
- 每行内按X坐标从左到右排序,还原表格的列顺序,最终输出的
[[String]]就是和原表格完全对应的二维数组
三、整合调用示例
把两个函数串起来,就能得到结构化的表格数据了:
func processPDFTable(url: URL) { guard let highResImage = convertPDFToHighResImage(url: url) else { print("PDF转高清图片失败") return } extractStructuredTableText(from: highResImage) { structuredTable in guard let table = structuredTable else { print("OCR结构化处理失败") return } // 打印结构化结果,也可以直接映射到业务模型中 DispatchQueue.main.async { print("结构化表格结果:") for row in table { print(row.joined(separator: " | ")) } } } }
可微调的参数提示
- 如果表格字体特别小,可以把转图的
scale调到3x,进一步提升清晰度 rowThreshold的数值可以根据PDF的实际行间距调整,字体大的表格可以设到20- OCR的
recognitionLanguages必须对应PDF的实际语言,否则识别准确率会暴跌
内容来源于stack exchange




