资讯动态

Python实战:中文手写简历OCR识别与结构化抽取方案

发布时间:2026/10/4 5:36:02 来源:尧图企业网站定制
简介本资源是一套基于Python实现的中文手写简历OCR识别设计源码面向具备一定Python与机器学习基础、希望实践计算机视觉与文字识别技术的开发者及人力资源信息化场景。项目围绕手写中文简历的自动识别与电子化处理展开涵盖图像预处理、特征提取、模型训练与识别输出等模块可用于简历检索、存储与管理的自动化流程。压缩包共25个文件包含15个JPG图像样本、9个Python源码文件及1个txt说明文档整体约8.09MB其中图像文件为模型训练提供手写简历样本源码文件则实现图像处理、数字与时间识别、表格选择及结果输出等关键逻辑。目前已有380人学习下载。通过该源码读者可获取一套结构完整的OCR项目实现理解从样本图像到文字输出的处理链路并参考其模块划分与调用关系为中文手写识别相关开发提供可复用的实践基础。1. 中文手写简历 OCR为什么通用识别引擎一上手就翻车做过招聘系统或者人事档案数字化的朋友大概率遇到过这个场景一批纸质简历扫描件丢过来格式五花八门有表格的、有纯手写的、有打印和手写混排的字段位置完全不固定。你兴冲冲地拿通用 OCR 接口跑一遍打印体部分识别率还行一到手写区域就开始输出乱码——手机号识别成138…期望薪资变成一串无意义符号工作经历整段丢失。这不是引擎不行而是通用 OCR 的训练数据以印刷体为主中文手写体笔画粘连、连笔、个人书写风格差异极大直接套用必然翻车。这篇要讲的就是怎么用 Python 搭一套专门针对中文手写简历的 OCR 识别方案从图像预处理、手写区域检测、文字识别到结构化字段抽取把整条链路跑通。适合两类人一是正在做招聘 SaaS 或企业内部人事系统的后端/算法工程师需要把简历扫描件自动转成结构化数据二是想入门 OCR 落地但被通用方案坑过的开发者。核心思路不是从头训练一个大模型而是用「检测 识别 后处理」三段式管线把开源工具和轻量模型组合起来在 CPU 上也能跑出可用效果。2. 技术选型为什么 PaddleOCR 是当前中文手写简历的最优起点2.1 通用 OCR 与手写 OCR 的本质差异通用 OCR 引擎比如 Tesseract 默认模型在印刷体上的表现已经相当成熟但中文手写场景下有三个硬伤。第一手写汉字的笔画间距不固定同一个字在不同人笔下可能连成一团传统基于连通域分割的方法直接失效。第二简历中的手写内容往往是数字和汉字混排比如2019.07-2021.03 在某某公司担任 Java 开发日期格式、公司名、职位名混在一起通用引擎的分割逻辑会把它们切碎。第三简历扫描件质量参差不齐有的带表格线、有的有印章遮挡、有的拍照角度倾斜这些干扰在印刷体文档里很少同时出现。所以手写简历 OCR 的核心不是识别这一个动作而是先找到手写区域再针对手写区域做识别最后按字段规则做结构化。这三步里检测和识别的模型选择直接决定上限。2.2 PaddleOCR 与 RapidOCR 的取舍当前中文 OCR 开源方案里PaddleOCR 是绕不开的选择。它的 PP-OCRv4 系列模型在中文场景下的检测和识别精度都比较扎实而且官方提供了手写体微调的路径。RapidOCR 是 PaddleOCR 模型的 ONNX 推理封装优势是部署轻、依赖少纯 CPU 环境下推理速度快但它的模型版本更新滞后于 PaddleOCR 官方手写场景的适配需要自己替换模型。我一般会这样选如果是内部工具、对精度要求高、有 GPU 或可以接受 CPU 慢一点直接用 PaddleOCR 全套如果是边缘设备或者需要嵌入到已有 Python 服务里、不想装 PaddlePaddle 那一堆依赖用 RapidOCR 加自定义识别模型。下面以 PaddleOCR 为主线讲RapidOCR 的替换方式在部署章节会提。2.3 环境搭建与最小验证先装环境。Python 版本建议 3.8 到 3.10PaddlePaddle 对 3.11 的支持在部分版本上还有坑。# 安装 PaddlePaddle CPU 版本GPU 版本去官网查对应 CUDA 的命令 python -m pip install paddlepaddle2.5.2 -i https://pypi.tuna.tsinghua.edu.cn/simple # 安装 PaddleOCR python -m pip install paddleocr2.7.0.3 # 验证安装 python -c from paddleocr import PaddleOCR; print(ok)装完之后跑一个最小识别确认基础管线能通from paddleocr import PaddleOCR # use_angle_clsTrue 开启方向分类简历扫描件经常有倾斜 # langch 指定中文模型 ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) img_path resume_sample.jpg result ocr.ocr(img_path, clsTrue) # result 结构[[[box], (text, confidence)], ...] for line in result[0]: box, (text, conf) line print(f文本: {text}, 置信度: {conf:.4f})这段代码的逻辑是PaddleOCR初始化时加载检测、方向分类、识别三个模型ocr()方法对输入图像先做检测得到文本框再对每个框做方向判断和文字识别。use_angle_clsTrue这个参数在简历场景下建议必开因为扫描件经常有 90 度或小角度旋转不开的话识别结果会大量乱码。show_logFalse只是关掉冗余日志调试时可以打开看模型加载情况。跑通之后你会拿到一堆文本框和对应文字但这时候还是散乱的下一步要做的是把手写区域从整张图里分离出来。3. 图像预处理与手写区域检测把该识别的部分框出来3.1 简历扫描件的四类干扰与处理顺序拿到一张简历扫描件先别急着丢给 OCR。我处理过的简历里干扰大致分四类表格线、印章、倾斜、光照不均。处理顺序有讲究搞反了会互相干扰。第一步做倾斜校正。用 OpenCV 的minAreaRect或者霍夫变换检测文档边缘算旋转角度后做仿射变换。第二步去表格线。简历里的表格线如果不去掉OCR 检测模型会把线和文字粘在一起识别结果里混入大量竖线符号。用形态学操作提取水平线和垂直线然后从原图里减掉。第三步处理印章。红色印章可以用颜色通道分离在 HSV 空间里把红色区域 mask 掉再 inpaint 修复。第四步做光照均衡用 CLAHE 自适应直方图均衡化对拍照件效果明显。import cv2 import numpy as np def preprocess_resume(img_path): img cv2.imread(img_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 1. 倾斜校正基于二值化后的最小外接矩形 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) coords np.column_stack(np.where(binary 0)) angle cv2.minAreaRect(coords)[-1] if angle -45: angle 90 angle if abs(angle) 0.5: # 小于 0.5 度不折腾 h, w img.shape[:2] M cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) img cv2.warpAffine(img, M, (w, h), flagscv2.INTER_CUBIC, borderModecv2.BORDER_REPLICATE) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2. 去表格线形态学提取长直线 binary cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 15, 8) h_kernel cv2.getStructuringElement(cv2.MORPH_RECT, (40, 1)) v_kernel cv2.getStructuringElement(cv2.MORPH_RECT, (1, 40)) h_lines cv2.morphologyEx(binary, cv2.MORPH_OPEN, h_kernel) v_lines cv2.morphologyEx(binary, cv2.MORPH_OPEN, v_kernel) lines cv2.add(h_lines, v_lines) # 用原图减去表格线区域 img_no_lines cv2.inpaint(img, lines, 3, cv2.INPAINT_TELEA) # 3. 光照均衡 lab cv2.cvtColor(img_no_lines, cv2.COLOR_BGR2LAB) l, a, b cv2.split(lab) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) l clahe.apply(l) img_final cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_LAB2BGR) return img_final这段预处理里倾斜校正的角度阈值设 0.5 度是因为小于这个角度做旋转反而会引入插值模糊。去表格线的 kernel 长度 40 是根据 A4 扫描件在 200dpi 下的表格线长度经验值如果你的图分辨率不同要按比例调。cv2.inpaint的半径 3 是修复表格线留下的空白太大会把文字也糊掉。CLAHE 的clipLimit2.0是控制对比度增强的上限设太高会把纸张纹理也放大成噪点。3.2 手写区域与打印区域的分离策略预处理完下一步是判断哪些区域是手写、哪些是打印。这一步不做的话打印体部分用通用模型识别没问题但手写部分需要走不同的识别模型或者至少不同的后处理策略。常见做法有两种。一种是基于笔画特征的手写体的笔画宽度变化大、边缘不规则打印体笔画宽度均匀。用笔画宽度变换SWT算每个连通区域的笔画宽度方差方差大的判为手写。另一种更实用用 PaddleOCR 的检测模型先框出所有文本行然后对每个文本行做分类。分类可以用一个轻量 CNN训练数据用公开的手写/打印混合数据集或者自己标注几百张简历图。我一般会先用检测模型框出所有文本行然后对每个框内的图像做简单特征判断计算二值化后的连通域数量与框面积的比值手写体因为笔画粘连这个比值通常比打印体低。这个方法不需要额外训练准确率大概八成够用。误判的部分靠后面的字段规则兜底。def classify_handwriting(crop_binary): 粗略判断文本行是否为手写连通域密度法 num_labels, labels, stats, _ cv2.connectedComponentsWithStats( crop_binary, connectivity8) # 去掉背景 num_labels - 1 area crop_binary.shape[0] * crop_binary.shape[1] if area 0: return False density num_labels / area # 经验阈值打印体连通域密度通常高于手写体 return density 0.002这个函数的逻辑是打印体每个字是独立连通域密度高手写体连笔多连通域少但面积大密度低。阈值 0.002 是在 200dpi 扫描件上试出来的不同分辨率要重新标定。这个方法只能做粗筛实际用的时候我会把判定为手写的区域单独存下来走后面的手写识别模型。4. 手写文字识别与字段结构化从一堆文本框到可用数据4.1 手写识别模型的微调与推理PaddleOCR 官方提供的中文识别模型在印刷体上很强但手写体直接跑会掉点。如果你的简历手写占比高建议做一步微调。微调数据可以用 CASIA-HWDB 手写汉字数据集或者自己标注几百张简历手写区域。标注格式按 PaddleOCR 的要求每行是「图像路径\t标签」。微调命令大致如下# 生成训练标签文件后用 PaddleOCR 的识别训练脚本 python tools/train.py -c configs/rec/PP-OCRv4/ch_PP-OCRv4_rec.yml \ -o Global.pretrained_model./pretrain_models/ch_PP-OCRv4_rec_train/best_accuracy \ Global.train_data_dir./train_data \ Global.epoch_num100 \ Global.learning_rate0.0001这里pretrained_model指向官方预训练权重learning_rate调到 1e-4 是因为微调不需要大学习率太大会把预训练学到的特征冲掉。epoch_num100是经验值手写数据量小的话 50 轮就够看验证集精度停。如果不想微调直接用官方模型加后处理也能凑合。推理时把识别模型的rec_image_shape从默认的3,48,320改成3,48,480给长文本更多空间手写连笔的长字段识别率会好一些。4.2 简历字段的结构化抽取规则识别出来的文本是一行一行的要变成「姓名、电话、邮箱、工作经历」这些字段需要一套抽取规则。简历的字段位置虽然不固定但有一些强模式可以抓。字段匹配规则后处理手机号正则1[3-9]\d{9}去掉识别结果中的空格和横线邮箱正则[\w.-][\w.-]\.\w修正常见 OCR 混淆识别成或a姓名位于简历顶部、2-4 个汉字、置信度 0.9排除简历个人等关键词日期区间正则\d{4}[./-]\d{1,2}\s*[-~至]\s*\d{4}[./-]\d{1,2}统一格式为YYYY-MM学历关键词匹配本科/硕士/博士/大专取最高学历import re def extract_fields(ocr_lines): 从 OCR 行结果中抽取结构化字段 fields {phone: None, email: None, name: None, education: None} full_text .join([t for _, t, _ in ocr_lines]) # 手机号先去掉常见干扰字符 phone_match re.search(r1[3-9]\d[\s-]?\d{4}[\s-]?\d{4}, full_text) if phone_match: fields[phone] re.sub(r[\s-], , phone_match.group()) # 邮箱OCR 常把 识别成 a 或 ©做替换后再匹配 email_text full_text.replace(©, ).replace( a , ) email_match re.search(r[\w.-][\w.-]\.\w, email_text) if email_match: fields[email] email_match.group() # 学历关键词优先级 for edu in [博士, 硕士, 本科, 大专]: if edu in full_text: fields[education] edu break # 姓名取置信度最高的短行排除常见非姓名词 exclude {简历, 个人, 求职, 应聘} candidates [(t, c) for _, t, c in ocr_lines if 2 len(t) 4 and c 0.9 and t not in exclude] if candidates: fields[name] max(candidates, keylambda x: x[1])[0] return fields这段抽取逻辑里手机号的正则允许中间有空格和横线是因为手写时人习惯分段写。邮箱的©替换是血泪经验PaddleOCR 在某些字体下会把识别成版权符号。姓名的抽取用置信度排序加排除词是因为简历顶部通常就是姓名但个人简历这种标题也会被识别成短行必须排掉。4.3 置信度过滤与人工复核队列OCR 不可能百分百准尤其是手写。工程上必须设计一个置信度阈值低于阈值的字段进人工复核队列而不是直接入库。我一般设两档置信度 0.95 直接采用0.8 到 0.95 标记为待确认低于 0.8 直接进人工队列。def route_by_confidence(fields, confidences, high0.95, low0.8): 按置信度分流自动通过 / 待确认 / 人工复核 auto, review, manual {}, {}, {} for key, value in fields.items(): conf confidences.get(key, 0) if conf high: auto[key] value elif conf low: review[key] value else: manual[key] value return auto, review, manual这个分流函数看起来简单但实际用的时候要按字段调阈值。手机号和邮箱这种有强校验规则的即使置信度低一点也可以自动通过因为正则能兜底姓名和工作经历这种没有强规则的阈值要调高。我一般手机号阈值设 0.7姓名设 0.9。5. 避坑记录手写简历 OCR 落地时最容易翻车的五个点5.1 现象识别结果里全是竖线或横线符号原因表格线没去掉检测模型把表格线和文字粘在一起当成一个文本行识别模型把线识别成了|或-。解决预处理阶段必须做形态学去线kernel 长度按图像分辨率调整去线后用 inpaint 修复不要直接涂白涂白会在文字上留缺口。5.2 现象手机号识别成 10 位或 12 位原因手写数字连笔导致分割错误或者扫描件边缘裁切掉了首位。解决正则匹配时放宽位数到 10 到 12 位匹配到之后做校验位验证不通过的进人工队列。另外在预处理阶段检查图像边缘如果手机号区域贴边做一次 padding 再识别。5.3 现象同一份简历两次识别结果不一致原因PaddleOCR 的检测模型对图像尺寸敏感如果输入图像 resize 的尺寸不同检测框会变识别结果跟着变。解决固定输入图像的短边尺寸比如统一 resize 到短边 960 像素长边按比例缩放。推理时不要开多尺度测试简历场景不需要。5.4 现象CPU 上推理速度极慢一张图要十几秒原因PaddleOCR 默认加载三个模型检测模型的计算量最大。如果图像分辨率很高比如 300dpi 的 A4 扫描件检测阶段会非常慢。解决预处理阶段把图像降采样到 200dpi 等效分辨率短边控制在 960 到 1280 之间。另外可以开启use_mpTrue多进程或者换 RapidOCR 的 ONNX 推理CPU 上能快 2 到 3 倍。5.5 现象手写中文识别成形近字比如未识别成末原因手写体形近字是识别模型的固有难点尤其是笔画简单的字。解决在字段后处理阶段加词典约束。比如工作经历字段里出现末大概率是未可以用 jieba 分词加自定义词典做纠正。另外对于关键字段姓名、公司名可以维护一个白名单识别结果与白名单做模糊匹配。6. 进阶技巧用版面分析把简历字段位置先验用起来前面讲的都是识别完再抽取但简历有一个天然优势版面结构相对固定。姓名在顶部、联系方式在姓名附近、工作经历按时间倒序排列。如果能先做版面分析把字段位置先验用起来抽取准确率能再上一个台阶。具体做法是用 PaddleOCR 的版面分析模型或者简单的投影法把简历分成几个区域——头部信息区、教育背景区、工作经历区、技能证书区。然后针对每个区域用不同的抽取规则。比如头部信息区重点抽姓名、电话、邮箱工作经历区重点抽日期区间和公司名。def layout_split(img): 基于水平投影的简单版面分割 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 水平投影统计每行黑色像素数 row_sum np.sum(binary, axis1) / 255 # 找到空白行投影值接近 0作为分割线 threshold binary.shape[1] * 0.01 blank_rows np.where(row_sum threshold)[0] # 将连续空白行合并成分割区间 segments [] if len(blank_rows) 0: start blank_rows[0] prev blank_rows[0] for r in blank_rows[1:]: if r - prev 5: # 间隔大于 5 行算新段 segments.append((start, prev)) start r prev r segments.append((start, prev)) return segments这个投影法的逻辑是简历里不同区块之间通常有空白行空白行的水平投影值接近零。把连续空白行找出来就能把简历切成几段。threshold设成宽度的 1% 是经验值太小会把文字行内的间隙也当成分割太大则切不开。切出来的段按从上到下顺序大致对应头部、教育、工作经历等区域。拿到区域之后对每个区域单独跑 OCR 和字段抽取比全图跑一遍再猜位置要准。尤其是姓名在头部区域里取置信度最高的 2 到 4 字短行基本不会错。还有一个技巧是维护一个字段-区域映射表把常见简历模板的字段位置统计出来。比如 80% 的简历姓名在图像上方 15% 区域内电话在姓名下方 5% 区域内。这个先验可以做成配置文件不同模板用不同配置。我一般会先跑一批样本统计各字段的归一化坐标分布然后设一个宽松的范围做约束。最后说一个验证方法拿 50 份真实简历做测试集人工标注好字段真值然后跑整条管线算每个字段的准确率和召回率。手机号和邮箱这种强规则字段准确率应该能到 95% 以上姓名和公司名这种依赖识别的准确率 85% 左右算正常。如果低于这个数优先查预处理和置信度阈值而不是急着换模型。我自己踩过的最大坑是早期太迷信模型精度花了两周微调识别模型结果发现瓶颈根本不在识别而在预处理没做好——表格线没去干净检测框全是错的识别模型再好也没用。后来养成习惯任何 OCR 项目先花半天把预处理调到位再动模型。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑