GLM-OCR识别结果后处理:Python数据结构优化技巧

GLM-OCR识别结果后处理:Python数据结构优化技巧 GLM-OCR识别结果后处理Python数据结构优化技巧每次用GLM-OCR处理完一堆文档图片看着识别出来的那一大段密密麻麻的文本是不是有点头疼识别准确率可能不错但出来的文本顺序混乱、段落不分、关键信息散落各处根本没法直接用。这就像收到了一箱乐高零件虽然每个零件都很好但散乱一地你得自己动手把它们拼成一辆车、一座房子。GLM-OCR给出的就是这些“零件”——原始的文本行和坐标。而我们要做的就是用Python的数据结构当一回“文本乐高大师”把这些零件智能地组装成结构清晰、可直接使用的JSON或表格数据。今天我就结合自己处理大量合同、报表、票据的经验跟你聊聊怎么用列表、字典这些基础的Python工具玩转OCR后处理让机器识别的文字真正为你所用。1. 从混乱到有序理解OCR的“原始输出”在开始动手组装之前我们得先搞清楚手头有哪些“零件”。GLM-OCR通常不会直接给你一段完美的文章它的输出更接近一种“原材料”状态。1.1 典型的原始识别数据结构通常OCR引擎包括GLM-OCR返回的结果是一个列表列表里的每个元素代表识别到的一行或一个文本块信息。每个元素本身又是一个字典包含了文本内容及其在图片中的位置。一个简化版的例子看起来是这样的raw_ocr_result [ {text: 发票号码INV20231001, bbox: [50, 100, 300, 120]}, # bbox: [x1, y1, x2, y2] {text: 日期2023-10-26, bbox: [50, 130, 200, 150]}, {text: 商品A, bbox: [60, 200, 120, 220]}, {text: 单价100.00, bbox: [250, 200, 350, 220]}, {text: 数量2, bbox: [400, 200, 450, 220]}, {text: 商品B, bbox: [60, 230, 120, 250]}, {text: 单价200.00, bbox: [250, 230, 350, 250]}, {text: 小计600.00, bbox: [350, 300, 450, 320]}, {text: 总计600.00, bbox: [350, 330, 450, 350]}, ]这里bbox边界框是关键。它用四个数字[x1, y1, x2, y2]定义了文本行在图片上的矩形区域分别是左上角的x、y坐标和右下角的x、y坐标。所有后续的“智能”处理都依赖于对这些坐标的分析。1.2 原始数据带来的直接挑战直接看上面这个列表你可能会发现几个问题逻辑关系丢失“商品A”和它的“单价”、“数量”在语义上是一行但在列表里是三个独立的项。阅读顺序混乱虽然坐标隐含了位置信息但列表本身的顺序可能不是正确的阅读顺序比如从左到右从上到下。结构层级模糊哪些是标题哪些是表头哪些是表格内容哪些是总计机器没有告诉你。我们的目标就是写一个“翻译器”把上面这种raw_ocr_result转换成下面这种结构化的数据{ header: { invoice_number: INV20231001, date: 2023-10-26 }, items: [ {name: 商品A, unit_price: 100.00, quantity: 2, subtotal: 200.00}, {name: 商品B, unit_price: 200.00, quantity: 2, subtotal: 400.00} ], footer: { subtotal: 600.00, total: 600.00 } }接下来我们就看看怎么用Python的数据结构一步步实现这个转换。2. 第一步排序与分组——给文本行“排座位”处理混乱数据的第一步往往是排序。我们需要依据bbox坐标将文本行按照人类自然的阅读顺序通常是从上到下从左到右排列。2.1 实现基于坐标的智能排序一个简单但有效的排序策略是先比较文本行中心的y坐标即行高将同一水平行的文本归到一起再对每一行内的文本按x坐标即从左到右排序。def sort_ocr_results_by_reading_order(ocr_results): 根据bbox坐标将OCR结果按阅读顺序从上到下从左到右排序。 # 计算每一行文本的垂直中心点y_center和水平中心点x_center for item in ocr_results: bbox item[bbox] item[y_center] (bbox[1] bbox[3]) / 2 # (y1 y2) / 2 item[x_center] (bbox[0] bbox[2]) / 2 # (x1 x2) / 2 # 排序首先按y_center分组允许一定误差组内按x_center排序 # 先按y_center粗略排序 ocr_results.sort(keylambda x: x[y_center]) # 再进行更精细的行分组 sorted_results [] current_row [] y_threshold 10 # 假设y坐标相差10像素以内算同一行这个值需要根据实际情况调整 for i, item in enumerate(ocr_results): if not current_row: current_row.append(item) else: # 如果当前item与当前行第一个元素的y坐标接近则视为同一行 if abs(item[y_center] - current_row[0][y_center]) y_threshold: current_row.append(item) else: # 当前行结束对行内元素按x坐标排序后加入结果 current_row.sort(keylambda x: x[x_center]) sorted_results.extend(current_row) current_row [item] # 开始新的一行 # 不要忘记最后一组 if current_row: current_row.sort(keylambda x: x[x_center]) sorted_results.extend(current_row) # 清理临时添加的字段 for item in sorted_results: item.pop(y_center, None) item.pop(x_center, None) return sorted_results # 使用示例 sorted_results sort_ocr_results_by_reading_order(raw_ocr_result) for item in sorted_results: print(item[text]) # 输出顺序更接近发票号码 - 日期 - 商品A 单价 数量 - 商品B 单价 数量 - 小计 - 总计这个函数就像一个“座位引导员”把乱坐的客人按照“排”行和“座”列重新安排妥当。2.2 利用字典进行行聚类上面的方法在排序时进行了隐式分组。有时我们可能需要更显式地保留“行”的概念。这时可以用字典来构建一个以行号为键的数据结构。def group_text_by_row(ocr_results, row_height_tolerance15): 将文本行按垂直位置分组到不同的行中。 返回一个字典键为行号或基准y坐标值为该行所有文本项的列表。 # 首先为每个项计算一个代表其垂直位置的‘行标签’。 # 一个简单的方法将其y_center除以容忍度并取整相同结果的视为同一行。 rows {} for item in ocr_results: bbox item[bbox] y_center (bbox[1] bbox[3]) / 2 row_key int(y_center / row_height_tolerance) # 生成一个行标识符 if row_key not in rows: rows[row_key] [] rows[row_key].append(item) # 现在rows字典的键是行标识值是该行所有未排序的文本项。 # 我们需要对字典按键排序以保证行顺序并对每一行内的项按x坐标排序。 ordered_rows {} for row_key in sorted(rows.keys()): # 按行号从上到下排序 row_items rows[row_key] row_items.sort(keylambda x: (x[bbox][0] x[bbox][2]) / 2) # 按x_center排序 ordered_rows[row_key] row_items return ordered_rows # 使用示例 rows_dict group_text_by_row(raw_ocr_result) for row_key, items in rows_dict.items(): print(f行 {row_key}: {[item[text] for item in items]}) # 输出可能类似 # 行 13: [发票号码INV20231001] # 行 15: [日期2023-10-26] # 行 20: [商品A, 单价100.00, 数量2] # 行 23: [商品B, 单价200.00, 数量2] # 行 30: [小计600.00] # 行 33: [总计600.00]通过字典进行行聚类后数据的二维表格属性就非常清晰了为后续提取结构化信息打下了坚实基础。3. 第二步解析与结构化——从文本行到信息实体排序分组之后我们得到的是有序的文本行。接下来需要从这些文本字符串中提取出有意义的键值对如“日期2023-10-26”-{“date”: “2023-10-26”}并将相关的行组合成更大的逻辑单元如一个完整的商品条目。3.1 使用列表和字典构建嵌套结构我们的目标是构建一个嵌套的字典结构来反映数据的层次关系。对于发票例子顶层字典可能有header、items、footer等键。def parse_invoice_structure(ordered_rows_dict): 假设 ordered_rows_dict 是上一步 group_text_by_row 的输出。 尝试解析出表头、商品列表和表尾。 这是一个启发式规则示例实际规则需根据具体文档格式定制。 invoice_data { header: {}, items: [], footer: {} } all_rows list(ordered_rows_dict.values()) # 1. 解析表头通常在最前面几行包含“发票号码”、“日期”等关键词 header_keywords [发票号码, 日期, 订单号, 客户] for row_items in all_rows[:3]: # 假设表头在前3行内 for item in row_items: text item[text] for keyword in header_keywords: if keyword in text: # 简单分割实际可能需要更健壮的解析如正则表达式 if in text or : in text: separator if in text else : key, value text.split(separator, 1) clean_key key.strip().replace(, ).replace(:, ) invoice_data[header][clean_key] value.strip() # 2. 解析商品项识别表格数据区域 # 假设商品行包含“商品”字样或符合“名称 单价 数量”的模式 item_started False for row_items in all_rows: row_texts [item[text] for item in row_items] row_text_combined .join(row_texts) # 检测是否进入商品区域启发式规则 if not item_started and any(word in row_text_combined for word in [商品, 名称, 品名]): item_started True # 这一行可能是表头跳过 continue if item_started: # 检测是否离开商品区域遇到“小计”、“总计”等 if any(word in row_text_combined for word in [小计, 总计, 合计, 金额]): item_started False # 这一行属于表尾会在下一步解析 break # 尝试将当前行解析为一个商品项 # 这里是一个简单假设行内有2-3个元素可能是[名称单价数量] if 2 len(row_items) 4: item_dict {} # 更智能的解析需要根据列位置或内容模式来判断 # 例如假设第一个元素是名称包含“单价”的是价格包含“数量”的是数量 for elem in row_items: txt elem[text] if 单价 in txt: # 提取数字 import re numbers re.findall(r\d\.?\d*, txt) if numbers: item_dict[unit_price] float(numbers[0]) elif 数量 in txt: numbers re.findall(r\d\.?\d*, txt) if numbers: item_dict[quantity] int(float(numbers[0])) else: # 假设它不是明显的单价/数量字段且不是纯数字则可能是名称 if not re.match(r^\d\.?\d*$, txt.strip()): item_dict[name] txt # 计算小计如果提供了单价和数量 if unit_price in item_dict and quantity in item_dict: item_dict[subtotal] item_dict[unit_price] * item_dict[quantity] if item_dict: # 如果成功解析出一些信息 invoice_data[items].append(item_dict) # 3. 解析表尾总计等 footer_keywords [小计, 总计, 合计, 税额, 应付金额] for row_items in all_rows[-5:]: # 假设表尾在最后5行内 for item in row_items: text item[text] for keyword in footer_keywords: if keyword in text: # 提取数值 import re numbers re.findall(r\d\.?\d*, text) if numbers: clean_key keyword.strip() invoice_data[footer][clean_key] float(numbers[0]) return invoice_data # 使用示例 structured_data parse_invoice_structure(rows_dict) import json print(json.dumps(structured_data, indent2, ensure_asciiFalse))这段代码充满了启发式规则if...any...这正是OCR后处理的核心——将你对文档布局和内容的理解编码成逻辑规则。没有一套规则能通吃所有文档但针对某一类固定格式的文档如特定公司的发票、某种格式的报表这种方法是高效且可靠的。3.2 利用正则表达式进行精准提取在上面的解析中我们已经用到了简单的正则表达式re.findall(r\d\.?\d*, text)来提取数字。对于更复杂的字段正则表达式是强大的工具。import re def enhanced_field_parser(text): 使用正则表达式更健壮地提取常见字段。 patterns { date: r(\d{4}[-/年]\d{1,2}[-/月]\d{1,2}日?), # 匹配日期 invoice_number: r(发票号(码)?[:]?\s*)([A-Za-z0-9-]), # 匹配发票号 money: r(\d\.?\d*)\s*(元|¥|)?, # 匹配金额 phone: r(\d{3,4}-\d{7,8}|\d{11}), # 匹配电话 } extracted {} for field, pattern in patterns.items(): match re.search(pattern, text) if match: # 提取匹配组中最可能的值通常最后一个分组是值 extracted[field] match.group(match.lastindex if match.lastindex else 0) return extracted # 使用示例 test_texts [日期2023-10-26, 总金额1250.50元, 联系电话138-0013-8000] for txt in test_texts: print(f{txt} - {enhanced_field_parser(txt)})4. 第三步纠错与增强——让结果更可靠OCR识别并非100%准确特别是对于模糊、倾斜或复杂版式的图片。在结构化的同时或之后进行一些纠错和逻辑校验能大幅提升输出数据的可靠性。4.1 基于字典的拼写检查与候选词替换对于已知的、固定的词汇如商品名称、公司名可以维护一个纠错词典。class OCRPostProcessor: def __init__(self): # 定义一个常见的OCR误识别映射例如 0-O, 1-I 等 self.common_ocr_errors { 0: O, 1: I, 5: S, 8: B, # 可以添加更多 } # 领域特定词典 self.product_name_correction { 冏品A: 商品A, 苹杲: 苹果, # ... } def correct_text(self, text): 简单的文本纠错 # 1. 应用常见字符替换 for wrong, right in self.common_ocr_errors.items(): text text.replace(wrong, right) # 2. 应用领域词典校正 for wrong, right in self.product_name_correction.items(): if wrong in text: text text.replace(wrong, right) break # 简单起见只替换第一个匹配 return text def validate_and_correct_items(self, items_list): 对解析出的商品列表进行逻辑校验和修正 corrected_items [] for item in items_list: # 校正商品名 if name in item: item[name] self.correct_text(item[name]) # 逻辑校验如果小计存在检查是否等于单价*数量 if all(k in item for k in (unit_price, quantity, subtotal)): calculated item[unit_price] * item[quantity] if abs(calculated - item[subtotal]) 0.01: # 允许微小浮点误差 print(f警告商品{item.get(name)}小计计算不符解析值{item[subtotal]}, 计算值{calculated}以计算值为准。) item[subtotal] round(calculated, 2) corrected_items.append(item) return corrected_items # 使用示例 processor OCRPostProcessor() # 假设我们从parse_invoice_structure得到了 items sample_items [{name: 冏品A, unit_price: 100.0, quantity: 2, subtotal: 200.0}] corrected processor.validate_and_correct_items(sample_items) print(corrected) # 输出: [{name: 商品A, ...}] 名称被纠正4.2 利用集合检查必填字段对于结构化输出我们常常要求某些字段是必填的。可以用Python的集合操作来快速检查。def validate_structure(data, required_fields_by_section): 验证结构化数据是否包含必需的字段。 required_fields_by_section: 字典如 {header: [invoice_number, date], items: [name]} missing_fields {} for section, required_fields in required_fields_by_section.items(): if section in data: current_fields set(data[section].keys() if isinstance(data[section], dict) else (set(data[section][0].keys()) if data[section] else set())) required_set set(required_fields) missing required_set - current_fields if missing: missing_fields[section] list(missing) else: missing_fields[section] f缺失整个{section}部分 return missing_fields # 使用示例 required_map {header: [发票号码, 日期], items: [name, unit_price]} validation_result validate_structure(structured_data, required_map) if validation_result: print(f以下必填字段缺失{validation_result}) else: print(所有必填字段检查通过。)5. 总结走完这一趟从原始OCR文本到结构化JSON的旅程你会发现核心武器其实就是Python里那些最基础的数据结构——列表、字典、集合再加上字符串处理和正则表达式。整个过程就像是在玩一个高级的“文本拼图”游戏。排序和分组是理清碎片解析和结构化是按图索骥纠错和校验是最后的质量检查。没有一招鲜的通用算法成功的关键在于对你所要处理的特定类型文档的深入理解。你需要仔细观察几十份样本总结出它们的共同布局规律和内容模式然后将这些规律转化为if-else判断、排序键和正则表达式。下次当GLM-OCR吐给你一堆杂乱文本时别急着头疼。不妨把它看作一次用代码施展“整理术”的机会。从最简单的按坐标排序开始一步步构建你的解析规则看着混乱的数据逐渐变得规整、清晰、可用这个过程本身就充满了工程师所特有的乐趣。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。