☰
DeepSeek API返回JSON自动转Excel的全链路方案
2026/10/14 4:04:04 网站建设 项目流程

DeepSeek 这类大模型 API 接入业务系统时,很多团队只关注对话和文本生成,忽略了一个高频刚需——把结构化结果落成表格。我年初帮某公司做了一套智能报表流水线,核心就是让 DeepSeek 返回的 JSON 数据自动转成 Excel,再按计划任务推给业务方。这中间踩了不少坑,也从 API 解析、列映射、格式美化到自动化调度,完整趟出了一条可复用的链路。这篇文章把我这套全链路方案拆开讲清楚,从请求构造到最终文件落地,每一步都会给出可直接参考的实现细节,适合正在做 AI 数据落地、报表生成、接口集成的开发者。

1. 整体方案设计与技术选型

1.1 为什么表格导出不能只靠“另存为 CSV”

很多人在接大模型 API 之前,天然觉得数据导出很简单:拿到 JSON 直接写 CSV 不就行了?实际做业务系统时完全不是这么回事。业务方要的表格通常带合并单元格、字段别名、百分比格式、日期格式、多级表头,甚至还要按某个字段拆分多个 sheet。CSV 虽然通用,但是一旦字段里出现逗号、换行、UTF-8 BOM 问题,再加上 Excel 打开 CSV 的编码兼容问题,麻烦会接踵而至。

另一个更隐蔽的问题是:大模型返回的 JSON 结构并不稳定。同一份数据有时候字段名是product_name,有时候变成productName,有时候多嵌套了一层data.items。如果直接把 JSON 拍平写进表格,运气好的时候能跑通,运气差的时候表格里全是空列。所以整个方案的第一步不是写导出代码,而是先定一个标准化的中间数据结构,让 API 输出的“野生 JSON”经过清洗后变成表格能直接消费的“规范行集”。

我在这套方案里最终选择了三层架构:上游请求层负责把用户自然语言查询转成 DeepSeek 能理解的任务指令并拿到稳定 JSON;中间解析层负责字段映射、类型推断和缺失值处理;下游导出层负责把干净的表格数据渲染成多种格式。这样每层都能独立测试、独立替换,坏了哪一环都不至于推倒重来。

1.2 技术栈选型:为什么是 Python + pandas + openpyxl

技术栈的选择要结合团队现有能力和运维成本。我们最终定的是 Python 3.10 + requests + pandas + openpyxl,调度用 APScheduler,部署在一台普通 Linux 服务器上。选择 Python 的理由很直白:AI 生态和数据生态都在这里,DeepSeek 官方 SDK 也提供了 Python 包,后续要接数据清洗、统计分析甚至可视化,在同一个进程内做管道式处理最省事。

表格导出环节,pandas 负责数据结构和类型转换,openpyxl 负责 Excel 的细粒度控制。为什么不直接用 pandas 的to_excel?因为to_excel底层如果指定了 openpyxl engine,虽然能写自定义 sheet,但要做列宽、冻结点、单元格样式、超链接这些润色操作,还是得拿到 openpyxl 的 workbook 句柄来二次加工。所以我的标准做法是:pandas 只做中间表,openpyxl 做最终渲染,两者配合而不是互相替代。

依赖安装也比较干净,一条命令就能解决:

pip install pandas openpyxl requests apscheduler deepseek-api

这里提醒一句:deepseek-api是社区的封装包,如果不放心,直接用 requests 调 HTTP 接口也完全可以。我在生产环境里其实更倾向于直接用 requests,因为少一层封装就少一层排查负担,后面讲请求层的时候会给出完整示例。

1.3 目录结构与模块划分

刚开始做这个项目时,我把所有逻辑都塞在一个脚本里,后来发现改一个表头格式就要到处找代码,实在太痛苦。后来我按职责拆成了下面这样的目录结构:

deepseek_export/ ├── main.py # 入口,初始化调度器 ├── config.py # 全局配置(API Key、导出路径、模板配置) ├── api_client/ │ ├── deepseek_client.py # DeepSeek API 请求与重试逻辑 │ └── prompt_templates.py # 提示词模板,保证 JSON 输出稳定 ├── parser/ │ ├── field_mapper.py # 字段映射与别名归一 │ └── type_inferer.py # 类型推断与格式转换 ├── exporter/ │ ├── excel_exporter.py # openpyxl 渲染引擎 │ └── csv_exporter.py # 兜底 CSV 导出 └── scheduler/ └── job_manager.py # APScheduler 任务注册

模块化之后,每个文件的职责单一,想要增加一个 JSON 转 Markdown 表格的导出器,只要在exporter目录下新增一个类就行,不需要改动上游逻辑。

2. API 解析与数据预处理全流程

2.1 构造稳定的提示词模板

DeepSeek API 返回结果的质量,很大程度上由提示词决定。要让表格导出稳定,我建议在提示词里明确要求输出 JSON,并给定一个严格的格式示例。这里有一个关键经验:不要只告诉模型“输出 JSON”,要给出完整的 schema 示例,否则它会自由发挥。

我当时在prompt_templates.py里维护了这样一套模板:

EXTRACTION_TEMPLATE = """ 你是一个数据抽取助手。请从以下文本中提取结构化信息。 要求: 1. 只输出 JSON,不输出任何解释文字。 2. JSON 格式必须如下: { "items": [ {"日期": "2026-01-01", "品类": "手机", "销售额": 12345.6, "门店": "华东一店"} ] } 字段说明: - 日期:标准日期格式 YYYY-MM-DD - 销售额:数字类型,不要带货币符号 - 门店:文本类型 文本内容: {input_text} """

这套模板的核心思路是:给模型一个“锚点”结构,同时用字段说明约束类型。实测下来,只要字段说明写清楚,模型输出的成功率能到 95% 以上。剩下 5% 的脏数据,就靠下一层的解析逻辑来兜底。

2.2 请求构造与异常重试

API 请求本身不难,难在稳定。一个典型的调用流程包括:组装请求体、设置超时、处理 HTTP 错误、解析业务错误码。DeepSeek 的接口遵循 OpenAI 兼容风格,所以请求体长这样:

import requests def call_deepseek(prompt, api_key, model="deepseek-chat"): url = "https://api.deepseek.com/chat/completions" headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = { "model": model, "messages": [ {"role": "system", "content": "你是一个严谨的数据抽取助手。"}, {"role": "user", "content": prompt} ], "temperature": 0, "response_format": {"type": "json_object"} } resp = requests.post(url, headers=headers, json=payload, timeout=30) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"]

注意temperature必须设置为 0。表格导出的场景需要的是确定性和可复现性,不是创造性,temperature 设得越高,字段值就越容易飘。另外response_format指定json_object后,DeepSeek 会尽量保证输出是合法 JSON,这能省掉很多解析时的“抢救”工作。

关于重试:我实现了三层重试策略。第一层是连接超时重试,使用 requests 自带的适配器;第二层是针对 HTTP 429/500 的指数退避重试,最多重试 3 次;第三层是结果校验重试,也就是解析 JSON 失败后,把错误信息反馈给模型,让它重新生成。第三层非常有用,它比本地硬解析要聪明得多。

2.3 字段映射与“野生 JSON”归一化

拿到模型返回的 JSON 字符串后,第一步是json.loads,但这里要小心:模型可能输出 Markdown 代码块包裹的 JSON,比如:

```json {"items": [...]}
直接 `json.loads` 会报错。我的处理办法是把字符串里的 ```` ```json ```` 和 ```` ``` ```` 标签剥掉再做解析,这看起来蠢,但确实是高频真实情况。 字段映射是我认为全链路中最核心的一环。大模型输出字段名经常漂移,比如有时候是 `"销售总额"`,有时候是 `"total_sales"`。我写了一个字段归一化器,把所有可能的字段名映射到一个标准字段上: ```python ALIAS_MAP = { "日期": ["日期", "date", "day", "统计日期"], "品类": ["品类", "category", "cat", "商品类别"], "销售额": ["销售额", "sales", "amount", "total_sales", "销售总额"], "门店": ["门店", "store", "shop", "分店名称"] } def normalize_item(raw_item): normalized = {} for std_field, aliases in ALIAS_MAP.items(): for key, value in raw_item.items(): if key.strip() in aliases: normalized[std_field] = value break return normalized

这里我刻意没有做模糊匹配,只做了精确别名匹配。原因是模糊匹配(比如 levenshtein 距离)在小数据量下表现尚可,但业务字段一旦增加,误匹配率会迅速升高,而且排查起来很痛苦。维护一张显式的别名表虽然笨,但团队其他人接手时一目了然,出问题也能快速定位。

2.4 类型推断与缺失值填充

表格里每一列都应该有明确的类型。我在type_inferer.py里实现了一套基于采样数据的类型推断逻辑:遍历某一列的所有值,逐个尝试转日期、转浮点、转整数,如果某一个类型能覆盖 90% 以上的值,就按这个类型做转换,剩余无法转换的值统一标记为空或待人工确认。

这里要特别注意销售额这类数值字段。模型可能输出"12,345.6"这种带千分位逗号的字符串,如果直接塞进 Excel,会被当成文本,后面做透视表就没法求和。我的处理是:先去掉千分位逗号,再去掉货币符号,最后转 float。日期字段也一样,统一转成YYYY-MM-DD格式,避免 Excel 识别成dd/mm/yyyy造成歧义。

缺失值不能随意填 0。业务场景里,缺失可能表示“没有数据”,也可能表示“数据搜集失败”,这两种情况的处理方式完全不同。我的方案是增加一个标记列,比如销售额_缺失标记,值为 1 表示原始数据缺失,这样后续统计时能单独过滤,而不是和真实 0 值混在一起。

3. 表格导出核心实现与细节打磨

3.1 生成多 sheet 业务报表

业务方通常不满足于一张表。比如这家公司需要按月份拆 sheet,每个月一张,同时还有一个“汇总”sheet 在最前面放总计。pandas 的ExcelWriter可以轻松实现多 sheet 写入,但要做样式润色,最好直接操作 openpyxl。

我的实现流程分成三步:先把中间数据按月份分组,每个分组转成 DataFrame;然后写出每个 sheet 的基础数据;最后遍历所有 sheet 做样式统一调整。关键代码如下:

import pandas as pd from openpyxl.utils.dataframe import dataframe_to_rows from openpyxl.styles import Font, PatternFill, Alignment, Border, Side def export_multi_sheet(file_path, all_records): df = pd.DataFrame(all_records) df["月份"] = pd.to_datetime(df["日期"]).dt.strftime("%Y-%m") wb = Workbook() summary_sheet = wb.active summary_sheet.title = "汇总" # 写入汇总逻辑省略 for month, group in df.groupby("月份"): ws = wb.create_sheet(title=month) for row in dataframe_to_rows(group, index=False, header=True): ws.append(row) auto_format_sheet(ws) wb.save(file_path)

注意dataframe_to_rows会把 DataFrame 的列名和值都迭代出来,省去了手工遍历单元格的繁琐。不过在写入之前一定要确认 DataFrame 的列顺序已经调整好,因为在 DataFrame 里重排列顺序代价小,但在 Excel 里挪列就要牵扯整列操作了。

3.2 列宽、冻结窗格与单元格格式化

表格导出的体验差距,往往就体现在这些“润色”细节上。业务方拿到一个列宽参差不齐、表头没有底色、滚动超过 50 行就看不到标题的表格,心里好感度会直线下降。我在auto_format_sheet里面做了这样几件事:

首先,根据列内容的实际长度自动估算列宽。中文按 2 个单位宽度计算,英文字符按 1 个单位,数字按 1.2 个单位,然后乘以 1.1 的冗余系数,上限设置为 40,避免出现内容很长的备注字段把整张表撑爆。

其次,表头设置为加粗 + 浅灰色填充 + 居中对齐,冻结首行。这里有个技巧:如果表格中有“汇总”行,最好在汇总行上方加一个细分隔线,并单独加粗。部分业务方喜欢带筛选器,我就在表头行执行ws.auto_filter.ref = ws.dimensions,这样他们可以直接下拉筛选。

封装一个独立函数是必要的:

THIN_BORDER = Border( left=Side(style="thin"), right=Side(style="thin"), top=Side(style="thin"), bottom=Side(style="thin") ) def auto_format_sheet(ws): header_font = Font(bold=True, color="FFFFFF") header_fill = PatternFill("solid", fgColor="4472C4") for cell in ws[1]: cell.font = header_font cell.fill = header_fill cell.alignment = Alignment(horizontal="center", vertical="center") cell.border = THIN_BORDER ws.freeze_panes = "A2" # 估算列宽 for col_cells in ws.columns: max_len = 0 col_letter = col_cells[0].column_letter for cell in col_cells: val = cell.value if val is not None: length = sum(2 if ord(ch) > 255 else 1 for ch in str(val)) max_len = max(max_len, length) ws.column_dimensions[col_letter].width = min(max_len + 2, 40)

冻结窗格选择A2是最通用的做法,表头占一行时就用它;如果表头占两行,就要改成A3。这一步最好通过配置项控制,不要写死。

3.3 大文件导出的内存控制

当记录数超过几万行时,exporter 很容易吃光内存。主要原因是 pandas 一次性读入所有记录,同时 openpyxl 在保存前也会持有完整的工作表对象。我的经验是,超过 5 万行建议不要走 pandas 中转,直接按行写 openpyxl,或者使用openpyxl.worksheet._writer的只写模式。

from openpyxl import Workbook wb = Workbook(write_only=True) ws = wb.create_sheet() ws.append(["日期", "品类", "销售额", "门店"]) for record in record_iterator: # 改用迭代器,而不是全量列表 ws.append([record["日期"], record["品类"], record["销售额"], record["门店"]]) wb.save("large_export.xlsx")

只写模式最大的优点是内存占用基本恒定,数据量翻倍,内存不会跟着翻倍。缺点是没法在写完后轻松改动某个单元格样式,所以如果你的文件不需要太多样式,就用只写模式;需要样式,就要权衡吞吐量和表现力。我通常采取一个折中:数据量大时先只写模式落盘,再用 openpyxl 的 load_workbook 在关键行(比如表头和汇总行)做二次样式加工,这样内存峰值可控,也能保住必要的界面体验。

3.4 CSV 兜底导出方案

并不是所有下游系统都适合接收 Excel。有些老系统只认 CSV,而且用的是 GBK 编码。所以我在csv_exporter.py里做了一版兜底导出:支持指定编码(默认 UTF-8-SIG),支持自定义分隔符,支持给文本字段加引号。

这里有一个容易踩的坑:如果字段内容包含分隔符或换行,CSV 的引号处理必须依赖csv模块,不能手工用字符串拼接。手工拼接的结果是一旦字段里有逗号,整个文件的列数就乱了。正确做法是:

import csv def export_csv(file_path, headers, rows, encoding="utf-8-sig"): with open(file_path, "w", newline="", encoding=encoding) as f: writer = csv.writer(f) writer.writerow(headers) writer.writerows(rows)

另外提一下utf-8-sig:如果你直接写utf-8,Windows 上的 Excel 双击打开 CSV 会乱码,因为缺少 BOM。utf-8-sig会在文件头部自动写入 BOM 标记,让 Excel 正确识别编码。如果对接的是 Linux 上的后续脚本,可能更希望用纯utf-8不带 BOM,这个按下游系统要求配置就行。

4. 自动化落地与运行保障

4.1 定时调度与任务注册

自动化离不开调度。我在这套系统里选用 APScheduler 而非系统 cron,原因是它能在应用内管理任务状态,并且方便动态增删任务。当前场景是每天凌晨 3 点执行一次全量导出,上午 9 点还要跑一次增量更新,因此我配置了多个触发器。

from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.cron import CronTrigger scheduler = BlockingScheduler() @scheduler.scheduled_job(CronTrigger(day_of_week="mon-fri", hour=3, minute=0)) def daily_full_export(): run_full_export() @scheduler.scheduled_job(CronTrigger(day_of_week="mon-fri", hour=9, minute=0)) def daily_incremental_update(): run_incremental_update() if __name__ == "__main__": scheduler.start()

这里要强调:定时任务的执行环境和交互式环境不一样,必须考虑进程守护。我用的方案是 systemd 服务托管,把启动命令写成一个 service 文件,设置Restart=always,这样进程即使挂了也能自动拉起。日志输出要走 stdout 重定向到文件,或者用 Python logging 写入固定路径,否则排查问题时两眼一抹黑。

4.2 幂等导出与增量同步

导出的自动化最怕重复执行产生脏数据。比如定时任务重跑了一次,结果把同一个月的报表追加了两遍,业务方看到数据翻倍,基本就会对你失去信任。解决这个问题的方法就是幂等:每次全量导出前,先把目标目录下已存在的对应文件删除;增量导出时,基于某个业务主键做去重。

我在增量同步里维护了一张export_state.json,记录每个数据源最近一次成功导出的时间戳和主键集合。每次跑增量任务时先读这份状态文件,以主键为准过滤掉已经导出的行,再追加到 Excel 中。这样即使同一个批次被重复执行,也不会产生重复行。

def load_state(): if os.path.exists("export_state.json"): with open("export_state.json", "r", encoding="utf-8") as f: return json.load(f) return {"last_exclude_keys": [], "last_export_time": ""} def save_state(new_state): with open("export_state.json", "w", encoding="utf-8") as f: json.dump(new_state, f, ensure_ascii=False, indent=2)

4.3 失败重试与告警通知

API 调用不可能永远稳定。我遇到过 DeepSeek 接口偶尔返回 500,还有一次是请求超时导致整个任务中断。因此全链路里必须加失败重试和告警。重试策略前面已经说了三层,这里重点说告警。

我用的方案是:任务失败后,把错误堆栈、失败任务名、失败批次号整理成一条消息,调用企业微信机器人 webhook 推送到指定群。这样即使半夜任务挂了,第二天早上大家心里也有数。webhook 实现非常简单:

def send_alert(message): url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY" data = {"msgtype": "text", "text": {"content": message}} requests.post(url, json=data, timeout=10)

关键细节是请求 webhook 本身也可能失败,所以告警函数要做一次 try-except,不能让告警逻辑反过来拖垮主流程。另外,连续多次失败时不要每失败一次就发一条消息,否则一个故障能刷屏几十条。我在告警模块里加了静默窗口,同一任务 30 分钟内最多推送 3 条。

4.4 运行监控与报表体检

任务跑通了并不代表表格是对的。我还给整个流水线加了一个“体检”环节:导出完成后,自动读取生成的 Excel,检查列数是否与模板一致、行数是否在合理范围内、是否有整列为空、是否有异常的重复主键。这些检查如果发现异常,直接判定本次导出失败并触发告警,而不是把坏文件发给业务方。

这份体检逻辑也可以做成定时任务,每天对前一天的报表做一次滞后校验。这样即使导出时没有发现问题,后期数据更新导致的口径偏差也能被及时捕获。

5. 常见问题与排查技巧实录

5.1 模型返回 JSON 解析失败的三种救法

JSON 解析失败是全链路里最高频的问题,典型情况有三种:一是模型输出被 Markdown 代码块包裹;二是字段名带了特殊字符;三是模型在 JSON 前后加了冗余说明文字。

第一种情况的解法是剥掉代码块标记,前面已经说过;第二种是清理非法字段名,比如把字段名中的空格、点号、冒号替换成下划线;第三种是定位 JSON 的起始花括号和结束花括号,截取中间部分再解析。我写了一个健壮性较强的解析函数:

import json, re def robust_json_loads(text): text = text.strip() if text.startswith("```"): text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text, flags=re.MULTILINE) start = text.find("{") end = text.rfind("}") if start != -1 and end != -1: text = text[start:end+1] return json.loads(text)

这个函数不能处理所有情况,但能解决我遇到的 90% 的脏数据。剩下 10% 的情况,就靠 2.2 节说的“结果校验重试”——把解析错误反馈给模型,让它重新生成。

5.2 Excel 日期变成数字的问题

如果你直接把2026-01-05这样的字符串写进 openpyxl 的单元格,Excel 通常能识别成日期,但有时候会显示成一串数字(其实是 Excel 内部的日期序列号)。这取决于单元格预设格式。

解决办法是在写入日期单元格后明确设置number_format:

cell.number_format = "YYYY-MM-DD"

批量设置时可以遍历指定列的所有行。同样的问题也会出现在百分比字段上,建议统一设置:

cell.number_format = "0.00%"

5.3 内存溢出与 OOM 处置

我们的导出任务曾经因为单次拉取 20 万条记录导致 OOM,服务器直接卡死。后来我做了三处优化:一是 API 拉取数据改成了分页,每页 1000 条;二是导出流程改成了边拉边写,不保留全量内存数据;三是给写入 Excel 的工作簿启用了只写模式。这样处理后,单次处理 20 万条记录的内存峰值控制在 300MB 以内,服务器跑得很稳。

5.4 任务重复执行导致重复数据

这个问题在 4.2 节已经说了幂等方案,但这里要补充一个排查技巧:当你发现数据重复时,除了检查任务是否重复执行,还要看状态文件是否正确更新。有时候任务在导出中途异常退出,状态文件没来得及写入,下一次执行就会把同一个批次再处理一遍。解决方法是在任务开始时先记录开始时间,在任务结束时再更新状态,而不是处理完立刻更新。

5.5 实用排查速查表

现象可能原因优先排查方向
表格某列全是空值模型输出的字段名与预期不一致打印原始 JSON,对照字段别名表
日期显示为数字单元格日期格式未设置设置 YYYY-MM-DD number_format
中文乱码CSV 编码问题改用 utf-8-sig 或指定 GBK
导出中途异常退出内存不足或 API 超时查看日志,检查分页参数
重复行数据任务非幂等检查状态文件写入时机
表格样式丢失用了只写模式后二次加载用 load_workbook 做补偿样式

这套速查表是我每次排障的基础。说句实在话,很多问题单独看都微不足道,但它们组合在一起就会消耗大量时间。把这些高频问题固化成检查清单,新同事接手时也能快速上手。

5.6 几点经验补充

最后再分享几个零碎但重要的经验。DeepSeek 的响应 JSON 里字段排序不是固定的,所以永远不要依赖 JSON 字段顺序,列顺序必须在映射阶段就确定。还有,如果业务方反馈表格打开慢,十有八九是列宽估算太保守导致列数过多,或者单个 sheet 数据量太大,建议把超大表拆分成多个 sheet 分月存储。另外,任何代码改动上线后,都要先用一份真实业务数据跑一遍全链路,而不是拿测试数据碰运气——测试数据掩盖过的问题太多了,真实数据的脏程度远超预期,先用两三天真实数据验证再全量上线,才比较稳妥。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询