简介:这份资源面向需要将图片表格批量数字化的开发者与办公人员,提供一套基于 Python 调用腾讯云 OCR 接口的完整实现,可识别 jpg/png 照片中的手写文字与手绘表格,并输出为 Excel 文件,适用于教育、财务、工程等场景的票据、笔记与图表整理。压缩包共 3 个文件,包含 2 个 py 脚本与 1 个 exe 可执行程序,整体约 96.13MB;其中主程序负责调用云接口完成识别,辅助模块承担图像处理、结果解析与数据清洗,exe 则让未安装 Python 环境的用户也能直接运行。资源已有 561 人学习,读者可从中掌握 requests 发送 HTTP 请求、腾讯云 SDK 集成、pandas 构建 DataFrame 并导出 Excel 的完整链路,同时了解批量遍历目录、逐张识别保存的实现思路,以及将脚本打包为 Windows 可执行文件的方法,便于快速落地到实际自动化办公流程中。
1. 从一堆手机拍的报销单说起:这套 Python + 腾讯云 OCR 方案到底能干什么
财务同事甩过来一个压缩包,里面是三百多张手机拍的报销单,有手写金额、有画得歪歪扭扭的表格、还有几张是直接拿笔在 A4 纸上画的考勤表。要求当天下午把每张单子的关键字段录进 Excel。人工敲,眼睛先废。这时候能救命的,就是一套把 jpg/png 直接喂进去、吐出一张结构化 Excel 的脚本。核心思路不复杂:Python 负责批量读图和调度,腾讯云的文字识别接口负责把图里的字抠出来,最后用 openpyxl 或 pandas 把结果写成 xlsx。它解决的不是「识别一张图」这种玩具需求,而是「几百张格式不统一的照片,批量转成能筛选、能求和、能导入数据库的表格」。适合谁?做财务自动化、档案数字化、表单录入外包的从业者,以及手头有大量纸质表格照片、又不想一张张手敲的 Python 初学者。下面我把这套东西从环境搭到落盘,连同我踩过的坑,一次讲清楚。
2. 环境与接口选型:为什么是腾讯云 OCR 而不是本地开源模型
2.1 手写和手画表格,决定了你不能用普通 OCR
普通印刷体 OCR 引擎(比如 Tesseract)对付打印的发票还行,一旦遇到手写数字、连笔签名、手画的表格线,识别率会断崖式下跌。手画表格更麻烦:它没有规整的边框,单元格位置全靠笔画围出来,传统基于连通域和直线检测的方案直接失效。腾讯云的文字识别里,通用印刷体识别适合规整文档,而「通用手写体识别」和「表格识别」这两个接口才是这套方案的主力。表格识别接口会返回每个单元格的行列坐标和文本,等于帮你把「手画表格」还原成了二维结构,这是本地开源模型很难稳定做到的。选它不是因为迷信云厂商,而是手写 + 手画表格这个组合,目前云端接口的鲁棒性确实更省心。
2.2 开通服务与拿到密钥的正确姿势
去腾讯云控制台搜「文字识别」,开通后进入访问管理(CAM)创建密钥。你会拿到一对SecretId和SecretKey。这里有个血泪经验:千万别把密钥硬编码进脚本然后传到代码仓库,我见过太多人这么干,第二天就收到欠费短信。正确做法是写进环境变量,或者用腾讯云的子账号 + 最小权限策略,只给 OCR 相关权限。
# 把密钥写进环境变量,脚本里用 os.environ 读取 export TENCENT_SECRET_ID="你的SecretId" export TENCENT_SECRET_KEY="你的SecretKey" # 安装官方 SDK,注意是 tencentcloud-sdk-python,不是旧版的 qcloud pip install tencentcloud-sdk-python pip install openpyxl pandas pillowtencentcloud-sdk-python是官方维护的统一 SDK,OCR 模块在tencentcloud.ocr.v20181119这个命名空间下。openpyxl负责写 xlsx,pandas用来做数据整理,pillow在需要压缩图片时用得上。版本上不用追最新,能跑通就行,但 SDK 建议用近一年的版本,老版本对表格识别接口的字段支持不全。
2.3 图片预处理:别把 10MB 的原图直接怼上去
手机拍的照片动辄 5 到 10MB,接口对单张图片有大小限制(一般 base64 编码后不超过 7MB),而且大图传输慢、还容易超时。我一般会先用 pillow 把长边压到 2000 像素以内,再转成 base64。注意压缩别太狠,手写字压糊了识别率反而下降,质量参数给 85 左右比较稳。
from PIL import Image import base64, io def img_to_base64(path, max_side=2000, quality=85): img = Image.open(path) # 按长边等比缩放,避免超大图触发接口大小限制 if max(img.size) > max_side: ratio = max_side / max(img.size) img = img.resize((int(img.width * ratio), int(img.height * ratio))) buf = io.BytesIO() # 统一转 RGB,png 带透明通道时接口可能报错 img.convert("RGB").save(buf, format="JPEG", quality=quality) return base64.b64encode(buf.getvalue()).decode() # 调用示例 b64 = img_to_base64("报销单.jpg") print(len(b64)) # 观察 base64 长度,超过 7MB 就要继续压这段代码做了三件事:等比缩放、统一转 RGB、编码成 base64。参数max_side控制长边像素,quality控制 JPEG 质量。为什么要转 RGB?因为 png 的透明通道在部分接口里会引发解码异常,统一成三通道最省事。打印 base64 长度是为了心里有数,超过接口上限就继续调小max_side。
3. 调接口拿结果:手写体识别与表格识别的两条路线
3.1 通用手写体识别:适合字段散落的单据
如果照片里就是零散的手写字段(姓名、金额、日期),用GeneralHandwritingOCR接口最直接。它返回的是文本块列表,每个块带识别文字和位置。你拿到之后按位置从上到下、从左到右排序,再按关键词匹配就能抽出字段。
from tencentcloud.common import credential from tencentcloud.common.profile.client_profile import ClientProfile from tencentcloud.common.profile.http_profile import HttpProfile from tencentcloud.ocr.v20181119 import ocr_client, models import os, json def get_client(): cred = credential.Credential( os.environ["TENCENT_SECRET_ID"], os.environ["TENCENT_SECRET_KEY"] ) http = HttpProfile(endpoint="ocr.tencentcloudapi.com", reqTimeout=30) profile = ClientProfile(httpProfile=http) return ocr_client.OcrClient(cred, "ap-guangzhou", profile) def handwriting_ocr(image_b64): client = get_client() req = models.GeneralHandwritingOCRRequest() req.ImageBase64 = image_b64 resp = client.GeneralHandwritingOCR(req) # 返回结构里 TextDetections 是文本块列表 return json.loads(resp.to_json_string())["TextDetections"] blocks = handwriting_ocr(b64) for b in blocks: print(b["DetectedText"], b["Polygon"]) # 文字 + 四点坐标reqTimeout=30是请求超时,网络差的时候别设太小。ap-guangzhou是地域,选离你服务器近的能降延迟。返回的Polygon是四个角点坐标,这是后面做版面还原的关键——有了坐标,你才能判断哪个字在哪一行、哪一列。很多人只取文字丢掉坐标,结果字段全串行,这是新手最容易翻车的地方。
3.2 表格识别:手画表格的还原主力
手画的表格用RecognizeTableOCR(表格识别)接口。它返回TableDetections,里面每个单元格带行号、列号、文本和坐标。等于接口帮你把表格结构解析好了,你只需要按行列号拼成二维数组。
def table_ocr(image_b64): client = get_client() req = models.RecognizeTableOCRRequest() req.ImageBase64 = image_b64 resp = client.RecognizeTableOCR(req) data = json.loads(resp.to_json_string()) # TableDetections 里每个元素是一个单元格 cells = data.get("TableDetections", []) # 按行列号归位 rows = {} for c in cells: r, col = c["RowTl"], c["ColTl"] rows.setdefault(r, {})[col] = c["Text"] # 转成有序二维列表 table = [ [rows[r].get(c, "") for c in sorted(rows[r])] for r in sorted(rows) ] return table for row in table_ocr(b64): print(row)RowTl和ColTl是单元格左上角所在的行列索引,用它归位比按坐标排序更稳。注意接口返回的行列号可能不从 0 开始,也可能有跳号,所以用字典兜底、缺的补空字符串。这一步是整个流程里最容易出玄学问题的地方——手画表格线条不闭合时,接口可能把两个单元格合并,或者把一个格子拆成两个。遇到这种情况,别急着骂接口,先看原图线条是不是断的。
3.3 两条路线怎么选:一张决策表
| 场景 | 推荐接口 | 理由 |
|---|---|---|
| 零散手写字段、无表格线 | GeneralHandwritingOCR | 按坐标排序后关键词匹配即可 |
| 有表格线(印刷或手画) | RecognizeTableOCR | 直接返回行列结构,省去版面分析 |
| 表格线断断续续、手画潦草 | 先表格识别,失败再退回手写体 | 表格识别对断线敏感,手写体更鲁棒 |
| 混合排版(表格 + 大段手写) | 两个接口都调,按坐标融合 | 单接口覆盖不全 |
我一般会先跑表格识别,如果返回的单元格数量明显少于肉眼看到的格子数,就说明线条没被识别出来,这时候退回手写体识别,自己按坐标聚类成行。这个「先表格后手写」的降级策略,能覆盖九成以上的真实照片。
4. 落盘成 Excel:字段抽取、清洗与批量写入
4.1 从识别结果到结构化字段
拿到二维表格或文本块之后,下一步是把它们变成有列名的记录。如果是固定格式的表单(比如每张报销单都是「日期 / 事由 / 金额」三列),直接按列索引取值。如果是格式不固定的,就得靠关键词匹配:在文本块里找「金额」「合计」这类词,取它右边或下边的数字。
import re def extract_amount(blocks): # 在文本块里找含"金额"或"合计"的块,取同行右侧的数字 for i, b in enumerate(blocks): if "金额" in b["DetectedText"] or "合计" in b["DetectedText"]: # 用正则从当前块和相邻块里抠数字 for nb in blocks[i:i+3]: m = re.search(r"\d+\.?\d*", nb["DetectedText"]) if m: return float(m.group()) return None def extract_date(blocks): for b in blocks: m = re.search(r"\d{4}[-/年]\d{1,2}[-/月]\d{1,2}", b["DetectedText"]) if m: return m.group() return None正则\d+\.?\d*匹配整数或小数金额,\d{4}[-/年]\d{1,2}[-/月]\d{1,2}兼容2024-01-05、2024/1/5、2024年1月5日几种写法。关键词匹配的窗口取当前块往后 3 个块,是因为识别结果里「金额」和数字经常被切成两个块。这个窗口大小可以调,但别太大,否则会误抓到下一行的数字。
4.2 用 openpyxl 批量写入并保留格式
数据整理成列表后,用 openpyxl 写入。相比 pandas 的to_excel,openpyxl 能更细地控制单元格格式,比如金额列设成数字格式、表头加粗。
from openpyxl import Workbook from openpyxl.styles import Font def write_excel(records, out_path="result.xlsx"): wb = Workbook() ws = wb.active ws.title = "识别结果" headers = ["文件名", "日期", "金额", "备注"] ws.append(headers) # 表头加粗 for cell in ws[1]: cell.font = Font(bold=True) for rec in records: ws.append([rec.get(h, "") for h in headers]) # 金额列设成两位小数 for row in ws.iter_rows(min_row=2, min_col=3, max_col=3): for cell in row: cell.number_format = "0.00" wb.save(out_path) write_excel([{"文件名": "a.jpg", "日期": "2024-01-05", "金额": 128.5, "备注": ""}])ws.append按行追加,number_format = "0.00"保证金额显示两位小数。注意如果金额抽出来是字符串,写进去会变成文本格式,Excel 里没法直接求和,所以抽取阶段就要float()转好。这一步不做,后面财务用 SUM 求和时会发现结果是 0,又是一轮排查。
4.3 批量处理与失败重试
单张能跑通之后,套一层循环处理整个文件夹,并且给每张图加失败重试。网络抖动、接口限流都会导致偶发失败,不重试的话几百张里总有几张漏掉。
import os, time def batch_process(folder, out_path="result.xlsx"): records = [] for name in os.listdir(folder): if not name.lower().endswith((".jpg", ".png", ".jpeg")): continue path = os.path.join(folder, name) for attempt in range(3): # 最多重试 3 次 try: b64 = img_to_base64(path) blocks = handwriting_ocr(b64) records.append({ "文件名": name, "日期": extract_date(blocks), "金额": extract_amount(blocks), "备注": "" }) break except Exception as e: print(f"{name} 第 {attempt+1} 次失败: {e}") time.sleep(2 ** attempt) # 指数退避 else: records.append({"文件名": name, "备注": "识别失败"}) write_excel(records, out_path) batch_process("./photos")time.sleep(2 ** attempt)是指数退避,第一次等 2 秒、第二次 4 秒、第三次 8 秒,避免短时间内反复冲击接口触发限流。for...else的写法是:循环正常跑完没 break,说明三次都失败,记一条「识别失败」而不是直接丢掉,方便事后人工补。这个细节决定了你交付的表格是「完整但有几条待补」还是「悄悄少了几行」。
5. 避坑与排查:那些让我加班到凌晨的坑
5.1 现象:接口报「图片解码失败」,但图片明明能打开
原因:png 带透明通道,或者图片是 CMYK 色彩模式,接口只认 RGB 的 JPEG/PNG。解决:预处理阶段统一img.convert("RGB"),并且存成 JPEG。我现在的习惯是无论原图什么格式,一律转 RGB + JPEG 再编码,这个坑就再没出现过。
5.2 现象:手写数字「1」和「7」、「0」和「6」频繁认错
原因:手写体本身歧义大,加上照片光线不均、有阴影。解决:一是拍照时尽量正对、光线均匀,这是源头;二是识别后在业务层做校验,比如金额字段如果识别出异常大的值,标记出来人工复核。指望接口 100% 准是不现实的,工程上要留人工兜底的口子。
5.3 现象:表格识别返回的单元格行列号乱跳,拼出来的表错位
原因:手画表格线条不闭合,接口把相邻单元格合并或拆分。解决:先看返回的单元格数量,和肉眼数对比;如果差太多,退回手写体识别按坐标聚类。另外可以在预处理时做一次二值化增强线条,但对潦草手画效果有限,别抱太大期望。
5.4 现象:跑几十张后开始报「请求频率超限」
原因:默认并发或调用间隔太短,触发了接口的 QPS 限制。解决:在循环里加time.sleep(0.2)控制节奏,或者用线程池限制并发数。别一上来就开 20 个线程猛冲,限流封禁得不偿失。我一般单线程加小延时,几百张图也就几分钟的事。
5.5 现象:Excel 里金额列没法求和,显示成文本
原因:抽取阶段金额是字符串,直接写进单元格变成文本格式。解决:抽取时float()转换,写入时设number_format。如果数据里混了「¥」符号或「元」字,先用正则把非数字字符去掉再转。
6. 进阶:把识别准确率再往上抬一截的几个实操技巧
先说一个最容易被忽略的点:图片方向。手机拍的照片 EXIF 里带旋转信息,PIL 打开时不一定自动应用,导致传给接口的图是躺着的,识别率直接腰斩。我现在的固定动作是先做一次方向校正。
from PIL import Image, ImageOps def load_upright(path): img = Image.open(path) # 按 EXIF 方向信息自动旋转 img = ImageOps.exif_transpose(img) return img.convert("RGB")ImageOps.exif_transpose会读取 EXIF 的 Orientation 标签并旋转到正确方向。这一步加上之后,我手里那批横拍的单据识别率肉眼可见地涨了一截。很多人排查半天以为是接口不行,其实是图躺着了。
第二个技巧是分区域识别。如果表单格式固定,比如金额永远在右下角,你可以先用 pillow 把那个区域裁出来单独识别,减少无关文字干扰,准确率比整图识别高。裁剪坐标可以先用一张样图试出来,写死成比例。
def crop_region(img, left_r, top_r, right_r, bottom_r): w, h = img.size box = (int(w*left_r), int(h*top_r), int(w*right_r), int(h*bottom_r)) return img.crop(box) # 裁右下角 40% x 20% 的区域专门识别金额 region = crop_region(load_upright("报销单.jpg"), 0.6, 0.8, 1.0, 1.0)比例参数按你的表单实际布局调,先用一张图打印出裁剪结果确认位置对了再批量跑。
第三个是结果校验规则。识别完之后加一层业务校验:日期是否在合理范围、金额是否为正数、必填字段是否为空。校验不过的单独输出到一个「待复核」sheet,而不是混在正常数据里。这样交付出去的表格,财务一眼就知道哪些要人工看。
| 技巧 | 作用 | 代价 |
|---|---|---|
| EXIF 方向校正 | 修正躺图,识别率显著提升 | 几乎无 |
| 分区域裁剪 | 减少干扰,定点字段更准 | 需预先确定区域比例 |
| 业务规则校验 | 拦截明显错误,减少人工复核量 | 需定义校验规则 |
| 指数退避重试 | 降低偶发失败漏单 | 失败时耗时略增 |
最后说个习惯。从那以后我每次做这类批量识别,都会先拿 5 张最有代表性的图(最糊的、手写最潦草的、表格线最断的)跑一遍全流程,确认字段抽取和落盘都对,再放开跑全量。因为全量跑完才发现字段错位,返工的成本远高于先试这 5 张。这套 Python + 腾讯云 OCR 的组合,把几百张照片变成一张能筛选能求和的 Excel,中间的门道基本都在上面这些细节里了。希望帮到你。
本文还有配套的精品资源,点击获取