文章目錄
- 一. 項目結構
- 二.流程分析
- 2.1 批處理器核心代碼解析
- 三. 跨頁表格相似度匹配原理
- 3.1 表頭內容相似度-特征向量歸一化
- 3.2 表頭內容相似度-余弦相似度
- 3.3 定時緩存清理
ocr掃描有其局限性。對于pdf文本類型這種pdfbox,aspose-pdf,spire直接提取文本的精準性更高。經過綜合對比我們覺得aspose和spire在讀取pdf文本方面較為優秀。基于此我們可能需要提取pdf中所有表格數據,完成數據錄入。但是表格數據不同,還存在跨頁表格問題。但是按照以下方案即可解決。本文的表格處理思想來源于mybatis的底層設計。
特征 | 余弦相似度 | 編輯距離 |
---|---|---|
原理 | 衡量向量方向的夾角(語義相似性) | 計算字符串轉換所需的最小操作次數(字符級差異) |
輸入類型 | 向量(如文本的TF-IDF或詞嵌入向量) | 字符串或序列 |
關注點 | 語義層面的相似性(如主題、用詞) | 結構層面的差異(如拼寫錯誤、字符順序) |
輸出范圍 | [-1, 1](通常取絕對值或歸一化為0-1) | 非負整數(0表示完全匹配) |
計算復雜度 | O(n)(向量化后快速計算) | O(n*m)(對長文本較慢) |
典型應用 | 文檔相似度、推薦系統、語義搜索 | 拼寫糾錯、DNA序列比對、短文本模糊匹配 |
開源地址
一. 項目結構
本設計基于aspose-pdf實現
|-- SpringContextUtil.java
`-- pdf|-- AbstractTextMappingTemplate.java #抽象模板映射器 解析內容映射到結構化對象|-- PDFboxTable.java # 暫留擴展|-- PdfTableParsingEngine.java # 表格解析引擎 提供從PDF文檔中提取并處理表格數據的功能|-- StringEscapeUtil.java # 字符串轉義工具類 防止注入攻擊|-- TableBatchProcessor.java # 具體表格執行處理器 表格批處理器|-- annotation # 映射注解包|-- aspect # 注解處理器包|-- converter # 抽象模板映射器具體實現 包`-- entity # 想要映射的結構化對象包
二.流程分析
- 表格解析器提取pdf表格文本
- 表格批處理器負責具體執行表格解析
- 字符串轉義避免惡意攻擊
- 抽象映射器允許用戶具體實現映射實體
2.1 批處理器核心代碼解析
表格解析器每檢測一頁的所有表格,就提交到批處理器進行具體數據清洗,歸一化。以下是進行數據批處理的核心邏輯
/*** 添加表格到批處理隊列** @param pageIndex 頁碼索引* @param tables 頁面中的表格列表*/public void addPageTables(int pageIndex, List<AbsorbedTable> tables) {// 資源限制檢查 一頁10個表格if (tables.size() > MAX_TABLES_PER_PAGE) {log.warn("頁面{}表格數量超過限制: {}", pageIndex, tables.size());// 截取前MAX_TABLES_PER_PAGE個表格tables = tables.subList(0, MAX_TABLES_PER_PAGE);}// 處理當前頁的表格List<StringBuilder> processedTables = new ArrayList<>();for (AbsorbedTable table : tables) {// 處理單個表格StringBuilder tableContent = processSingleTable(table);if (tableContent == null) continue;// 數據清洗PdfTableParsingEngine.cleanData(tableContent);// 生成表格指紋String tableFingerprint = generateTableFingerprint(tableContent);// 將表格指紋和內容存儲到跨頁表格緩存中crossPageTableCache.putIfAbsent(tableFingerprint, new CacheEntry(new StringBuilder(tableContent)));// 更新緩存條目的最后訪問時間crossPageTableCache.get(tableFingerprint).updateLastAccessTime();// 檢查是否為跨頁表格if (isCrossPageTable(tableFingerprint)) {// 合并跨頁表格tableContent = mergeCrossPageTable(tableContent, tableFingerprint);} else {// 異常檢測(連續重復表格)if (isDuplicateTable(tableFingerprint)) {log.warn("檢測到連續重復表格類型: {}", tableFingerprint);continue;}}// 添加到處理隊列processedTables.add(tableContent);}// 將處理后的表格添加到緩沖隊列if (!processedTables.isEmpty()) {try {// 嘗試添加到隊列,如果隊列已滿則提交當前隊列中的所有表格if (!tableBufferQueue.offer(processedTables, 100, TimeUnit.MILLISECONDS)) {log.info("緩沖隊列已滿,提交批處理任務");submitBatchTask();// 重新嘗試添加tableBufferQueue.put(processedTables);}} catch (InterruptedException e) {log.error("添加表格到緩沖隊列失敗: {}", e.getMessage());Thread.currentThread().interrupt();}}}
如上述代碼,
- processSingleTable(AbsorbedTable table)用于具體解析表格內容并拼接成特定字符串。
- cleanData(StringBuilder builder) 移除所有空白字符和換行符
- generateTableFingerprint(StringBuilder tableContent) 用于識別跨頁表格相似度合并
- crossPageTableCache 緩存跨頁表格,因為是以頁為單位檢測表格的。下一頁需要保留上一頁表格
- mergeCrossPageTable(tableContent, tableFingerprint) 設定相似度大于85%且不為100%。為同一表格。進行合并。
- submitBatchTask() 提交批處理任務
- processBatchTables(List<List> batchTables) 獲取抽象映射器的具體實現。根據具體規則進行映射匹配
三. 跨頁表格相似度匹配原理
- 1.根據特定表頭內容相似度
- 2.根據表格樣式特征
3.1 表頭內容相似度-特征向量歸一化
字符串長度建議不要超過特征矩陣維度長度
使用余弦相似矩陣,比較兩個表頭字符串相似度.一般認為表頭字串很短,因此初始化16特征向量即可
表示我們可以把字符ascii映射到特征向量上,并通過單位向量歸一化結果。獲取第一塊內容字串的標準化特征向量。同理對第二塊內容字串做標準化計算。
/*** 計算內容相似度(基于矢量相似度)** @param str1 字符串1* @param str2 字符串2* @return 內容相似度*/private double calculateContentSimilarity(String str1, String str2) {if (str1 == null || str2 == null) {throw new IllegalArgumentException("輸入字符串不能為空");}// 將字符串轉換為特征向量double[] vector1 = stringToVector(str1);double[] vector2 = stringToVector(str2);// 計算余弦相似度return cosineSimilarity(vector1, vector2);}/*** 將字符串轉換為特征向量** @param str 輸入字符串* @return 特征向量*/private double[] stringToVector(String str) {// 初始化特征向量double[] vector = new double[VECTOR_DIMENSION];// 創建字符頻率映射Map<Character, Integer> charFrequency = new HashMap<>();// 統計字符頻率for (char c : str.toCharArray()) {charFrequency.put(c, charFrequency.getOrDefault(c, 0) + 1);}// 將字符頻率映射到特征向量for (char c : charFrequency.keySet()) {int index = Math.abs(c) % VECTOR_DIMENSION;vector[index] += charFrequency.get(c);}// 歸一化向量normalizeVector(vector);return vector;}/*** 歸一化向量** @param vector 輸入向量*/private void normalizeVector(double[] vector) {double magnitude = 0.0;// 計算向量模長for (double value : vector) {magnitude += value * value;}magnitude = Math.sqrt(magnitude);// 歸一化向量if (magnitude > 0) {for (int i = 0; i < vector.length; i++) {vector[i] /= magnitude;}}}
3.2 表頭內容相似度-余弦相似度
- 我們將原始特征向量進行標準化(歸一化)處理,使其轉化為單位向量(模長為1),從而消除向量尺度差異對相似性度量的影響。(注:此步驟確保所有向量處于同一量綱空間,使得后續計算具有可比性)
- 對于兩個單位向量 u u u 和 v v v,其點積在數值上等于它們的余弦相似度(即 c o s θ cosθ cosθ)。
幾何意義:余弦相似度反映向量方向的接近程度,與向量維度無關。
數學表達:
c o s θ = u ? v ∣ u ∣ ? ∣ v ∣ cosθ=\frac{u·v}{|u|·|v|} cosθ=∣u∣?∣v∣u?v?
結果解釋:
cos ? θ ≈ 1 c o s θ ≈ 1 \cos\theta \approx 1cosθ≈1 cosθ≈1cosθ≈1:向量方向高度一致,對應字符串內容幾乎相同。
cos ? θ ≈ 0 c o s θ ≈ 0 \cos\theta \approx 0cosθ≈0 cosθ≈0cosθ≈0:向量正交,字符串內容無相關性。
應用示例:在文本匹配任務中,可通過該值量化兩段文本的語義相似性。
點積與哈達瑪積的區別:
點積輸出標量,用于衡量整體相似性;
哈達瑪積為元素級乘法,輸出同維向量,常用于局部特征交互。
private double cosineSimilarity(double[] vector1, double[] vector2) {if (vector1.length != vector2.length) {throw new IllegalArgumentException("向量維度不匹配");}double dotProduct = 0.0;double magnitude1 = 0.0;double magnitude2 = 0.0;for (int i = 0; i < vector1.length; i++) {dotProduct += vector1[i] * vector2[i];magnitude1 += vector1[i] * vector1[i];magnitude2 += vector2[i] * vector2[i];}magnitude1 = Math.sqrt(magnitude1);magnitude2 = Math.sqrt(magnitude2);if (magnitude1 == 0.0 || magnitude2 == 0.0) {return 0.0;} else {return dotProduct / (magnitude1 * magnitude2);}}
3.3 定時緩存清理
由于我們為了保證跨頁表格的關聯關系。我們使用map集合保存上一頁表格內容。
/*** 構造函數*/public TableBatchProcessor() {// 使用虛擬線程池處理批量映射任務this.executorService = Executors.newVirtualThreadPerTaskExecutor();// 初始化表格緩沖隊列this.tableBufferQueue = new LinkedBlockingQueue<>(BUFFER_CAPACITY);// 初始化表格類型計數器this.tableTypeCounter = new ConcurrentHashMap<>();// 初始化跨頁表格緩存this.crossPageTableCache = new ConcurrentHashMap<>();// 初始化緩存清理調度器this.cacheCleanupScheduler = Executors.newScheduledThreadPool(1);// 啟動定時清理任務this.cacheCleanupScheduler.scheduleAtFixedRate(this::cleanupCrossPageTableCache, 1, 1, TimeUnit.MINUTES);}
我設計了最早時間淘汰機制,同時為了進一步防止內存溢出。設計了map最大值。超出閾值清理所有。但顯然這是有問題的,可能導致跨表關聯關系斷開。因此先以拋出異常解決
/*** 清理跨頁表格緩存(增強版)*/private void cleanupCrossPageTableCache() {long currentTime = System.currentTimeMillis();List<String> expiredKeys = new ArrayList<>();for (Map.Entry<String, CacheEntry> entry : crossPageTableCache.entrySet()) {if (currentTime - entry.getValue().lastAccessTime > CACHE_ENTRY_TTL) {expiredKeys.add(entry.getKey());}}// 限制緩存條目數量if (crossPageTableCache.size() > MAX_CACHE_ENTRIES) {crossPageTableCache.clear();throw new IllegalStateException("緩存條目數量超過限制");}for (String key : expiredKeys) {crossPageTableCache.remove(key);log.info("清理過期緩存條目: {}", key);}}