首页 / 资讯中心 / 文章详情

用千问大模型实现文档表格转JSON的完整实践指南

用千问大模型实现文档表格转JSON的完整实践指南 ★ FEATURED ARTICLE
每次拿到一堆PDF、Word或者扫描件里面有大量表格数据需要录入系统我都觉得是体力活。直到我把千问大模型接进来让它把文档里的表格信息直接吐成JSON整个流程才真正跑通。这篇文章就想把我在这个项目里的完整思路、踩过的坑、写好的Prompt和能直接用的代码分享出来。先交代一下背景我手里的文档来源很杂有供应商发货单、质检报告、合同附件格式五花八门。需求很明确——把表格内容提取出来转成结构化JSON方便入库和后续做统计分析。传统OCR方案我试过几套对小表格还行遇到合并单元格、多级表头、跨页表格输出的结果基本没法直接用还得人工校对半天。后面转向千问大模型来兜底效果出乎意料地好但也踩了不少坑。这篇主要写给正在做文档结构化、表格信息抽取的开发者不管你是用Python还是Java只要懂HTTP调用和基础JSON操作都能跟着复现。1. 方案整体设计与模型选型1.1 表格识别的痛点为什么OCR不够用表格数据提取这件事本质上不是“把字认出来”而是“把表格的二维结构还原出来”。传统OCR擅长的是字符识别的层面图片里有一行字它把字给你认出来但对于“这个单元格跨了几列”、“这个数字对应的是哪个月份”、“上级表头是怎么分组的”这类结构信息常规OCR就抓瞎了。举个例子一张发货单里表格的表头可能是两级的第一行是“2025年一季度”下面拆成“一月份”、“二月份”、“三月份”每一列下面再对应数量和金额。传统OCR输出往往是一堆文本块你得自己写规则去对齐、去推断层级。而大模型天生能理解语义和结构只要把表格内容以文本或图片形式喂进去它能在理解表格含义的基础上直接输出结构化数据。我用千问的实测感受是它对“表格结构”的理解能力远超预期尤其是带视觉能力的模型能直接看图里的单元格边界合并单元格它也能通过语义判断出来。不过模型能力再强也不能直接把原始文档扔进去求输出需要在前置和后置环节做好设计。1.2 千问系列模型怎么选千问系列模型并不是只有一款选型直接决定了效果天花板。我这边按表格识别场景整理了三个可用的方向模型方向输入类型适用场景我的推荐纯文本模型文本文档能直接提取出文字内容且表格结构简单处理干净的电子表格类PDF视觉模型文本图片扫描件、图片型PDF、复杂表格手写体之外的绝大多数场景长文本模型文本超长文档、多页表格需要一次性读完合同、报告类长文档实际操作下来我主力用的是视觉模型因为很多文档的排版一旦转成文本再恢复表格结构信息就已经丢了。视觉模型直接看图片对单元格位置的感知更准确。如果文档是原生可复制文字的PDF我会先尝试提取文本再决定要不要走图片识别路线。选模型时还有个性能权衡点视觉模型处理单张图片的速度明显比纯文本慢成本也更高。所以我的策略是“能转文本的先转文本文本提取出来的表格结构乱了再升级到视觉模型”。这种方式既控制了成本又保证了准确率。1.3 整体流程设计整个方案我拆成了五个环节文档解析 - 内容切分 - 构造Prompt - 模型调用 - JSON校验与修正。下面把每个环节的职责说清楚。文档解析环节解决的是“怎么把PDF、Word、图片变成模型能理解的内容”包括提取文本、渲染成图片、做预处理。内容切分环节解决的是“文档太长模型一次吃不下怎么办”核心是让每个请求保持合适的上下文长度。构造Prompt环节解决的是“怎么让模型输出可靠JSON”这是整个项目最核心的部分后面有专门一节展开讲。模型调用环节就很简单了把请求发出去拿结果。最后一个校验环节最容易被忽略大模型输出有概率性不能假设它100%稳定要做JSON格式校验、字段完整性校验、内容合理性校验不合格就自动重跑。这五个环节里前两个环节出错会导致后续全部白费所以我会在文档解析之后先做一个人工抽检确认输入没问题再继续往下跑。2. 文档预处理与关键细节2.1 PDF要不要转图片很多人第一步就卡住了PDF到底直接转文本还是转图片给模型我的判断标准是这样的先看PDF是“文字型”还是“图片型”。文字型PDF可以直接提取文本CtrlA能选中的那种就是图片型PDF本质是扫描图要么靠OCR要么直接把整页渲染成图片喂给视觉模型。对于文字型PDF我通常先用文本提取方案。但这里有个陷阱PDF里的文本流顺序不一定是视觉顺序遇到多栏排版、文本框嵌套时提取出来的文本顺序是乱的表格结构自然也就歪了。这种情况我会放弃纯文本方案把页面渲染成图片交给视觉模型处理。对于图片型PDF传统做法是先OCR再拼结构现在直接渲染成PNG给视觉模型一步到位。千问视觉模型对中文印刷体的识别准确率非常高省去了串联多个工具的复杂度。2.2 表格切片与分辨率控制文档预处理里最容易翻车的细节是图片过大或过小。图片过大模型接收的token消耗会显著增加而且有些API对图片尺寸有上限图片过小表格里的文字模糊识别准确率断崖式下降。我的经验是单张图片宽度控制在1500到2000像素输出格式用PNG保证文字边缘清晰。过宽的表格优先缩小尺寸而不是切图因为切片容易切断表格线影响结构识别。如果缩小到极限还是识别不清那就按列做切割但切割时必须保留表头和对应列的上下文不能每列单独飘出去识别。这里有个实际案例一张A3纸大小的仓库库存表列数非常多缩小后字体太小看不清。我按列切割成左右两部分每部分都包含表头行再分别识别最后在JSON合并阶段拼接。合并时要注意列名冲突问题左右两部分的表头文字一样JSON的字段名会用不同前缀区分。2.3 调用参数的设置模型调用参数里最关键的是这三个temperature、max_tokens、response_format。temperature控制随机性表格提取是“确定性任务”不需要创造力我直接拉到0。某些平台的API最低能调到0这里建议不要调成负数不合法0就是最稳的。max_tokens决定了输出上限。表格数据量大的时候输出JSON可能超过2000个token默认值往往不够。我的经验是把max_tokens设置成输入图片对应token消耗的2到3倍宁可多给避免输出被截断。response_format这项尤其重要。千问的API支持JSON模式在请求里显式声明输出为JSON对象模型会更倾向于输出干净的结构化数据而不是夹带描述性文字。声明了JSON模式也不代表一定合法后面我会专门讲校验兜底。3. Prompt设计与JSON结构约束3.1 让模型稳定输出JSON的关键写法Prompt是整条流水线的灵魂写得好不好效果差出一大截。我总结出一套稳定的写法模板分几层来写第一层是任务描述直接说清楚你要干什么事“你是表格结构化助手请从输入的表格内容中提取数据严格按照JSON格式输出”。第二层是输出结构定义用代码块给出一个示例或JSON Schema。这是最关键的一层模型看到明确的键名和值类型输出才会稳定。比如你要提取发货单就写{ 订单编号: 字符串, 发货日期: YYYY-MM-DD, 商品明细: [ { 商品名称: 字符串, 数量: 数字, 单价: 数字, 金额: 数字 } ] }第三层是约束说明要写清楚“只输出JSON不要任何解释”“没有数据的字段填null而不是忽略”“数字不要带单位”。这些约束看着琐碎但每条都能减少后续清洗的工作量。我踩过一个很典型的坑第一次跑的时候没写“数字不要带单位”模型把所有价格字段都输出成了“12.5元”“100/箱”这种带单位的字符串前端直接拿它做数值计算全报类型错误。后来在Prompt里加了一条强约束这个现象就基本消失了。3.2 复杂表格的结构化规则日常文档里最恶心的表格是合并单元格和多级表头。以多级表头为例普通的行列对应关系已经失效了必须让模型输出嵌套结构才能保留层级语义。我给模型定义规则时是这么写的上级表头作为对象字段名下级表头作为该对象下的子字段。举例来说2025年一季度2025年一季度2025年二季度一月二月三月对应JSON结构就是{ 2025年一季度: { 一月: 数据, 二月: 数据, 三月: 数据 }, 2025年二季度: {} }合并单元格的处理要麻烦一点。单元格向下合并时模型容易把空的单元格当成null但实际上它应该继承上方单元格的值。我在Prompt里专门写了一条“遇到纵向合并单元格该单元格的值与上方单元格相同请填充相同值不要输出null”。这条规则加入后纵向合并表格的提取准确率提升很明显。3.3 多轮修正和自校验机制prompt写得再好也挡不住复杂表格的意外情况。我设计的方案是“一次识别 一次校验修正”。第一次调用模型给出JSON。我先做程序化校验检查JSON是否可解析、必填字段是否存在、数组长度是否和数据行数匹配。如果校验失败我把错误信息和原始表格图片一起放进第二次请求让模型自己看看哪里出问题了然后重新输出修正后的JSON。这个方案在实践中有效解决了两个问题一个是表格在图片里被截断导致的数据缺失模型二次审视时往往能通过上下文补回来另一个是模型偶尔会创作出表格里根本不存在的字段校验时把Schema对不上的字段标出来第二次模型就会收敛。4. 核心代码实现与调通记录4.1 环境准备与API接入环境方面我用的Python 3.10安装了openai库和pymupdf库用来处理PDF转图片。千问的API兼容OpenAI调用格式只需要把base_url指向千问服务的兼容地址再把api_key换成自己的就行。pip install openai pymupdf代码里的api_key建议放在环境变量里不要硬编码进源码。我平时是在本地调试时读取.env文件部署到服务器时再注入环境变量。4.2 完整的识别与解析代码下面这段代码是实战里跑通的版本包括PDF渲染成图片、图片转base64、调用视觉模型识别表格、取出JSON结果。核心逻辑已经注释出来了。import os import base64 import json import fitz # PyMuPDF from openai import OpenAI client OpenAI( api_keyos.getenv(DASHSCOPE_API_KEY), base_urlhttps://dashscope.aliyuncs.com/compatible-mode/v1 ) def pdf_page_to_base64(pdf_path, page_index): doc fitz.open(pdf_path) page doc[page_index] pix page.get_pixmap(matrixfitz.Matrix(2, 2)) # 2倍缩放提高清晰度 img_bytes pix.tobytes(png) return base64.b64encode(img_bytes).decode(utf-8) def extract_table_to_json(pdf_path, page_index): img_base64 pdf_page_to_base64(pdf_path, page_index) prompt 你是表格结构化助手。请提取图片中的表格内容并以JSON格式输出。 约束 1. 只输出JSON不要任何解释或前后缀 2. 数字字段不要带单位纯数字 3. 合并单元格的纵向继承值不要输出null 4. 表格为空则输出空数组 输出结构示例 {rows: [{列名1: 值1, 列名2: 123}]} response client.chat.completions.create( modelqwen-vl-max, messages[ {role: user, content: [ {type: image_url, image_url: {url: fdata:image/png;base64,{img_base64}}}, {type: text, text: prompt} ]} ], temperature0, max_tokens2048, response_format{type: json_object} ) return json.loads(response.choices[0].message.content) # 使用示例 result extract_table_to_json(发货单.pdf, 0) print(json.dumps(result, ensure_asciiFalse, indent2))跑通这段代码的前提是几点图片不能太模糊、Prompt约束要清晰、max_tokens要给够。我最初调的时候max_tokens只给512遇到一张数据稍微多一点的表输出直接截断了JSON解析直接报错。后来改成2048问题就消失了。这里补充一个细节response_format{type: json_object}在某些模型版本上要求Prompt里必须出现“json”这个词否则API会报错。所以Prompt里一定要保留“以JSON格式输出”这样的字眼这是兼容性小坑。4.3 结果校验与异常兜底调用模型拿到JSON只是第一步真正的生产级代码还需要加一层校验。我最常用的校验函数长这样def validate_result(obj): if not isinstance(obj, dict): raise ValueError(结果不是对象) if rows not in obj: raise ValueError(缺少rows字段) if not isinstance(obj[rows], list): raise ValueError(rows不是数组) if len(obj[rows]) 0: print(警告表格为空请确认文档页是否正确) # 这里可以按业务字段补充必填项检查 return True校验不通过时我会触发重试逻辑最多重试三次。重试时把上一次的错误信息拼进Prompt让模型修正。重试三次后仍然失败就把这一页记录下来进入人工处理队列兜底。5. 常见问题与排查经验5.1 表格内容顺序错乱这是最频繁遇到的问题尤其是PDF不是标准表格排版时模型会把数据行的顺序打乱或者把列对应错位。排查思路是这样的先确定输入的图片方向是否正确PDF渲染可能会出现页面被裁剪的情况检查图片是否完整再检查Prompt里是否强调了“严格按原始表格的行列顺序输出”。加了这个强调后顺序错乱问题减少了很多。如果还出现顺序问题就得检查表格里是否有跨行的合并单元格这种格子最容易让模型产生歧义。我在一张人员信息表里遇到过一个“部门”列跨了三行模型在输出后面两行数据时把部门字段填成了null按前面的规则处理后恢复正常了。5.2 JSON格式非法API已经声明了JSON模式但偶尔还是会出现JSON解析失败的情况原因通常是输出里带了markdown代码块标记比如json和这种情况用正则剥掉就行import re def extract_json_str(text): match re.search(r(?:json)?\s*([\s\S]*?)\s*, text) if match: return match.group(1) return text.strip()另一个常见原因是输出被截断max_tokens不够。截断时JSON到一半就断了校验时会直接抛JSONDecodeError。这种情况除了调大max_tokens还可以用流式输出提前判断进度但这套方案适合服务端异步场景本地脚本里直接调大token上限最简单。5.3 多页表格和超大文档单页Tables还好应对多页连续的表格就比较棘手了。比如一份十页的采购单表头只在第一页后面每一页都是数据行。如果每一页单独识别模型不知道列名是什么出来的JSON键名就会变成“列1”“列2”。我的处理方式是第一页单独识别获取表头结构后面续页把第一页输出JSON的表头拼进Prompt让模型沿用同一个键名输出。这样整个文档的JSON结构就完全统一了。如果是几十页的文档逐页调用API的成本会很高而且每页都有延迟。这边建议在文档解析环节就做表格区域定位判断哪些页面确实包含表格把空白页、纯文字页筛掉从源头上减少无效调用。6. 成本控制与后续扩展方向6.1 成本与效率实测用的视觉模型单张图片的token消耗大约是800到1500 token加上输出JSON的token每页的综合消耗在2000到3000 token之间。实际跑了一批100页的混合文档准确率大约在90%以上剩余少量问题集中在手写体和极度复杂的多级嵌套表头上。成本这块如果用更低配的模型单页消耗能节省一半但准确率会下降。我的建议是先跑一批测试集计算准确率和成本之间的平衡点再决定用什么档位的模型。生产环境最好做两层常规表格用低成本档位复杂表格自动升级到高配模型。6.2 方案扩展与应用场景这个方案不只是“识别发货单”稍微改一下Prompt和输出结构就能扩展到很多场景银行对账单转为流水JSON、简历信息结构化、合同条款提取、发票全字段识别甚至仓库的erp系统可以直接通过这个通道把纸质单据扫进数据库。还有一个可以做的改造把识别结果自动对比源表格用程序做行数和关键字段的交叉校验。我在后续迭代中已经把这部分加进去了相当于给模型输出加了一层“机器校验”错误率进一步下降。如果文档量再涨一个量级可以考虑把同步调用改成异步任务队列把整个识别过程拆成“上传文档 - 后台解析 - 回调结果”的标准服务前端只需要等回调。这套架构在生产环境已经跑得很稳。最后分享一个很小的调试技巧千问模型对“表格标题”和“表格内容”的处理粒度不同如果某列总是识别错试着在表头行后面加一个“单位”注释行比如“商品名称品牌”“金额元”模型反而会理解得更准。这属于Prompt工程里的暗经验多试几次就慢慢摸出门道了。
阅读完成 · 觉得有帮助?
咨询建站